diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa2ed31..80dedef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,18 +11,24 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + submodules: recursive - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable - - name: Run Forge install - run: forge install + - uses: actions/setup-node@v4 + with: + node-version: 20 - - name: Run Forge build - run: forge build --sizes + # @openzeppelin/upgrades-core is required by the OpenZeppelin Foundry Upgrades plugin + - name: Install Node dependencies + run: npm install - - name: Run Forge tests - run: forge test -vvv --ffi \ No newline at end of file + # `make test` does the full build first: the upgrade-safety validation rejects an + # incremental one. Same target a contributor runs locally. + - name: Build and test + run: make test diff --git a/.gitignore b/.gitignore index e13c4e7..269f69f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ broadcast/ lib/ out/ docOut/ +lcov.info cache/ .~lock.test.odt# nethereum-gen.settings @@ -14,3 +15,4 @@ cache_hardhat/ #drawio *.bkp *.dtmp +history diff --git a/.gitmodules b/.gitmodules index c1b7892..cc79950 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,9 @@ [submodule "lib/openzeppelin-foundry-upgrades"] path = lib/openzeppelin-foundry-upgrades url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/SnapshotEngine"] + path = lib/SnapshotEngine + url = https://github.com/CMTA/SnapshotEngine diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d1ac1df --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,213 @@ +# IncomeVault — Agent Guide + +> **Note — keep in sync:** `AGENTS.md` and `CLAUDE.md` must always be **identical**. Any edit to one must be applied verbatim to the other. + +> **Note — commit messages:** After each group of modifications or each feature added, always provide a **one-line GitHub commit message** (Conventional-Commits style, e.g. `feat: ...`, `fix: ...`, `docs: ...`). +> +> **Never put `!` in a commit message** — not as the breaking-change marker (`feat!: ...`), not anywhere else. In an interactive bash, `!` inside double quotes triggers history expansion, so `git commit -m "feat!: ..."` aborts with `bash: !: unrecognized history modifier`. Signal a breaking change with an uppercase `BREAKING CHANGE:` line in the commit body instead, and keep the subject line free of `!`. + +## What this project is + +`IncomeVault` is a CMTA prototype smart contract that distributes coupon/dividend payments to the holders of a security token. Dividends are deposited in the vault in an **ERC-20 payment token** (e.g. USDC), segregated per distribution date (`time`), and claimed pro-rata using an on-chain snapshot read through the **`ISnapshotState`** interface ([SnapshotEngine](https://github.com/CMTA/SnapshotEngine)). + +The vault is **token agnostic**: it never calls the token, only the snapshot source. Any contract implementing `ISnapshotState` works — the external `SnapshotEngine` bound to a CMTAT (or to any ERC-20), a token embedding the snapshot modules, or a custom implementation. + +> **The contracts are NOT audited.** Do not present them as production-ready. + +## Key concepts + +- **Not an ERC-4626 vault, deliberately** — a 4626 share entitles whoever holds it *now*; a dividend is allocated by **record date**. `doc/README.md` → "Comparison with ERC-4626 / ERC-7540 vaults" has the full reasoning and the cases where 4626/7540 *would* be right. Do not "standardise" this onto 4626. +- **Segregated deposits by `time`** — `time` is a Unix timestamp identifying a distribution. State is keyed by it: `segregatedDividend[time]`, `segregatedClaim[time]`, `claimedDividend[holder][time]`. +- **Claim flow** — the snapshot source schedules a snapshot at `time` → deposit role calls `deposit(time, amount)` → operator calls `setStatusClaim(time, true)` → holders call `claimDividend(time)` / `claimDividendBatch(times)`. +- **The EIP-712 domain version is `"1"` and must stay `"1"`.** It is set in `__IncomeVaultBase_init_unchained`; bumping it with a release would invalidate every ERC-7741 signature already issued. ERC-7741's id `0xa9e50872` **is** advertised through `supportsInterface` (the standard requires it) — unlike ERC-7540's operator id, which is not. +- **`IIncomeVault` is the stated API and is compiler-enforced.** It is inherited by `IncomeVaultInternal`, the common base of both payout paths, so every deployable *and* any embedded host implements it — add a public function to the distribution surface and you add it here too. It owns the `TIME_ERROR_CODE` enum, because `validateTimeCode` returns it. Keep it inheriting **nothing**: `type(...).interfaceId` covers only directly-declared selectors, and both deployment variants advertise that id. +- **Directory layout says what a file *is*.** `deployment/` holds only deployable contracts, `modules/` only abstract capability mixins, `storage/` only declarations, `interfaces/` only interfaces, and `IncomeVaultBase.sol` is the single composition root at the root. **`public/` is the exception and is deliberate** (M-5): it groups the external surface by *who may call*, because on a compliance contract the gated/ungated boundary is the first thing a reviewer checks. Do not merge `IncomeVaultOpen` and `IncomeVaultRestricted` into one "distribution module". +- **One capability, one module, one ERC-7201 namespace.** The project runs four namespaces: `IncomeVaultInternal` (distribution), `SnapshotSource`, `Operator` and `ERC7741Module`. Claim delegation is `IncomeVaultOperatorModule`, not a mapping in the distribution struct (M-6). A new capability with state gets its own namespace — never a field appended to someone else's. +- **Claim delegation reuses ERC-7540's operator signatures exactly.** `IERC7540Operator` must keep `type(...).interfaceId == 0xe3bc4e65`; a test asserts it. Do **not** add that id to `supportsInterface` — the vault is not an asynchronous vault and must not advertise as one. +- **Snapshot source** — reached through the three hooks of `IncomeVaultSnapshotCore`, never a direct call. `IncomeVaultSnapshotModule` is the standalone answer: an `ISnapshotSource` set at initialization, never zero, exposed by `dividendSnapshotSource()` and stored in its **own** ERC-7201 namespace. `ISnapshotSource` (`src/interfaces/ISnapshotSource.sol`) declares exactly the three functions the vault calls — a strict subset of the SnapshotEngine's `ISnapshotState`, signatures verbatim, so every `ISnapshotState` implementation satisfies it. **Do not add an ERC-165 guard on it**: the canonical `SnapshotEngine` advertises no id for it and the guard would reject it. If the vault ever needs a fourth function, add it here — not by widening back to `ISnapshotState`. +- **`transferDividendSelf` is a self-call helper with no role check.** Its only protection is `msg.sender == address(this)` (raw `msg.sender`, never `_msgSender()`, so a forwarder cannot impersonate the vault). It exists because `try`/`catch` needs an external call. Never relax that guard, and never add another public entry point to `_transferDividend`. +- **Every payout must come out of its own period.** `_transferDividend` refuses an amount larger than `unclaimedDividend(time)`. Without it, a claim made after a mid-window sweep is funded from another period's deposit. Unreachable in normal operation — a period's entitlements sum to at most its deposit — so the check only bites when a period has been over-swept. +- **`segregatedDividend` is the pro-rata denominator, not a balance.** It is fixed at the deposit for the whole period and never reduced by a payout. What a period still holds is `unclaimedDividend(time) = segregatedDividend - paidDividend`, and that is what bounds `withdraw`. Never bound a sweep by `segregatedDividend` alone — that let a fully-claimed period drain another period's funds. +- **`_setStatusClaim` is idempotent and owns `_openClaimCount`.** It is the only writer of the claim status; a repeated write returns early so the counter stays exact. `setDividendSnapshotSource` depends on that counter reaching zero, so any new path that changes a claim status must go through it. +- **Pro-rata formula** — `senderDividend = (senderBalance * segregatedDividend[time]) / tokenTotalSupply`, rounded down. Dust stays in the vault; the issuer withdraws it after `timeLimitToWithdraw`. +- **Claim window** — `validateTime` / `validateTimeCode` reject a claim when the claim is not activated, `block.timestamp < time` (too early), or `block.timestamp > time + timeLimitToWithdraw` (too late). +- **Access control — authorization-hook pattern.** The logic contracts declare *what* is protected: one `internal view virtual` hook per capability, invoked by a modifier, declared **without a body**. The deployment contract declares *who*: `IncomeVault` overrides every hook with `onlyRole(...)`, `IncomeVaultOwnable2Step` with `onlyOwner`. The two are chosen at deployment and are not interchangeable. Capability table: `doc/README.md` → Access control. +- **The payout paths must not inherit a policy.** `IncomeVaultOpen`/`IncomeVaultRestricted` inherit `IncomeVaultValidationCore` (the `_validateTransfer` question) — never `IncomeVaultValidationModule` (one answer). The module is inherited by the **deployment** contracts, like the access-control base. Re-coupling them makes the logic unembeddable in any CMTAT: C3 fails with `Error (5005)`, which no override can repair. Same rule for the snapshot side: they inherit `IncomeVaultSnapshotCore` (the three questions) — never `IncomeVaultSnapshotModule` (one answer). `test/mocks/` holds the two compile-time guards: `EmbeddedDividendHostMock.sol` (a non-CMTAT host) and `CMTATDividendHostMock.sol` (a `CMTATUpgradeableInternalSnapshot` paying its own dividends). +- **`ReentrancyGuardTransient` is listed last** in the payout paths, matching CMTAT's ordering. Moving it earlier reintroduces the same unresolvable linearization failure. +- **Transfer restriction** — every payout, pull (`claimDividend`) **and** push (`distributeDividend`), goes through `IncomeVaultValidationModule`: pause, address freeze, and an optional `IRuleEngine`. Rejected payouts revert with `IncomeVault_InvalidTransfer(from, to, value)`; in a batch one blocked holder reverts the whole call rather than being skipped, so a compliance failure cannot be silently dropped. Any new payout path must call `_validateTransfer` too. +- **RuleEngine is read-only here** — the vault uses `IRuleEngine.canTransfer` only. It is not a bound token, so `transferred(...)` would revert, and a payout must not mutate stateful rules. +- **Upgradeable** — deployed behind an OpenZeppelin **Transparent Proxy**; `initialize(...)` replaces the constructor. State is held in an **ERC-7201** namespaced struct (`IncomeVault.storage.IncomeVaultInternal`, slot `0xe4f8b033…0c00`), as in OZ Upgradeable and CMTAT v3 — there is no `__gap` and no sequential storage slot. +- **Gasless / meta-tx is a deployment decision** (M-8). `IncomeVaultBase` has **no** ERC-2771; `IncomeVaultBaseERC2771` adds CMTAT's `ERC2771Module` plus the `_msgSender`/`_msgData`/ `_contextSuffixLength` overrides that resolve the `ERC2771ContextUpgradeable` / `ContextUpgradeable` diamond, and both shipped deployments inherit that. The forwarder is set in the constructor and is **immutable**. Do not move `ERC2771Module` back into `IncomeVaultBase`: a forwarder can name any `_msgSender()`, so a deployment that does not want one must be able to not have one. `test/mocks/NoForwarderVaultMock.sol` is the guard. +- **Reentrancy** — claims use `nonReentrant` from `ReentrancyGuardTransient` (EIP-1153); `_transferDividend` sets `claimedDividend[holder][time] = true` *before* the ERC-20 transfer. + +## File tree + +``` +src/ +├── IncomeVaultBase.sol # The composition root: assembles the modules, +│ # __IncomeVaultBase_init_unchained. Hooks left abstract. +│ # NO access-control, NO validation, NO meta-tx policy. +├── IncomeVaultBaseERC2771.sol # The base plus ERC-2771: forwarder constructor and the +│ # _msgSender/_msgData/_contextSuffixLength overrides. +│ # Both shipped deployments inherit THIS one. +├── deployment/ # What you actually deploy — nothing abstract lives here +│ ├── IncomeVault.sol # AccessControlModule; every hook -> onlyRole(...) +│ └── IncomeVaultOwnable2Step.sol # Ownable2StepUpgradeable; every hook -> onlyOwner +├── public/ # The external surface, split by WHO MAY CALL. Deliberate: +│ │ # the gated/ungated boundary is the thing a reviewer of a +│ │ # compliance contract checks first. Do not merge these two. +│ ├── IncomeVaultOpen.sol # Permissionless: claimDividend, claimDividendBatch, validateTime(Code|Batch) +│ └── IncomeVaultRestricted.sol # Role-gated: deposit, withdraw, withdrawAll, distributeDividend, +│ # setStatusClaim, setTimeLimitToWithdraw +├── modules/ # Abstract capability mixins, one per capability +│ ├── IncomeVaultInternal.sol # Distribution state: the ERC-7201 struct + getters, +│ │ # _computeDividend(Batch), _transferDividend, _setStatusClaim +│ ├── IncomeVaultValidationCore.sol # ONLY `_validateTransfer` — inherits nothing, keep it that way +│ ├── IncomeVaultValidationModule.sol # Pause + Enforcement + RuleEngine; canTransfer, +│ │ # setRuleEngine, detectTransferRestriction. Hooks abstract. +│ ├── IncomeVaultSnapshotCore.sol # ONLY the 3 snapshot hooks — inherits nothing, keep it that way +│ ├── IncomeVaultSnapshotModule.sol # One answer: a stored ISnapshotSource in its OWN ERC-7201 +│ │ # namespace; dividendSnapshotSource, setDividendSnapshotSource +│ ├── IncomeVaultOperatorModule.sol # ERC-7540 claim delegation in its OWN ERC-7201 namespace; +│ │ # setOperator, isOperator, _requireHolderOrOperator +│ ├── ERC7741Module.sol # EIP-712 signed operator authorisation, own ERC-7201 namespace +│ ├── VersionModule.sol # VERSION constant behind IERC3643Version.version() +│ └── Ownable2StepERC165Module.sol # ERC-165 advertisement of ERC-173 / Ownable2Step +├── interfaces/ +│ ├── IIncomeVault.sol # The stated distribution API + the TIME_ERROR_CODE enum; +│ │ # inherited by IncomeVaultInternal so solc enforces it +│ ├── ISnapshotSource.sol # The 3 snapshot functions the vault calls — subset of ISnapshotState +│ ├── IERC7540Operator.sol # The ERC-7540 operator subset, verbatim; id MUST stay 0xe3bc4e65 +│ └── IERC7741.sol # Signed operator authorisation; id MUST stay 0xa9e50872 +└── storage/ # Declaration-only: nothing here has behaviour + ├── IncomeVaultInvariantStorage.sol # Custom errors and events shared by every variant + └── IncomeVaultRolesStorage.sol # The four INCOME_VAULT_*_ROLE constants — inherited ONLY by IncomeVault + +script/ +├── DeployIncomeVault.s.sol # role-based variant; `deploy(config)` split from `run()` +└── DeployIncomeVaultOwnable2Step.s.sol # single-owner variant + +test/ +├── HelperContract.sol # Constants + `_deployContracts()` / `_deployOwnableVault()` +├── IncomeVault.t.sol # Single claim: deposit, claim, pause, freeze, error cases +├── IncomeVaultBatch.t.sol # claimDividendBatch behaviour +├── IncomeVaultRestricted.t.sol # deposit/withdraw/withdrawAll/distributeDividend + access control +├── IncomeVaultStorage.t.sol # ERC-7201: slot derivation, field offsets, the two namespaces +├── AccessControlHooks.t.sol # Both variants: every hook accepts/rejects, role separation, +│ # Ownable2Step handover, ERC-165 +├── VersionModule.t.sol # version() on EVERY deployable contract — keep exhaustive +├── CodeQuality.t.sol # regressions for CLAUDE_ANALYSIS.md findings, incl. the +│ # push/pull claim-window parity (H-1) and restrictions (H-2) +├── IncomeVaultInterface.t.sol # M-7: drive a proxy through IIncomeVault alone, ERC-165 id, +│ # embedded hosts present the same interface +├── SnapshotSource.t.sol # I-1: a 3-function source is enough; the real engine still fits +├── SetDividendSnapshotSource.t.sol # M-2 setter: gated on openClaimCount() == 0, zero-address, event +├── RuleEngineIntegration.t.sol # End-to-end with RuleEngine + RuleWhitelistMock +├── DistributeBestEffort.t.sol # A-4: one blocked holder is skipped, not reverted +├── DepositBatch.t.sol # depositBatch, incl. the per-transaction gas comparison +├── UnclaimedDividend.t.sol # E-3: saturating residue, per-period withdraw bound +├── Operator.t.sol # ERC-7540 operator subset: setOperator, claim-for +├── OperatorAuthorization.t.sol # ERC-7741 signed authorisation, EIP-712, ERC-1271 +├── Deactivate.t.sol # deactivateContract, permanent kill +├── EdgeCases.t.sol # zero supply, zero balance, boundary times +├── script/Deploy.t.sol # C-4: both deployment scripts, config validation +├── NoForwarderDeployment.t.sol # M-8: a vault on the plain base has no isTrustedForwarder, +│ # the shipped ones still do, and payouts work either way +├── invariant/ # handler + 7 invariants; validate any change by sabotaging +│ # the contract and checking an invariant actually fails +└── mocks/ + ├── ERC20PaymentMock.sol # Minimal ERC-20 used as payment token + ├── MinimalSnapshotSourceMock.sol # I-1: implements ISnapshotSource and nothing else + ├── IncomeVaultOverrideMock.sol # compile guard for the `virtual` convention + ├── NoForwarderVaultMock.sol # M-8 guard: a deployment on IncomeVaultBase, no ERC-2771 + ├── EmbeddedDividendHostMock.sol # M-1/M-2 compile guard: a non-CMTAT host paying dividends + └── CMTATDividendHostMock.sol # M-1/M-2 compile guard: a CMTATUpgradeableInternalSnapshot + # paying its own dividends, its own snapshots, own canTransfer +``` + +Tests deploy the vault through `Upgrades` (openzeppelin-foundry-upgrades), which requires `--ffi`, `@openzeppelin/upgrades-core` from npm, and a **full** build (`forge clean && forge build`). + +## Other important files + +| Path | Purpose | +| --- | --- | +| `foundry.toml` | solc 0.8.36, optimizer 200 runs, EVM `prague`, `ffi`, `ast`, `build_info`, `storageLayout`, `fs_permissions` on `./out` (needed by OZ Upgrades) | +| `remappings.txt` | `CMTAT/`, `RuleEngine/`, `SnapshotEngine/`, `OZ/`, `OZUpgradeable/`, `@openzeppelin/*`, `openzeppelin-foundry-upgrades/`, `forge-std/` | +| `hardhat.config.js` | Only used for `solidity-docgen` (`npx hardhat docgen`), mirrors the Foundry solc settings. `settings` must stay **inside** `solidity` — at the top level Hardhat ignores it and the optimizer silently does not run. `docgen.outputDir` writes straight to `doc/solidityAPI`, so regenerating needs no manual move | +| `package.json` | npm scripts: lint (ethlint/prettier), `uml`, `surya:*`, `docgen`; dependency `@openzeppelin/upgrades-core` | +| `.soliumrc.json`, `.soliumignore` | Ethlint/Solium configuration | +| `CHANGELOG.md` | changelog.md conventions; current release heading `2.0.0-rc0`. The `-rc` suffix marks the candidate and is **not** carried into `VERSION`, which stays `2.0.0` — the two are allowed to differ only in that suffix | +| `doc/README.md` | The reference doc: snapshot source, roles table, claim restrictions, formula, threat model & FAQ, plus the technical choices (upgradeability, pause, token agnosticism, reentrancy, gasless GSN/ERC-2771) and the schema/graphs | +| `doc/cmtat-standard/` | The CMTA framework functional specifications PDF, plus four documents. `CMTAT-Distribution-impl.md` is the comparison of section 3.2.4 (functionalities 27-32) against this implementation, keeping only a summary line per proposal. `CMTAT-Distribution-Amendments.md` holds the nine **amendments** (C-1..C-9) to functionalities that already exist; `CMTAT-Distribution-Additions.md` the three **additions** (A-1..A-3) the specification does not describe at all; `CMTAT-Distribution-ERC4626.md` the question behind C-9 — what the deposit may be held as between functionalities 29 and 30. Each quotes the source text it argues from **verbatim**, so it is readable without the PDF: 3.2.4 in all three, the 3.1.1 debt attributes in the amendments (C-6, C-8 cite them), and the ERC-4626 `asset` / `totalAssets` / `previewRedeem` / `redeem` requirements in the 4626 one. Verify a quote against the source before editing it — the PDF via `pdftotext -layout`, ERC-4626 via `lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol`. Keep the reasoning for an id in exactly one file. `doc/README.md` keeps a summary and links here | +| `doc/script/check_sizes.py` | The EIP-170 check, run by `make build` after a **full** `forge build`. `forge build --sizes` cannot be used: it fails on `CMTATDividendHostMock`, a never-deployed compile guard that is over the limit by design, and `--skip` would make the compilation partial so the Upgrades plugin rejects the build-info. This scopes the check to `src/` via each artifact's `compilationTarget`, and **fails when it measures nothing** — every filter can legitimately empty the set, so "nothing to report" and "nothing was looked at" would otherwise print the same reassuring line | +| `doc/script/coverage-README.md` | The note copied into `doc/coverage/README.md` by `make coverage-report`; that directory is recreated each run, so edit it here | +| `doc/script/gen_toc.py` | Regenerates the tables of contents in `doc/README.md` and `doc/TOOLCHAIN.md`: `python3 doc/script/gen_toc.py doc/README.md doc/TOOLCHAIN.md`, or `--check` to fail when one is stale. They are **generated markdown lists between `` markers**, never `[TOC]` — that is Doxygen syntax and GitHub renders it as literal text. Do not hand-edit inside the markers; re-run after adding or renaming a heading | +| `doc/TOOLCHAIN.md` | Tested dependency versions, doc-generation and lint commands | +| `doc/solidityAPI/index.md` | Generated Solidity API. Refresh with `npx hardhat docgen`; it writes in place. **`solidity-docgen` must be patched first** — it matches tag names with `\w`, which excludes `$`, so `@return $` aborts the run and `@param $` silently drops the description. `npm install` reverts the patch; re-apply with the `solidity-docgen-doc` skill's `patch_docgen.sh` (D-2) | +| `doc/surya/`, `doc/schema/` | Surya call graphs, inheritance graphs and markdown reports (one per `src/**/*.sol`), UML class diagram, PlantUML sources and the remaining drawio diagrams. Regenerate with `npm run surya:graph` + `surya:inheritance` + `surya:report` (output goes to the scratch `docOut/`, then replaces `doc/surya/`) and `npm run uml` | +| `doc/schema/plantuml/` | PlantUML sources (`.puml`) and their rendered `.png`. `incomevault-architecture` is the overview embedded in **both** READMEs — keep it low-detail; `incomevault-global`, `-claimdividend`, `-ruleengine` and `-segregated-deposit` are the detailed ones in `doc/README.md`. The `.puml` is the source of truth — edit it, then re-render with `plantuml -tpng doc/schema/plantuml/.puml` and **look at the PNG**: PlantUML exits 0 on warnings and draws them into the image as a yellow banner. Embed the image in the docs, never the source text | +| `doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS_SECOND.md` | Second-pass code-quality review. Ids restart at `A-1`; read it **with** `CLAUDE_ANALYSIS.md`, which it does not repeat. Includes two explicit do-not-change verdicts (I-1, G-2) recorded so they are not re-opened | +| `doc/audits/AUDIT_OVERVIEW.md` | The security overview: scope, both analyzers' results, the two real findings, and the substantive defects the reviews caught that static analysis did not | +| `doc/audits/tools/vX.Y.Z/` | Per-release tool output: `slither-report.md`, `aderyn-report.md` and a `*-feedback.md` triaging every finding. Filter Slither on `lib`, not on dependency names, and check the report cites no `lib/` before trusting a count. Aderyn writes its `Found in` links with the **absolute** repo path of the machine that ran it — rewrite them to repo-relative before committing | +| `doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS.md` | Code-quality review (not a security audit). Findings carry stable ids (`A-1`, `H-2`, …) — cite them in commits and in the other docs, never in a contract comment, and read the Outstanding table before re-opening anything | +| `script/` | Deployment scripts, one per variant. Excluded from the style check and from coverage; their `require` messages are deliberate. Tested by `test/script/Deploy.t.sol` | +| `Makefile` | The task definitions — `make help` lists them. npm scripts and CI both delegate here, so there is one definition. Every compiling target does a **full** build because the Upgrades plugin rejects an incremental one — which is also why `--sizes` is not used: it fails on a test mock, and `--skip` would make the build partial. `doc/script/check_sizes.py` runs the EIP-170 check afterwards, scoped to `src/` | +| `.github/workflows/ci.yml` | CI: recursive checkout, `npm install`, then `make test` — the same target a contributor runs, so CI and local cannot drift. It does **not** use `--sizes` or a verbosity flag | + +## Dependencies (tested versions) + +- Solidity **0.8.36**, EVM target `prague` (contracts declare `pragma ^0.8.24`) +- CMTAT **v3.3.0-rc3** (submodule `lib/CMTAT`) +- RuleEngine **v3.0.0-rc5** (submodule `lib/RuleEngine`) +- SnapshotEngine **v0.5.0** (submodule `lib/SnapshotEngine`) +- openzeppelin-contracts **v5.7.0** (submodule) +- openzeppelin-contracts-upgradeable **v5.7.0** (submodule) +- openzeppelin-foundry-upgrades **v0.4.2** (submodule) +- forge-std **v1.16.1** (submodule) + +CMTAT v3.3.0 and RuleEngine v3.0.0 are release candidates — they are what the CMTA ecosystem is currently aligned on (RuleEngine v3.0.0-rc5 pins CMTAT v3.3.0-rc3). Submodules are **not** updated automatically — pin them to a release tag, never to an intermediary commit. + +## Common commands + +> Test helpers live in `HelperContract`: `_deployContracts()` builds the CMTAT, snapshot engine, payment token and role-based vault; `_deployOwnableVault()` adds the single-owner variant. Do not re-inline either — five suites used to carry a copy. + +```bash +make help # every target, and why the build must be full +make install # submodules + npm dependencies +make test # THE way to run the suite: full build, then forge test --ffi +make coverage # src/ only, tests and mocks excluded + +# `forge test --ffi` on its own fails every test after an incremental build — the Upgrades plugin +# rejects partial build-info. Use `make test` unless nothing has been recompiled since the last clean. +forge test --ffi --match-contract IncomeVaultTest # fine right after a `make build` + +npm run lint:sol # ethlint on src/ +npm run lint:sol:prettier # prettier-plugin-solidity +npm run surya:graph && npm run surya:inheritance && npm run surya:report # then replace doc/surya/ with docOut/ +npm run uml && npx hardhat docgen + +slither . --checklist --filter-paths "openzeppelin-contracts|test|CMTAT|RuleEngine|SnapshotEngine|forge-std" > slither-report.md +``` + +## Conventions & invariants + +- **Versioning:** `CHANGELOG.md` follows [changelog.md](https://changelog.md/) and states the project's own semver rule at the top — an **incompatible proxy storage change, a changed external API, or a reworked internal architecture is a MAJOR bump**. Add an entry for any user-visible contract change. A release bumps two things that must agree: the `CHANGELOG.md` heading and the `VERSION` constant in `src/modules/VersionModule.sol`. Add every new deployable contract to `test/VersionModule.t.sol`, which is exhaustive by design. +- **Upgrade safety:** the state lives in the ERC-7201 struct `IncomeVaultInternalStorage` (namespace `IncomeVault.storage.IncomeVaultInternal`). Append new fields to the **end** of that struct; never reorder or remove existing ones. Do **not** reintroduce `uint256[50] private __gap` — namespaced storage replaces it, and the contract must keep declaring zero sequential slots. A new module with its own state gets its own namespace, never a sequential variable; recompute its slot with `SlotDerivation.erc7201Slot()` and keep the derivation comment above the constant. `IncomeVault` has an `/// @custom:oz-upgrades-unsafe-allow constructor` annotation — keep it and keep `_disableInitializers()` in the constructor. +- **Authorization hooks:** a hook is `internal view virtual` on the declaration **and on every override** — `view` is what makes "an auth hook cannot mutate state" compiler-enforced, and it is free. CMTAT declares its hooks non-`view`; overriding them `view` is legal (an override may tighten mutability) and is what this project does. Override bodies stay **empty**, with the check riding on the modifier (`onlyRole(...)` / `onlyOwner`), never a bare `_checkRole` call. A new guarded capability means a new hook plus an override in **every** deployment variant. +- **`_authorizeRuleEngineManagement` shares its name with CMTAT's on purpose.** Both this project's `IncomeVaultValidationModule` and CMTAT's `ValidationModuleRuleEngine` sit on the same `ValidationModuleRuleEngineInternal`, whose ERC-7201 slot is a hardcoded constant — so a contract inheriting both has exactly **one** RuleEngine. One capability, one hook; a single override answering both declarations is correct. **Do not prefix or rename it** (M-4): two names over one slot means two policies for one door, and the weaker wins. +- **Role constants live with the layer that enforces them.** They belong in `IncomeVaultRolesStorage`, inherited only by `IncomeVault` — never in `IncomeVaultInvariantStorage`, or the Ownable variant would publish a role it never checks. +- **`@inheritdoc` needs the base imported by name** in the referencing file, even when it is already in scope through inheritance; otherwise the build fails with "references inexistent contract". +- **Claim accounting:** always set `claimedDividend[holder][time]` before any external call; keep `nonReentrant` on the claim entry points. +- **Deposits vs. open claims:** do not deposit for a `time` whose claim status is already `true` — it dilutes holders who have not yet claimed. +- **The claim window is shared.** `TIME_ERROR_CODE`, `_timeCode` and `_revertOnInvalidTime` live in `IncomeVaultInternal` so both the pull path (`claimDividend`) and the push path (`distributeDividend`) apply them. Any new payout path must call them too: without the "too early" bound, `ISnapshotState` falls back to live balances and the payout is computed from the wrong figures. +- **ERC-20 safety:** use `SafeERC20` (`safeTransfer` / `safeTransferFrom`) for the payment token. +- **Style:** 4-space indent, NatSpec (`@notice` / `@param` / `@dev`) on public and internal functions, custom errors prefixed `IncomeVault_`, named imports (`import {X} from "..."`), `SPDX-License-Identifier: MPL-2.0` header on every Solidity file. +- **Never point at a documentation *path* from a contract comment.** Documentation moves — this repo has already reorganised `doc/` twice — but a comment is frozen in the verified source of a deployed contract and can never be corrected. Worse, someone reading that source on a block explorer has no `doc/` to open. Put the substance in the comment and drop the pointer; if the derivation is genuinely too long, state the conclusion and let the doc carry the derivation with no cross-reference either way. **The only permitted reference is `CHANGELOG.md`** — a bare filename that survives any reorganisation and carries an instruction actionable without opening it (`VersionModule` is the one in-tree use). Do not cite audit reports or finding ids either, even by bare filename: state *why* the code is shaped the way it is, so a reader of the verified source needs nothing else. Do not cite the test that asserts a property — give the property and the consequence of breaking it. Mocks and tests are exempt; they are never deployed. +- **Documentation:** the README and `doc/` must state that the contracts are not audited; keep that disclaimer intact. + +## Known quirks (verify before "fixing") + +- `distributeDividend` deliberately bypasses the ValidationModule (no pause / freeze / RuleEngine check): it is an issuer-driven push, unlike the holder-driven claims. +- The `newDeposit` event keeps its lowercase name for backward compatibility with the v1 ABI. +- `IncomeVaultInvariantStorage` declares `event DividendSnapshotSourceSet`, and the getter is `dividendSnapshotSource()`. **Never name either of them `snapshotEngine`**: CMTAT declares `snapshotEngine()` with the same parameters and a *different return type*, which Solidity cannot reconcile by any override — see the snapshot bullet in Key concepts. +- `doc/coverage/` is generated but **committed** — regenerate it with `make coverage-report` in the same commit as any `src/` change, so the tracked report never describes a codebase other than the one beside it. It was git-ignored until this decision, because the repo once carried the *RuleEngine* project's coverage output long enough to read as authoritative; that risk is now handled by refreshing the report rather than by hiding it. `make coverage-report` deletes and recreates the directory, so the only hand-written file in it, `README.md`, is copied back in from `doc/script/coverage-README.md`. `make coverage` gives a summary, `make coverage-report` the HTML in `doc/coverage`. Two files report 0% because they declare hooks with no bodies (`IncomeVaultSnapshotCore`, `IncomeVaultValidationCore`), which is expected rather than a gap. diff --git a/CHANGELOG.md b/CHANGELOG.md index f000538..afbf64b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,5 +2,125 @@ Please follow [https://changelog.md/](https://changelog.md/) conventions. +## Semantic Version 2.0.0 + +Given a version number MAJOR.MINOR.PATCH, increment the: + +1. MAJOR version when the new version makes: + - Incompatible proxy **storage** change internally or through the upgrade of an external library (OpenZeppelin) + - A significant change in external APIs (public/external functions) or in the internal architecture +2. MINOR version when the new version adds functionality in a backward compatible manner +3. PATCH version when the new version makes backward compatible bug fixes + +See [https://semver.org](https://semver.org) + +## Type of changes + +- `Summary`: main new features/change with a description (keep it short) (not a changelog tag) +- `Added` for new features. +- `Changed` for changes in existing functionality. +- `Deprecated` for soon-to-be removed features. +- `Removed` for now removed features. +- `Fixed` for any bug fixes. +- `Security` in case of vulnerabilities. + +Reference: [keepachangelog.com/en/1.1.0/](https://keepachangelog.com/en/1.1.0/) + +Custom changelog tag: `Dependencies`, `Documentation`, `Testing` + +## Checklist + +> Before a new release, perform the following tasks + +- Code: Update the version name, variable VERSION +- Run formatter and linter + +```bash +forge fmt +forge lint +``` + +- Documentation + - Perform a code coverage: `make coverage-report`. The output lands in `doc/coverage/`, which is committed — regenerate it and include it in the release commit + - Perform an audit with several audit tools (Aderyn and Slither), update the report in the corresponding directory [./doc/audits/tools](./doc/audits/tools) + - Update surya doc by running the 3 scripts in [./doc/script](./doc/script) + - Update changelog + +## 2.0.0-rc0 + +### Breaking changes + +- The vault is no longer tied to the CMTAT. The holder balances and the total supply are read through the [`ISnapshotState`](https://github.com/CMTA/SnapshotEngine) interface, so **any** contract implementing it can be used as the snapshot source (the external `SnapshotEngine`, or a token embedding the snapshot logic). The state variable `CMTAT_TOKEN` (`ICMTATSnapshot`) is replaced by the snapshot source, reachable through `dividendSnapshotSource()`. +- `initialize` no longer takes an `IAuthorizationEngine`, removed by CMTAT v3. New signature: `initialize(address admin, IERC20 ERC20TokenPayment_, ISnapshotState snapshotEngine_, IRuleEngine ruleEngine_, uint256 timeLimitToWithdraw_)`. +- The vault no longer inherits the CMTAT `ValidationModule`. Its own `IncomeVaultValidationModule` composes the CMTAT `AccessControlModule`, `PauseModule` and `EnforcementModule` and calls the RuleEngine through the view entry point `IRuleEngine.canTransfer`. A rejected payout now reverts with `IncomeVault_InvalidTransfer(from, to, value)` instead of `CMTAT_InvalidTransfer`. +- `withdraw` and `withdrawAll` use `SafeERC20.safeTransfer` directly; the self-approval step and the error `IncomeVault_FailApproval` are removed. +- The state moved from sequential storage slots guarded by `uint256[50] private __gap` to a single **ERC-7201 namespaced storage** struct, as OpenZeppelin Upgradeable and CMTAT v3 do. Every `__gap` is removed and `IncomeVault` now declares **no** sequential storage slot at all. The external ABI is unchanged — `dividendSnapshotSource()`, `ERC20TokenPayment()`, `claimedDividend()`, `segregatedDividend()`, `segregatedClaim()` and `timeLimitToWithdraw()` are kept as explicit getters — but the storage layout is **not** compatible with a 1.x/2.0-rc deployment: this is a redeploy, not an upgrade. The snapshot source later moved out of `IncomeVault.storage.IncomeVaultInternal` into its own namespace (finding M-2), which shifted every remaining field of the internal struct down by one slot. + +### Added + +- A `Makefile` with the common tasks, and `make test` as the way to run the suite. The OpenZeppelin Upgrades plugin rejects an incremental build, so `forge test --ffi` after editing a contract fails every test with an error naming neither the cause nor the fix; `make test` does the full build first. Verified by reproducing the failure — plain `forge test` after an incremental build: 0 passed, 19 suites failed; `make test` from the identical state: 202 passed. `npm run test|build|coverage|lint` delegate to the same targets, and CI now runs `make test`. Finding C-3 of `CLAUDE_IMPROVEMENT.md`. +- [ERC-7741](https://eips.ethereum.org/EIPS/eip-7741) signed operator authorisation: `authorizeOperator`, `invalidateNonce`, `authorizations` and `DOMAIN_SEPARATOR`, in the new `ERC7741Module` with its own ERC-7201 namespace and the interface declared in `src/interfaces/IERC7741.sol`. A holder signs an EIP-712 message and anyone can submit it, so a custodian can be appointed without the holder ever transacting. Signatures are checked with `SignatureChecker`, so ERC-1271 contract wallets work. `type(IERC7741).interfaceId` equals the standard's `0xa9e50872` (asserted), and both deployment variants advertise it through `supportsInterface` as the standard requires. +- Deployment scripts: `script/DeployIncomeVault.s.sol` and `script/DeployIncomeVaultOwnable2Step.s.sol`, using the same `Upgrades` plugin as the tests so the deployment path is the tested one. Configuration comes from the environment, and `deploy(config)` is separated from `run()` so `test/script/Deploy.t.sol` exercises the same code without one. The scripts reject what the contract cannot check for itself — a payment token, snapshot source or rule engine that is not a contract. Finding C-4 of `CLAUDE_IMPROVEMENT.md`. +- `IERC7540Operator` (`src/interfaces/IERC7540Operator.sol`), declaring the three members whose signatures are ERC-7540's verbatim. `type(IERC7540Operator).interfaceId` equals the standard's `0xe3bc4e65`, asserted in the tests, so a signature drift breaks the build. The vault deliberately does **not** advertise that id through ERC-165 — it shares the operator methods, it is not an asynchronous vault. +- Claim delegation in the shape of ERC-7540: `setOperator(operator, approved)`, `isOperator(controller, operator)`, `claimDividendFor(holder, time)` and `claimDividendBatchFor(holder, times)`, with an `OperatorSet` event matching the standard. The dividends always go to the holder — an operator pays the gas and picks the moment, and can never redirect the payment. Finding E-1 of `CLAUDE_IMPROVEMENT.md`. +- `paidDividend(time)` and `unclaimedDividend(time)`, reporting how much a dividend time has paid out and how much it still holds. The residue an issuer sweeps is now readable on-chain instead of being reconstructed from `DividendClaimed` events. Costs **+22,274 gas on the first claim of each period** (66,687 -> 88,961, a cold `SSTORE` for the new per-time counter); later claims for the same period pay the warm price. Finding E-3. +- `depositBatch(times[], amounts[])`, crediting several dividend times in one transaction and pulling the payment token once for the total. Measured for three periods: **136,263 gas in-call against 116,812 for three separate `deposit` calls** — the batch is the more expensive of the two per call, and wins only once the 21,000 intrinsic cost of each saved transaction is counted (157,546 against 179,812 in total). It is a transaction-count optimisation, not a cheaper deposit. Finding E-2 of `CLAUDE_IMPROVEMENT.md`. +- `distributeDividendBestEffort`, a variant of `distributeDividend` that **skips** a holder whose payout is refused instead of reverting the whole run, returning `(paidCount, skipped[])` and emitting `DividendDistributionSkipped(time, holder, reason)` with the raw revert data. Each payout goes through an external self-call wrapped in `try`/`catch`, so a skipped holder is left completely untouched — not marked as claimed — and can still claim later. `distributeDividend` keeps its all-or-nothing semantics. Finding A-4 of `CLAUDE_IMPROVEMENT.md`. +- `setSnapshotEngine`, allowing the snapshot source to be migrated without a proxy upgrade — but only while **no claim period is open**, since amounts are computed from the source at claim time rather than fixed at deposit. Gated by a new `_authorizeSnapshotEngineManagement` hook (`DEFAULT_ADMIN_ROLE` / owner) and by the new `openClaimCount()` view; reverts `IncomeVault_ClaimPeriodOpen`. Note the gate narrows the hazard rather than removing it — see the security note in `doc/README.md`. Finding A-3 (option b) of `CLAUDE_IMPROVEMENT.md`. +- `openClaimCount()`, the number of dividend times whose claims are currently open. +- Events for every state write that had none: `ClaimStatusSet`, `ERC20TokenPaymentSet`, `TimeLimitToWithdrawSet`, `Withdraw` and `WithdrawAll`. Each is emitted from the internal `_setX` helper that performs the write, so they also fire during `initialize` — a vault configured once at deployment now has a complete on-chain trail. See `CLAUDE_ANALYSIS.md` C-1 to C-4. +- `doc/audits/CLAUDE_ANALYSIS.md`, a code-quality review (not a security audit). +- `VersionModule`, exposing the release version through `IERC3643Version.version()`, as the CMTAT, RuleEngine and SnapshotEngine do. `VERSION` reads **2.0.0**: the release triggers the MAJOR rule stated above on three counts — an incompatible proxy storage change, a changed `initialize` signature, and a reworked internal architecture. The `-rc0` suffix on this heading marks the release as a candidate and is deliberately **not** carried into the contract constant, which reports the version the code will ship as. +- `IncomeVaultOwnable2Step`, a second deployment contract using a single ERC-173 owner (`Ownable2StepUpgradeable`) instead of roles. The variant is chosen at deployment and cannot be swapped afterwards. It **cannot express separated duties** — the owner both funds and drains the vault — so it suits simple deployments only. +- `Ownable2StepERC165Module`, advertising ERC-173 and the Ownable2Step selectors. + +### Changed + +- The payout paths no longer inherit a transfer-restriction policy. `IncomeVaultValidationCore` declares `_validateTransfer` and inherits nothing; `IncomeVaultValidationModule` is now one *answer* to it, built on the CMTAT modules, and is inherited by the two deployment contracts rather than by `IncomeVaultOpen`/`IncomeVaultRestricted`. `IncomeVaultBase` consequently knows nothing about pause, freeze or the RuleEngine, and `__IncomeVaultBase_init_unchained` lost its `ruleEngine_` argument. `ReentrancyGuardTransient` was also reordered to match CMTAT's convention. Together these mean a host that already owns those modules — a CMTAT with a snapshot engine — can embed the dividend logic instead of hitting an unresolvable `Error (5005)`. No behaviour change; all 202 tests pass unchanged. +- The snapshot source is no longer a stored address behind a `snapshotEngine()` getter. `IncomeVaultSnapshotCore` declares the three questions the payout paths actually ask — `_snapshotInfo` and the two `_snapshotInfoBatch` overloads — and inherits nothing. `IncomeVaultSnapshotModule` is one *answer* to them: an `ISnapshotSource` held in **its own** ERC-7201 namespace (`IncomeVault.storage.SnapshotSource`). This removes a name collision that no override list could repair: CMTAT already declares `snapshotEngine()` with the same parameters and a **different return type**, so a CMTAT could not embed the dividend logic at all. Renames, all pre-release: `snapshotEngine()` to `dividendSnapshotSource()`, `setSnapshotEngine` to `setDividendSnapshotSource`, `_authorizeSnapshotEngineManagement` to `_authorizeSnapshotSourceManagement`, the event `SnapshotEngineSet` to `DividendSnapshotSourceSet(ISnapshotSource indexed)` and the error `IncomeVault_SnapshotEngineWithAddressZeroNotAllowed` to `IncomeVault_SnapshotSourceWithAddressZeroNotAllowed`. `initialize` is unchanged. +- `IIncomeVault` (`src/interfaces/IIncomeVault.sol`) states the distribution API — claiming, funding, pushing payouts, claim administration and the state getters — so an integrator imports one interface instead of a concrete contract and its whole dependency graph. It is inherited by `IncomeVaultInternal`, the common base of both payout paths, so the compiler keeps it in step with the implementation, and both deployment variants advertise its id through `supportsInterface`. The enum `TIME_ERROR_CODE` moved from `IncomeVaultInternal` to `IIncomeVault`: it is the return type of `validateTimeCode` and therefore part of the stated API. Its ABI encoding (`uint8`) is unchanged; only the qualified Solidity name moves. ERC-7540's operator id is still deliberately not advertised. +- Claim delegation moved out of the distribution storage into `IncomeVaultOperatorModule` (`src/modules/IncomeVaultOperatorModule.sol`), with its own ERC-7201 namespace `IncomeVault.storage.Operator` (slot `0x70af7571...5500`). `setOperator`, `isOperator`, `_setOperator` and `_requireHolderOrOperator` were gathered there from `IncomeVaultOpen` and `IncomeVaultInternal`, so one capability now lives in one module with one namespace, as `ERC7741Module` already did. The external ABI is unchanged. `_isOperator` was the **last** field of `IncomeVaultInternalStorage`, so removing it shifts no other field — but the mapping's slot does move, which is why this had to happen before a deployment. +- The `src/` layout now says what each file **is**, following CMTAT's own convention. `src/deployment/` holds the two deployable contracts; `src/libraries/` is gone — it contained four abstract contracts and no `library` — with `IncomeVaultInternal` and `Ownable2StepERC165Module` moving to `src/modules/` and the two declaration-only contracts to `src/storage/`. `IncomeVaultBase.sol` is the only file left at the root. `src/public/` is unchanged on purpose: splitting the external surface by who may call it is what makes the gated/ungated boundary legible. Import paths only; no contract renamed, no ABI change. +- Gasless support is now chosen at deployment. `IncomeVaultBase` no longer inherits `ERC2771Module`; the new `IncomeVaultBaseERC2771` adds it along with the `_msgSender`/`_msgData`/`_contextSuffixLength` overrides, and both shipped deployments inherit that instead — so their behaviour, ABI and forwarder handling are unchanged. A deployment that does not want a trusted forwarder now inherits `IncomeVaultBase` directly and carries none of the machinery, where previously declining meant passing the zero address and paying for it anyway. +- `doc/solidityAPI/index.md` is regenerated from the current contracts (8,737 -> 76,812 bytes); it had described the pre-CMTAT-v3 architecture. `npx hardhat docgen` now writes straight to `doc/solidityAPI` via `docgen.outputDir`, instead of into `docs/` for a manual move. Two blockers had to go first: `solidity-docgen` aborts on a `@return` naming the `$` ERC-7201 storage accessor, so those four tags are removed (OpenZeppelin leaves the accessors undocumented); and `hardhat.config.js` declared `settings` at the top level where Hardhat ignores it, so docgen compiled without the optimizer and reported a spurious contract-size warning. Documentation and tooling only, no contract behaviour change. Finding D-2 of `CLAUDE_IMPROVEMENT.md`. +- Two unused imports removed: `IERC165` in `Ownable2StepERC165Module` and `ISnapshotSource` in `IncomeVaultRestricted`, the latter left over from finding M-2. Found by Aderyn; re-running it took L-9 from 6 instances to 4, the remaining four being `@inheritdoc` false positives. No bytecode change — an unused import contributes no code. +- `detectTransferRestriction` now reports the whole payout decision instead of only the RuleEngine's part. A paused vault, a deactivated vault or a frozen party used to be reported as unrestricted while `canTransfer` returned false and the claim reverted, so the two views on one contract disagreed and the one carrying the ERC-1404 name was wrong. It returns CMTAT's `REJECTED_CODE_BASE` codes. `messageForTransferRestriction` answers for each of those codes with CMTAT's own strings, and returns `UnknownCode` rather than `No restriction` for a code it cannot explain. Finding H-1 of `CLAUDE_ANALYSIS_SECOND.md`. +- `_transferDividend`, `_computeDividend` and `_computeDividendBatch` are now `virtual`, matching the five internal functions they sit beside in `IncomeVaultInternal`. `_transferDividend` is the payout routine and the sanctioned way to extend it is an override, since the guide forbids adding another public entry point to it — it could not be overridden. Finding E-1 of `CLAUDE_ANALYSIS_SECOND.md`. +- The saturating remainder rule is extracted as `_unclaimed(segregated, paid)`, used by both `unclaimedDividend` and `_transferDividend`, and each reads its period slots once. Measured **167 gas** off every claim (118,924 to 118,757) with one source of truth for a rule the two callers must agree on. Finding B-1 of `CLAUDE_ANALYSIS_SECOND.md`. +- The deposit write, its zero-amount check and its `newDeposit` event move into a single internal `_deposit`, called once by `deposit` and once per element by `depositBatch`. Both paths carried a copy of all three, so the rule that a deposit is validated, recorded and announced together was held by convention rather than structurally. The batch's single `safeTransferFrom` for the whole total stays outside the helper, which is the reason that function exists. No behaviour change; the batch path measures 283 gas cheaper. Finding C-1 of `CLAUDE_ANALYSIS_SECOND.md`. +- `_revertOnInvalidTime` ends in an unconditional `else` instead of a fourth `else if`. `TIME_ERROR_CODE` is exhaustive, so the extra comparison was dead — and the old shape **failed open**: a value added to the enum without a matching arm fell through and silently allowed the claim. It now reverts. +- The snapshot source is now typed `ISnapshotSource` (new, `src/interfaces/ISnapshotSource.sol`) rather than `ISnapshotState`: the three functions the vault calls instead of the eight `ISnapshotState` declares. Signatures are copied verbatim, so every `ISnapshotState` implementation still satisfies it; callers pass one with an explicit cast, `ISnapshotSource(address(engine))`. **Storage layout and ABI are unchanged** — interface types encode as `address` — so this is a source-level change only. Finding I-1 of `CLAUDE_ANALYSIS.md`. +- `validateTimeBatch` reads `timeLimitToWithdraw` once instead of once per element, and both batch entrypoints take `calldata` instead of `memory`. Measured **-1,949 gas (-5.5%)** on an 8-element batch, -241 gas per additional element. See `CLAUDE_ANALYSIS.md` A-1. +- The five `public` functions of `IncomeVaultOpen` are now `virtual`, matching every other public function in the project. See `CLAUDE_ANALYSIS.md` E-1. +- Access control moved to the authorization-hook pattern. `IncomeVaultBase` (new) and the logic modules declare one `internal view virtual` hook per capability (`_authorizeDeposit`, `_authorizeWithdraw`, `_authorizeDistribute`, `_authorizeOperator`, `_authorizeRuleEngineManagement`, plus the CMTAT `_authorizePause`, `_authorizeDeactivate`, `_authorizeFreeze`); the deployment contract supplies the policy. `IncomeVault` keeps exactly the roles it had, so its behaviour is unchanged. +- The four `INCOME_VAULT_*_ROLE` constants moved from `IncomeVaultInvariantStorage` to the new `IncomeVaultRolesStorage`, inherited only by `IncomeVault`, so the single-owner variant does not publish roles it never checks. + +### Testing + +- Invariant suite (`test/invariant/`): a bounded handler drives deposits, claims, batch claims, both distribution variants, withdrawals, freezes, pauses and time warps against six invariants — no over-payment, no holder paid twice for one period across **any** combination of the three payout paths, monotonic claim flags, no unexplained batch payout, no value leaking to a non-holder, and per-time accounting bounded by deposits. 3,072 calls per invariant, budget pinned in `foundry.toml`. Finding B-3 of `CLAUDE_IMPROVEMENT.md`. +- The single-owner deployment used in five test files moved into `HelperContract._deployOwnableVault()`. Finding B-4. +- `deactivateContract` is now covered in both deployment variants: the pause precondition, the irreversibility (a deactivated vault can never be unpaused), double-deactivation, that every payout path is refused afterwards, and that `PAUSER_ROLE` alone is not sufficient. It was previously untested despite being the only irreversible action in the system. Finding B-1 of `CLAUDE_IMPROVEMENT.md`. +- Branch coverage of `src/` raised from **68.75% to 97.56%** (lines 92.09% → 95.65%, statements 94.54% → 97.54%): the initializer guards, a holder with no tokens at the snapshot, the ERC-1404 views with no RuleEngine configured, and every `TIME_ERROR_CODE` arm. Finding B-2. + +### Fixed + +- A payout is now bounded by what its own dividend time still holds. Sweeping a period mid-window lowers `segregatedDividend`, so a holder claiming afterwards was priced against the reduced figure while the period no longer held that much — and the shortfall was silently funded from **another period's deposit**. Such a claim now reverts `IncomeVault_NotEnoughAmount`. Found by the invariant suite; a deterministic reproduction is `testAClaimCannotBeFundedByAnotherPeriod`. +- `unclaimedDividend` saturates at zero instead of underflowing on an over-drawn period. A view must never revert. +- `withdraw` is now bounded by what a dividend time **still holds** (`unclaimedDividend`) rather than by what was deposited into it. `segregatedDividend` is the pro-rata denominator and is never reduced by a payout, so the old bound let a fully-claimed period be swept again — draining the funds deposited for a *different* period and leaving its holders unpayable, with no error raised. Finding E-3 of `CLAUDE_IMPROVEMENT.md`. +- `timeLimitToWithdraw` can no longer be set to zero, at initialization or through `setTimeLimitToWithdraw`. Zero collapsed the claim window `[time, time + limit]` to the single instant `block.timestamp == time` — one second later every claim already reverted `TooLateToWithdraw` — so a period became effectively unclaimable with no signal, the transaction having succeeded and the event fired. Reverts with `IncomeVault_TimeLimitToWithdrawZeroNotAllowed`. Any positive value is still accepted. Finding A-1 of `CLAUDE_IMPROVEMENT.md`. +- `distributeDividend` now applies the same transfer restrictions as a holder-driven claim — pause, address freeze and the RuleEngine. It previously bypassed the ValidationModule entirely, so an address the RuleEngine refuses, or a frozen holder, could still be paid by the issuer, and pausing the vault did not stop a distribution. One blocked holder reverts the whole distribution rather than being skipped. Finding H-2 of `CLAUDE_ANALYSIS.md`. +- `distributeDividend` now applies the same claim window as `claimDividend` (claims open, `time` reached, withdraw limit not expired). It previously checked only `segregatedClaim[time]`, so a distribution before `time` computed every payout from the **live** balances — `ISnapshotState` falls back to them when no snapshot has been recorded — and marked the period claimed at the wrong amount. Finding H-1 of `CLAUDE_ANALYSIS.md`. + + +- `INCOME_VAULT_DISTRIBUTE_ROLE` was defined as `keccak256("INCOME_VAULT_DEPOSIT_ROLE")` and therefore shared the deposit role. It is now `keccak256("INCOME_VAULT_DISTRIBUTE_ROLE")`. +- `initialize` checked the payment token against the zero address twice and never checked the snapshot source. The snapshot source is now rejected when zero (`IncomeVault_SnapshotEngineWithAddressZeroNotAllowed`). + +### Toolchain + +- Solidity 0.8.36, EVM target `prague`. +- CMTAT v2.4.0 → v3.3.0-rc3, RuleEngine v2.0.0 → v3.0.0-rc5, OpenZeppelin Contracts (and Contracts Upgradeable) v5.0.x → v5.7.0, OpenZeppelin Foundry Upgrades v0.1.0 → v0.4.2. +- New submodules: `lib/SnapshotEngine` (v0.5.0) and `lib/forge-std` (v1.16.1). +- `ReentrancyGuardUpgradeable` was removed from OpenZeppelin Contracts Upgradeable v5.7.0; the vault now uses `ReentrancyGuardTransient` (EIP-1153). + ## 1.0.0 - 🎉 first release! diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d1ac1df --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,213 @@ +# IncomeVault — Agent Guide + +> **Note — keep in sync:** `AGENTS.md` and `CLAUDE.md` must always be **identical**. Any edit to one must be applied verbatim to the other. + +> **Note — commit messages:** After each group of modifications or each feature added, always provide a **one-line GitHub commit message** (Conventional-Commits style, e.g. `feat: ...`, `fix: ...`, `docs: ...`). +> +> **Never put `!` in a commit message** — not as the breaking-change marker (`feat!: ...`), not anywhere else. In an interactive bash, `!` inside double quotes triggers history expansion, so `git commit -m "feat!: ..."` aborts with `bash: !: unrecognized history modifier`. Signal a breaking change with an uppercase `BREAKING CHANGE:` line in the commit body instead, and keep the subject line free of `!`. + +## What this project is + +`IncomeVault` is a CMTA prototype smart contract that distributes coupon/dividend payments to the holders of a security token. Dividends are deposited in the vault in an **ERC-20 payment token** (e.g. USDC), segregated per distribution date (`time`), and claimed pro-rata using an on-chain snapshot read through the **`ISnapshotState`** interface ([SnapshotEngine](https://github.com/CMTA/SnapshotEngine)). + +The vault is **token agnostic**: it never calls the token, only the snapshot source. Any contract implementing `ISnapshotState` works — the external `SnapshotEngine` bound to a CMTAT (or to any ERC-20), a token embedding the snapshot modules, or a custom implementation. + +> **The contracts are NOT audited.** Do not present them as production-ready. + +## Key concepts + +- **Not an ERC-4626 vault, deliberately** — a 4626 share entitles whoever holds it *now*; a dividend is allocated by **record date**. `doc/README.md` → "Comparison with ERC-4626 / ERC-7540 vaults" has the full reasoning and the cases where 4626/7540 *would* be right. Do not "standardise" this onto 4626. +- **Segregated deposits by `time`** — `time` is a Unix timestamp identifying a distribution. State is keyed by it: `segregatedDividend[time]`, `segregatedClaim[time]`, `claimedDividend[holder][time]`. +- **Claim flow** — the snapshot source schedules a snapshot at `time` → deposit role calls `deposit(time, amount)` → operator calls `setStatusClaim(time, true)` → holders call `claimDividend(time)` / `claimDividendBatch(times)`. +- **The EIP-712 domain version is `"1"` and must stay `"1"`.** It is set in `__IncomeVaultBase_init_unchained`; bumping it with a release would invalidate every ERC-7741 signature already issued. ERC-7741's id `0xa9e50872` **is** advertised through `supportsInterface` (the standard requires it) — unlike ERC-7540's operator id, which is not. +- **`IIncomeVault` is the stated API and is compiler-enforced.** It is inherited by `IncomeVaultInternal`, the common base of both payout paths, so every deployable *and* any embedded host implements it — add a public function to the distribution surface and you add it here too. It owns the `TIME_ERROR_CODE` enum, because `validateTimeCode` returns it. Keep it inheriting **nothing**: `type(...).interfaceId` covers only directly-declared selectors, and both deployment variants advertise that id. +- **Directory layout says what a file *is*.** `deployment/` holds only deployable contracts, `modules/` only abstract capability mixins, `storage/` only declarations, `interfaces/` only interfaces, and `IncomeVaultBase.sol` is the single composition root at the root. **`public/` is the exception and is deliberate** (M-5): it groups the external surface by *who may call*, because on a compliance contract the gated/ungated boundary is the first thing a reviewer checks. Do not merge `IncomeVaultOpen` and `IncomeVaultRestricted` into one "distribution module". +- **One capability, one module, one ERC-7201 namespace.** The project runs four namespaces: `IncomeVaultInternal` (distribution), `SnapshotSource`, `Operator` and `ERC7741Module`. Claim delegation is `IncomeVaultOperatorModule`, not a mapping in the distribution struct (M-6). A new capability with state gets its own namespace — never a field appended to someone else's. +- **Claim delegation reuses ERC-7540's operator signatures exactly.** `IERC7540Operator` must keep `type(...).interfaceId == 0xe3bc4e65`; a test asserts it. Do **not** add that id to `supportsInterface` — the vault is not an asynchronous vault and must not advertise as one. +- **Snapshot source** — reached through the three hooks of `IncomeVaultSnapshotCore`, never a direct call. `IncomeVaultSnapshotModule` is the standalone answer: an `ISnapshotSource` set at initialization, never zero, exposed by `dividendSnapshotSource()` and stored in its **own** ERC-7201 namespace. `ISnapshotSource` (`src/interfaces/ISnapshotSource.sol`) declares exactly the three functions the vault calls — a strict subset of the SnapshotEngine's `ISnapshotState`, signatures verbatim, so every `ISnapshotState` implementation satisfies it. **Do not add an ERC-165 guard on it**: the canonical `SnapshotEngine` advertises no id for it and the guard would reject it. If the vault ever needs a fourth function, add it here — not by widening back to `ISnapshotState`. +- **`transferDividendSelf` is a self-call helper with no role check.** Its only protection is `msg.sender == address(this)` (raw `msg.sender`, never `_msgSender()`, so a forwarder cannot impersonate the vault). It exists because `try`/`catch` needs an external call. Never relax that guard, and never add another public entry point to `_transferDividend`. +- **Every payout must come out of its own period.** `_transferDividend` refuses an amount larger than `unclaimedDividend(time)`. Without it, a claim made after a mid-window sweep is funded from another period's deposit. Unreachable in normal operation — a period's entitlements sum to at most its deposit — so the check only bites when a period has been over-swept. +- **`segregatedDividend` is the pro-rata denominator, not a balance.** It is fixed at the deposit for the whole period and never reduced by a payout. What a period still holds is `unclaimedDividend(time) = segregatedDividend - paidDividend`, and that is what bounds `withdraw`. Never bound a sweep by `segregatedDividend` alone — that let a fully-claimed period drain another period's funds. +- **`_setStatusClaim` is idempotent and owns `_openClaimCount`.** It is the only writer of the claim status; a repeated write returns early so the counter stays exact. `setDividendSnapshotSource` depends on that counter reaching zero, so any new path that changes a claim status must go through it. +- **Pro-rata formula** — `senderDividend = (senderBalance * segregatedDividend[time]) / tokenTotalSupply`, rounded down. Dust stays in the vault; the issuer withdraws it after `timeLimitToWithdraw`. +- **Claim window** — `validateTime` / `validateTimeCode` reject a claim when the claim is not activated, `block.timestamp < time` (too early), or `block.timestamp > time + timeLimitToWithdraw` (too late). +- **Access control — authorization-hook pattern.** The logic contracts declare *what* is protected: one `internal view virtual` hook per capability, invoked by a modifier, declared **without a body**. The deployment contract declares *who*: `IncomeVault` overrides every hook with `onlyRole(...)`, `IncomeVaultOwnable2Step` with `onlyOwner`. The two are chosen at deployment and are not interchangeable. Capability table: `doc/README.md` → Access control. +- **The payout paths must not inherit a policy.** `IncomeVaultOpen`/`IncomeVaultRestricted` inherit `IncomeVaultValidationCore` (the `_validateTransfer` question) — never `IncomeVaultValidationModule` (one answer). The module is inherited by the **deployment** contracts, like the access-control base. Re-coupling them makes the logic unembeddable in any CMTAT: C3 fails with `Error (5005)`, which no override can repair. Same rule for the snapshot side: they inherit `IncomeVaultSnapshotCore` (the three questions) — never `IncomeVaultSnapshotModule` (one answer). `test/mocks/` holds the two compile-time guards: `EmbeddedDividendHostMock.sol` (a non-CMTAT host) and `CMTATDividendHostMock.sol` (a `CMTATUpgradeableInternalSnapshot` paying its own dividends). +- **`ReentrancyGuardTransient` is listed last** in the payout paths, matching CMTAT's ordering. Moving it earlier reintroduces the same unresolvable linearization failure. +- **Transfer restriction** — every payout, pull (`claimDividend`) **and** push (`distributeDividend`), goes through `IncomeVaultValidationModule`: pause, address freeze, and an optional `IRuleEngine`. Rejected payouts revert with `IncomeVault_InvalidTransfer(from, to, value)`; in a batch one blocked holder reverts the whole call rather than being skipped, so a compliance failure cannot be silently dropped. Any new payout path must call `_validateTransfer` too. +- **RuleEngine is read-only here** — the vault uses `IRuleEngine.canTransfer` only. It is not a bound token, so `transferred(...)` would revert, and a payout must not mutate stateful rules. +- **Upgradeable** — deployed behind an OpenZeppelin **Transparent Proxy**; `initialize(...)` replaces the constructor. State is held in an **ERC-7201** namespaced struct (`IncomeVault.storage.IncomeVaultInternal`, slot `0xe4f8b033…0c00`), as in OZ Upgradeable and CMTAT v3 — there is no `__gap` and no sequential storage slot. +- **Gasless / meta-tx is a deployment decision** (M-8). `IncomeVaultBase` has **no** ERC-2771; `IncomeVaultBaseERC2771` adds CMTAT's `ERC2771Module` plus the `_msgSender`/`_msgData`/ `_contextSuffixLength` overrides that resolve the `ERC2771ContextUpgradeable` / `ContextUpgradeable` diamond, and both shipped deployments inherit that. The forwarder is set in the constructor and is **immutable**. Do not move `ERC2771Module` back into `IncomeVaultBase`: a forwarder can name any `_msgSender()`, so a deployment that does not want one must be able to not have one. `test/mocks/NoForwarderVaultMock.sol` is the guard. +- **Reentrancy** — claims use `nonReentrant` from `ReentrancyGuardTransient` (EIP-1153); `_transferDividend` sets `claimedDividend[holder][time] = true` *before* the ERC-20 transfer. + +## File tree + +``` +src/ +├── IncomeVaultBase.sol # The composition root: assembles the modules, +│ # __IncomeVaultBase_init_unchained. Hooks left abstract. +│ # NO access-control, NO validation, NO meta-tx policy. +├── IncomeVaultBaseERC2771.sol # The base plus ERC-2771: forwarder constructor and the +│ # _msgSender/_msgData/_contextSuffixLength overrides. +│ # Both shipped deployments inherit THIS one. +├── deployment/ # What you actually deploy — nothing abstract lives here +│ ├── IncomeVault.sol # AccessControlModule; every hook -> onlyRole(...) +│ └── IncomeVaultOwnable2Step.sol # Ownable2StepUpgradeable; every hook -> onlyOwner +├── public/ # The external surface, split by WHO MAY CALL. Deliberate: +│ │ # the gated/ungated boundary is the thing a reviewer of a +│ │ # compliance contract checks first. Do not merge these two. +│ ├── IncomeVaultOpen.sol # Permissionless: claimDividend, claimDividendBatch, validateTime(Code|Batch) +│ └── IncomeVaultRestricted.sol # Role-gated: deposit, withdraw, withdrawAll, distributeDividend, +│ # setStatusClaim, setTimeLimitToWithdraw +├── modules/ # Abstract capability mixins, one per capability +│ ├── IncomeVaultInternal.sol # Distribution state: the ERC-7201 struct + getters, +│ │ # _computeDividend(Batch), _transferDividend, _setStatusClaim +│ ├── IncomeVaultValidationCore.sol # ONLY `_validateTransfer` — inherits nothing, keep it that way +│ ├── IncomeVaultValidationModule.sol # Pause + Enforcement + RuleEngine; canTransfer, +│ │ # setRuleEngine, detectTransferRestriction. Hooks abstract. +│ ├── IncomeVaultSnapshotCore.sol # ONLY the 3 snapshot hooks — inherits nothing, keep it that way +│ ├── IncomeVaultSnapshotModule.sol # One answer: a stored ISnapshotSource in its OWN ERC-7201 +│ │ # namespace; dividendSnapshotSource, setDividendSnapshotSource +│ ├── IncomeVaultOperatorModule.sol # ERC-7540 claim delegation in its OWN ERC-7201 namespace; +│ │ # setOperator, isOperator, _requireHolderOrOperator +│ ├── ERC7741Module.sol # EIP-712 signed operator authorisation, own ERC-7201 namespace +│ ├── VersionModule.sol # VERSION constant behind IERC3643Version.version() +│ └── Ownable2StepERC165Module.sol # ERC-165 advertisement of ERC-173 / Ownable2Step +├── interfaces/ +│ ├── IIncomeVault.sol # The stated distribution API + the TIME_ERROR_CODE enum; +│ │ # inherited by IncomeVaultInternal so solc enforces it +│ ├── ISnapshotSource.sol # The 3 snapshot functions the vault calls — subset of ISnapshotState +│ ├── IERC7540Operator.sol # The ERC-7540 operator subset, verbatim; id MUST stay 0xe3bc4e65 +│ └── IERC7741.sol # Signed operator authorisation; id MUST stay 0xa9e50872 +└── storage/ # Declaration-only: nothing here has behaviour + ├── IncomeVaultInvariantStorage.sol # Custom errors and events shared by every variant + └── IncomeVaultRolesStorage.sol # The four INCOME_VAULT_*_ROLE constants — inherited ONLY by IncomeVault + +script/ +├── DeployIncomeVault.s.sol # role-based variant; `deploy(config)` split from `run()` +└── DeployIncomeVaultOwnable2Step.s.sol # single-owner variant + +test/ +├── HelperContract.sol # Constants + `_deployContracts()` / `_deployOwnableVault()` +├── IncomeVault.t.sol # Single claim: deposit, claim, pause, freeze, error cases +├── IncomeVaultBatch.t.sol # claimDividendBatch behaviour +├── IncomeVaultRestricted.t.sol # deposit/withdraw/withdrawAll/distributeDividend + access control +├── IncomeVaultStorage.t.sol # ERC-7201: slot derivation, field offsets, the two namespaces +├── AccessControlHooks.t.sol # Both variants: every hook accepts/rejects, role separation, +│ # Ownable2Step handover, ERC-165 +├── VersionModule.t.sol # version() on EVERY deployable contract — keep exhaustive +├── CodeQuality.t.sol # regressions for CLAUDE_ANALYSIS.md findings, incl. the +│ # push/pull claim-window parity (H-1) and restrictions (H-2) +├── IncomeVaultInterface.t.sol # M-7: drive a proxy through IIncomeVault alone, ERC-165 id, +│ # embedded hosts present the same interface +├── SnapshotSource.t.sol # I-1: a 3-function source is enough; the real engine still fits +├── SetDividendSnapshotSource.t.sol # M-2 setter: gated on openClaimCount() == 0, zero-address, event +├── RuleEngineIntegration.t.sol # End-to-end with RuleEngine + RuleWhitelistMock +├── DistributeBestEffort.t.sol # A-4: one blocked holder is skipped, not reverted +├── DepositBatch.t.sol # depositBatch, incl. the per-transaction gas comparison +├── UnclaimedDividend.t.sol # E-3: saturating residue, per-period withdraw bound +├── Operator.t.sol # ERC-7540 operator subset: setOperator, claim-for +├── OperatorAuthorization.t.sol # ERC-7741 signed authorisation, EIP-712, ERC-1271 +├── Deactivate.t.sol # deactivateContract, permanent kill +├── EdgeCases.t.sol # zero supply, zero balance, boundary times +├── script/Deploy.t.sol # C-4: both deployment scripts, config validation +├── NoForwarderDeployment.t.sol # M-8: a vault on the plain base has no isTrustedForwarder, +│ # the shipped ones still do, and payouts work either way +├── invariant/ # handler + 7 invariants; validate any change by sabotaging +│ # the contract and checking an invariant actually fails +└── mocks/ + ├── ERC20PaymentMock.sol # Minimal ERC-20 used as payment token + ├── MinimalSnapshotSourceMock.sol # I-1: implements ISnapshotSource and nothing else + ├── IncomeVaultOverrideMock.sol # compile guard for the `virtual` convention + ├── NoForwarderVaultMock.sol # M-8 guard: a deployment on IncomeVaultBase, no ERC-2771 + ├── EmbeddedDividendHostMock.sol # M-1/M-2 compile guard: a non-CMTAT host paying dividends + └── CMTATDividendHostMock.sol # M-1/M-2 compile guard: a CMTATUpgradeableInternalSnapshot + # paying its own dividends, its own snapshots, own canTransfer +``` + +Tests deploy the vault through `Upgrades` (openzeppelin-foundry-upgrades), which requires `--ffi`, `@openzeppelin/upgrades-core` from npm, and a **full** build (`forge clean && forge build`). + +## Other important files + +| Path | Purpose | +| --- | --- | +| `foundry.toml` | solc 0.8.36, optimizer 200 runs, EVM `prague`, `ffi`, `ast`, `build_info`, `storageLayout`, `fs_permissions` on `./out` (needed by OZ Upgrades) | +| `remappings.txt` | `CMTAT/`, `RuleEngine/`, `SnapshotEngine/`, `OZ/`, `OZUpgradeable/`, `@openzeppelin/*`, `openzeppelin-foundry-upgrades/`, `forge-std/` | +| `hardhat.config.js` | Only used for `solidity-docgen` (`npx hardhat docgen`), mirrors the Foundry solc settings. `settings` must stay **inside** `solidity` — at the top level Hardhat ignores it and the optimizer silently does not run. `docgen.outputDir` writes straight to `doc/solidityAPI`, so regenerating needs no manual move | +| `package.json` | npm scripts: lint (ethlint/prettier), `uml`, `surya:*`, `docgen`; dependency `@openzeppelin/upgrades-core` | +| `.soliumrc.json`, `.soliumignore` | Ethlint/Solium configuration | +| `CHANGELOG.md` | changelog.md conventions; current release heading `2.0.0-rc0`. The `-rc` suffix marks the candidate and is **not** carried into `VERSION`, which stays `2.0.0` — the two are allowed to differ only in that suffix | +| `doc/README.md` | The reference doc: snapshot source, roles table, claim restrictions, formula, threat model & FAQ, plus the technical choices (upgradeability, pause, token agnosticism, reentrancy, gasless GSN/ERC-2771) and the schema/graphs | +| `doc/cmtat-standard/` | The CMTA framework functional specifications PDF, plus four documents. `CMTAT-Distribution-impl.md` is the comparison of section 3.2.4 (functionalities 27-32) against this implementation, keeping only a summary line per proposal. `CMTAT-Distribution-Amendments.md` holds the nine **amendments** (C-1..C-9) to functionalities that already exist; `CMTAT-Distribution-Additions.md` the three **additions** (A-1..A-3) the specification does not describe at all; `CMTAT-Distribution-ERC4626.md` the question behind C-9 — what the deposit may be held as between functionalities 29 and 30. Each quotes the source text it argues from **verbatim**, so it is readable without the PDF: 3.2.4 in all three, the 3.1.1 debt attributes in the amendments (C-6, C-8 cite them), and the ERC-4626 `asset` / `totalAssets` / `previewRedeem` / `redeem` requirements in the 4626 one. Verify a quote against the source before editing it — the PDF via `pdftotext -layout`, ERC-4626 via `lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol`. Keep the reasoning for an id in exactly one file. `doc/README.md` keeps a summary and links here | +| `doc/script/check_sizes.py` | The EIP-170 check, run by `make build` after a **full** `forge build`. `forge build --sizes` cannot be used: it fails on `CMTATDividendHostMock`, a never-deployed compile guard that is over the limit by design, and `--skip` would make the compilation partial so the Upgrades plugin rejects the build-info. This scopes the check to `src/` via each artifact's `compilationTarget`, and **fails when it measures nothing** — every filter can legitimately empty the set, so "nothing to report" and "nothing was looked at" would otherwise print the same reassuring line | +| `doc/script/coverage-README.md` | The note copied into `doc/coverage/README.md` by `make coverage-report`; that directory is recreated each run, so edit it here | +| `doc/script/gen_toc.py` | Regenerates the tables of contents in `doc/README.md` and `doc/TOOLCHAIN.md`: `python3 doc/script/gen_toc.py doc/README.md doc/TOOLCHAIN.md`, or `--check` to fail when one is stale. They are **generated markdown lists between `` markers**, never `[TOC]` — that is Doxygen syntax and GitHub renders it as literal text. Do not hand-edit inside the markers; re-run after adding or renaming a heading | +| `doc/TOOLCHAIN.md` | Tested dependency versions, doc-generation and lint commands | +| `doc/solidityAPI/index.md` | Generated Solidity API. Refresh with `npx hardhat docgen`; it writes in place. **`solidity-docgen` must be patched first** — it matches tag names with `\w`, which excludes `$`, so `@return $` aborts the run and `@param $` silently drops the description. `npm install` reverts the patch; re-apply with the `solidity-docgen-doc` skill's `patch_docgen.sh` (D-2) | +| `doc/surya/`, `doc/schema/` | Surya call graphs, inheritance graphs and markdown reports (one per `src/**/*.sol`), UML class diagram, PlantUML sources and the remaining drawio diagrams. Regenerate with `npm run surya:graph` + `surya:inheritance` + `surya:report` (output goes to the scratch `docOut/`, then replaces `doc/surya/`) and `npm run uml` | +| `doc/schema/plantuml/` | PlantUML sources (`.puml`) and their rendered `.png`. `incomevault-architecture` is the overview embedded in **both** READMEs — keep it low-detail; `incomevault-global`, `-claimdividend`, `-ruleengine` and `-segregated-deposit` are the detailed ones in `doc/README.md`. The `.puml` is the source of truth — edit it, then re-render with `plantuml -tpng doc/schema/plantuml/.puml` and **look at the PNG**: PlantUML exits 0 on warnings and draws them into the image as a yellow banner. Embed the image in the docs, never the source text | +| `doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS_SECOND.md` | Second-pass code-quality review. Ids restart at `A-1`; read it **with** `CLAUDE_ANALYSIS.md`, which it does not repeat. Includes two explicit do-not-change verdicts (I-1, G-2) recorded so they are not re-opened | +| `doc/audits/AUDIT_OVERVIEW.md` | The security overview: scope, both analyzers' results, the two real findings, and the substantive defects the reviews caught that static analysis did not | +| `doc/audits/tools/vX.Y.Z/` | Per-release tool output: `slither-report.md`, `aderyn-report.md` and a `*-feedback.md` triaging every finding. Filter Slither on `lib`, not on dependency names, and check the report cites no `lib/` before trusting a count. Aderyn writes its `Found in` links with the **absolute** repo path of the machine that ran it — rewrite them to repo-relative before committing | +| `doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS.md` | Code-quality review (not a security audit). Findings carry stable ids (`A-1`, `H-2`, …) — cite them in commits and in the other docs, never in a contract comment, and read the Outstanding table before re-opening anything | +| `script/` | Deployment scripts, one per variant. Excluded from the style check and from coverage; their `require` messages are deliberate. Tested by `test/script/Deploy.t.sol` | +| `Makefile` | The task definitions — `make help` lists them. npm scripts and CI both delegate here, so there is one definition. Every compiling target does a **full** build because the Upgrades plugin rejects an incremental one — which is also why `--sizes` is not used: it fails on a test mock, and `--skip` would make the build partial. `doc/script/check_sizes.py` runs the EIP-170 check afterwards, scoped to `src/` | +| `.github/workflows/ci.yml` | CI: recursive checkout, `npm install`, then `make test` — the same target a contributor runs, so CI and local cannot drift. It does **not** use `--sizes` or a verbosity flag | + +## Dependencies (tested versions) + +- Solidity **0.8.36**, EVM target `prague` (contracts declare `pragma ^0.8.24`) +- CMTAT **v3.3.0-rc3** (submodule `lib/CMTAT`) +- RuleEngine **v3.0.0-rc5** (submodule `lib/RuleEngine`) +- SnapshotEngine **v0.5.0** (submodule `lib/SnapshotEngine`) +- openzeppelin-contracts **v5.7.0** (submodule) +- openzeppelin-contracts-upgradeable **v5.7.0** (submodule) +- openzeppelin-foundry-upgrades **v0.4.2** (submodule) +- forge-std **v1.16.1** (submodule) + +CMTAT v3.3.0 and RuleEngine v3.0.0 are release candidates — they are what the CMTA ecosystem is currently aligned on (RuleEngine v3.0.0-rc5 pins CMTAT v3.3.0-rc3). Submodules are **not** updated automatically — pin them to a release tag, never to an intermediary commit. + +## Common commands + +> Test helpers live in `HelperContract`: `_deployContracts()` builds the CMTAT, snapshot engine, payment token and role-based vault; `_deployOwnableVault()` adds the single-owner variant. Do not re-inline either — five suites used to carry a copy. + +```bash +make help # every target, and why the build must be full +make install # submodules + npm dependencies +make test # THE way to run the suite: full build, then forge test --ffi +make coverage # src/ only, tests and mocks excluded + +# `forge test --ffi` on its own fails every test after an incremental build — the Upgrades plugin +# rejects partial build-info. Use `make test` unless nothing has been recompiled since the last clean. +forge test --ffi --match-contract IncomeVaultTest # fine right after a `make build` + +npm run lint:sol # ethlint on src/ +npm run lint:sol:prettier # prettier-plugin-solidity +npm run surya:graph && npm run surya:inheritance && npm run surya:report # then replace doc/surya/ with docOut/ +npm run uml && npx hardhat docgen + +slither . --checklist --filter-paths "openzeppelin-contracts|test|CMTAT|RuleEngine|SnapshotEngine|forge-std" > slither-report.md +``` + +## Conventions & invariants + +- **Versioning:** `CHANGELOG.md` follows [changelog.md](https://changelog.md/) and states the project's own semver rule at the top — an **incompatible proxy storage change, a changed external API, or a reworked internal architecture is a MAJOR bump**. Add an entry for any user-visible contract change. A release bumps two things that must agree: the `CHANGELOG.md` heading and the `VERSION` constant in `src/modules/VersionModule.sol`. Add every new deployable contract to `test/VersionModule.t.sol`, which is exhaustive by design. +- **Upgrade safety:** the state lives in the ERC-7201 struct `IncomeVaultInternalStorage` (namespace `IncomeVault.storage.IncomeVaultInternal`). Append new fields to the **end** of that struct; never reorder or remove existing ones. Do **not** reintroduce `uint256[50] private __gap` — namespaced storage replaces it, and the contract must keep declaring zero sequential slots. A new module with its own state gets its own namespace, never a sequential variable; recompute its slot with `SlotDerivation.erc7201Slot()` and keep the derivation comment above the constant. `IncomeVault` has an `/// @custom:oz-upgrades-unsafe-allow constructor` annotation — keep it and keep `_disableInitializers()` in the constructor. +- **Authorization hooks:** a hook is `internal view virtual` on the declaration **and on every override** — `view` is what makes "an auth hook cannot mutate state" compiler-enforced, and it is free. CMTAT declares its hooks non-`view`; overriding them `view` is legal (an override may tighten mutability) and is what this project does. Override bodies stay **empty**, with the check riding on the modifier (`onlyRole(...)` / `onlyOwner`), never a bare `_checkRole` call. A new guarded capability means a new hook plus an override in **every** deployment variant. +- **`_authorizeRuleEngineManagement` shares its name with CMTAT's on purpose.** Both this project's `IncomeVaultValidationModule` and CMTAT's `ValidationModuleRuleEngine` sit on the same `ValidationModuleRuleEngineInternal`, whose ERC-7201 slot is a hardcoded constant — so a contract inheriting both has exactly **one** RuleEngine. One capability, one hook; a single override answering both declarations is correct. **Do not prefix or rename it** (M-4): two names over one slot means two policies for one door, and the weaker wins. +- **Role constants live with the layer that enforces them.** They belong in `IncomeVaultRolesStorage`, inherited only by `IncomeVault` — never in `IncomeVaultInvariantStorage`, or the Ownable variant would publish a role it never checks. +- **`@inheritdoc` needs the base imported by name** in the referencing file, even when it is already in scope through inheritance; otherwise the build fails with "references inexistent contract". +- **Claim accounting:** always set `claimedDividend[holder][time]` before any external call; keep `nonReentrant` on the claim entry points. +- **Deposits vs. open claims:** do not deposit for a `time` whose claim status is already `true` — it dilutes holders who have not yet claimed. +- **The claim window is shared.** `TIME_ERROR_CODE`, `_timeCode` and `_revertOnInvalidTime` live in `IncomeVaultInternal` so both the pull path (`claimDividend`) and the push path (`distributeDividend`) apply them. Any new payout path must call them too: without the "too early" bound, `ISnapshotState` falls back to live balances and the payout is computed from the wrong figures. +- **ERC-20 safety:** use `SafeERC20` (`safeTransfer` / `safeTransferFrom`) for the payment token. +- **Style:** 4-space indent, NatSpec (`@notice` / `@param` / `@dev`) on public and internal functions, custom errors prefixed `IncomeVault_`, named imports (`import {X} from "..."`), `SPDX-License-Identifier: MPL-2.0` header on every Solidity file. +- **Never point at a documentation *path* from a contract comment.** Documentation moves — this repo has already reorganised `doc/` twice — but a comment is frozen in the verified source of a deployed contract and can never be corrected. Worse, someone reading that source on a block explorer has no `doc/` to open. Put the substance in the comment and drop the pointer; if the derivation is genuinely too long, state the conclusion and let the doc carry the derivation with no cross-reference either way. **The only permitted reference is `CHANGELOG.md`** — a bare filename that survives any reorganisation and carries an instruction actionable without opening it (`VersionModule` is the one in-tree use). Do not cite audit reports or finding ids either, even by bare filename: state *why* the code is shaped the way it is, so a reader of the verified source needs nothing else. Do not cite the test that asserts a property — give the property and the consequence of breaking it. Mocks and tests are exempt; they are never deployed. +- **Documentation:** the README and `doc/` must state that the contracts are not audited; keep that disclaimer intact. + +## Known quirks (verify before "fixing") + +- `distributeDividend` deliberately bypasses the ValidationModule (no pause / freeze / RuleEngine check): it is an issuer-driven push, unlike the holder-driven claims. +- The `newDeposit` event keeps its lowercase name for backward compatibility with the v1 ABI. +- `IncomeVaultInvariantStorage` declares `event DividendSnapshotSourceSet`, and the getter is `dividendSnapshotSource()`. **Never name either of them `snapshotEngine`**: CMTAT declares `snapshotEngine()` with the same parameters and a *different return type*, which Solidity cannot reconcile by any override — see the snapshot bullet in Key concepts. +- `doc/coverage/` is generated but **committed** — regenerate it with `make coverage-report` in the same commit as any `src/` change, so the tracked report never describes a codebase other than the one beside it. It was git-ignored until this decision, because the repo once carried the *RuleEngine* project's coverage output long enough to read as authoritative; that risk is now handled by refreshing the report rather than by hiding it. `make coverage-report` deletes and recreates the directory, so the only hand-written file in it, `README.md`, is copied back in from `doc/script/coverage-README.md`. `make coverage` gives a summary, `make coverage-report` the HTML in `doc/coverage`. Two files report 0% because they declare hooks with no bodies (`IncomeVaultSnapshotCore`, `IncomeVaultValidationCore`), which is expected rather than a gap. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..88add01 --- /dev/null +++ b/Makefile @@ -0,0 +1,94 @@ +# IncomeVault — common tasks. +# +# Why this file exists: the OpenZeppelin Foundry Upgrades plugin validates upgrade safety by reading +# Foundry's build-info, and it rejects the output of an *incremental* compile. Running +# `forge test --ffi` straight after editing a contract therefore fails every test with +# +# Failed to run upgrade safety validation: ... Build info file ... is not from a full compilation. +# +# which names neither the cause nor the fix. `make test` does the full build first, so the trap is +# not something a contributor has to know about. + +.DEFAULT_GOAL := help +.PHONY: help install build test test-match coverage coverage-report gas lint fmt fmt-check doc deploy deploy-ownable clean + +FFI := --ffi + +## help: list the available targets +help: + @echo "IncomeVault — make targets" + @echo + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## / /' + @echo + @echo " Note: every target that compiles does a FULL build (forge clean && forge build)." + @echo " The upgrade-safety validation rejects an incremental one." + +## install: fetch submodules and the Node dependencies the Upgrades plugin needs +install: + git submodule update --init --recursive + npm install + +## build: full build (clean first — required by the upgrade-safety validation) +build: + forge clean + forge build + @python3 doc/script/check_sizes.py + +## test: full build, then the whole suite +test: build + forge test $(FFI) + +## test-match: run one contract, e.g. `make test-match C=OperatorTest` +test-match: build + forge test $(FFI) --match-contract $(C) -vv + +## coverage: line/branch/function coverage of src/, excluding tests and mocks +coverage: build + forge coverage $(FFI) --exclude-tests --no-match-coverage '(test|mocks?|script)/' --report summary + +## coverage-report: the same, rendered to HTML in doc/coverage (needs lcov/genhtml) +coverage-report: build + forge coverage $(FFI) --exclude-tests --no-match-coverage '(test|mocks?|script)/' \ + --report lcov --report-file lcov.info + rm -rf doc/coverage && mkdir -p doc/coverage + genhtml lcov.info --branch-coverage --output-dir doc/coverage + cp doc/script/coverage-README.md doc/coverage/README.md + rm -f lcov.info + @echo "open doc/coverage/index.html" + +## gas: gas report for the whole suite +gas: build + forge test $(FFI) --gas-report + +## lint: forge lint over the sources +lint: + forge lint src/ + +## fmt: format the sources in place +fmt: + forge fmt src/ test/ script/ + +## fmt-check: report formatting differences without writing +fmt-check: + forge fmt --check src/ test/ script/ + +## doc: regenerate the UML and the Surya diagrams/reports +doc: + npm run uml + cd doc/script && bash script_surya_graph.sh + cd doc/script && bash script_surya_inheritance.sh + cd doc/script && bash script_surya_report.sh + @echo "Surya output is in docOut/ — replace doc/surya/ with it (see doc/TOOLCHAIN.md)" + +## deploy: deploy the role-based vault (set the env vars first, see doc/README.md) +deploy: build + forge script script/DeployIncomeVault.s.sol --rpc-url $(RPC_URL) --broadcast $(FFI) + +## deploy-ownable: deploy the single-owner vault +deploy-ownable: build + forge script script/DeployIncomeVaultOwnable2Step.s.sol --rpc-url $(RPC_URL) --broadcast $(FFI) + +## clean: remove build output and scratch files +clean: + forge clean + rm -rf docOut lcov.info diff --git a/README.md b/README.md index 6e92691..0808ba5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # IncomeVault -> This project is not audited ! -> -> If you want to use this project, perform your own verification or send an email to [admin@cmta.ch](mailto:admin@cmta.ch). +The `IncomeVault` is a prototype to perform coupon-payment dividend with a token supporting on-chain snapshots, typically a [CMTAT](https://github.com/CMTA/CMTAT) bound to a [SnapshotEngine](https://github.com/CMTA/SnapshotEngine). -The `IncomeVault`is a prototype to perform coupon-payment dividend with a CMTAT and the snapshotModule +![IncomeVault architecture](./doc/schema/plantuml/incomevault-architecture.png) + +_Diagram source: [doc/schema/plantuml/incomevault-architecture.puml](./doc/schema/plantuml/incomevault-architecture.puml). The detailed step-by-step flow is in [doc/README.md](./doc/README.md)._ + +> This project has not undergone an audit and is provided as-is without any warranties. ## Introduction @@ -15,32 +17,57 @@ Currently, the vault supports only dividend under the form of another ERC-20 and - Dividends in ERC-20 compatible, which could be an ERC-20 stablecoin such as USDC or USDT for example - Interest paid out at given intervals which shall be a configurable parameter (i.e. every 6 months, every 1 year) -For the specific case where dividends are distributed in shares, meaning additional payout of the “existing” CMTAT Token, it is not currently supported due to the following reasons: -\- With the current architecture, depending on when you decide to mint the new tokens, you will increase the total supply used to compute the token holder shares. Therefore, you will reduce the dividends distributed to the token holders. -\- In general, for yield tokens, the formula used can be different. +The `IncomeVault` is **not** an [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) tokenized vault, and deliberately so: an ERC-4626 share entitles whoever holds it *now*, whereas a dividend must be allocated to whoever held the security token at a **record date**. See [Comparison with ERC-4626 / ERC-7540 vaults](./doc/README.md#comparison-with-erc-4626--erc-7540-vaults) for the full comparison, including when a 4626 vault *is* the right tool. + +Paying the dividend in **the security token itself** — a scrip or stock dividend — is **not supported**. The vault would have to hold a stock of that token, and the pro-rata formula divides by the token's total supply, which includes what the vault holds: holders would be diluted by their own dividend, and the shortfall would stay in the vault. A share dividend is also normally declared as a ratio (one new share per N held) rather than as a pot divided pro-rata, which is a different computation. Nothing in the code prevents pointing the vault at the security token, so the reasoning is set out in [Paying the dividend in the security token itself](./doc/README.md#paying-the-dividend-in-the-security-token-itself). ## Compatibility - The dividends can be paid with ERC-20 tokens as described in the [ERC-20](https://eips.ethereum.org/EIPS/eip-20) specification -- The shares used to compute the dividends part have to be a smart contract implementing the interface `ICMTATSnapshot` as described in the CMTAT. This interface is responsible to provide information on the token holder's balance and the total supply for a specific time. +- The shares used to compute the dividends part are read through the interface `ISnapshotSource` (`src/interfaces/ISnapshotSource.sol`), a strict subset of `ISnapshotState` as defined in the [SnapshotEngine](https://github.com/CMTA/SnapshotEngine) repository. It declares only the three functions the vault calls, so any `ISnapshotState` implementation satisfies it and a custom provider does not have to implement the five it would never use. + +The vault is **not** tied to the CMTAT: any contract implementing `ISnapshotState` can be used as the snapshot source, for example + +- the external `SnapshotEngine` bound to a CMTAT or to any other ERC-20, +- a token embedding the snapshot modules directly (`CMTATStandaloneInternalSnapshot`, `CMTATUpgradeableInternalSnapshot`), +- any custom contract exposing `snapshotInfo` / `snapshotInfoBatch`. + +The address is provided at initialization and is exposed by the public getter `dividendSnapshotSource()`. The vault reaches it through the three hooks of `IncomeVaultSnapshotCore`, so a token that already records snapshots can answer them from itself instead of pointing at a separate contract. ## Audits -The contracts are NOT audited, do not use them for production without auditing them !!!! +The contracts are NOT audited, do not use them for production without auditing them ! + +Static analysis is run with [Slither](https://github.com/crytic/slither) and [Aderyn](https://github.com/Cyfrin/aderyn). + +Every finding is triaged in a feedback file rather than left as a raw count, and the whole picture is summarised in [doc/audits/AUDIT_OVERVIEW.md](./doc/audits/AUDIT_OVERVIEW.md). -A report performed with [Slither](https://github.com/crytic/slither) is available in [doc/audits/tools](./doc/audits/tools/slither-report.md) +| Version | Tool | Result | Report | Triage | +| --- | --- | --- | --- | --- | +| v2.0.0 | Slither 0.11.5 | 0 High · 5 Med · 6 Low · 23 Info — nothing to fix | [report](./doc/audits/tools/v2.0.0/slither-report.md) | [feedback](./doc/audits/tools/v2.0.0/slither-report-feedback.md) | +| v2.0.0 | Aderyn 0.6.5 | 0 High · 10 Low — nothing to fix | [report](./doc/audits/tools/v2.0.0/aderyn-report.md) | [feedback](./doc/audits/tools/v2.0.0/aderyn-report-feedback.md) | +| v1.0.0 | Slither | superseded — predates the CMTAT v3 migration | [report](./doc/audits/tools/v1.0.0/slither-report.md) | — | + +```bash +slither . --checklist --filter-paths "node_modules,lib,test" \ + > doc/audits/tools/v2.0.0/slither-report.md +aderyn -x mocks --output doc/audits/tools/v2.0.0/aderyn-report.md +``` + +Both runs exclude mocks and tests. Filter on `lib` rather than on dependency names: this is a Foundry project, and a name-based filter silently puts the whole vendored tree in scope when a dependency it does not list is added. + +Check `grep -c 'lib/\|node_modules/' ` returns 0 before trusting any count. ## Documentation Here a summary of the main documentation -| Document | Link/Files | -| ----------------------- | ------------------------------------------------------ | -| Specification | [doc/specification](./doc/specification) | -| Technical documentation | [doc/technical](./doc/technical) | -| Solidity API (docgen) | [doc/solidityAPI/index.md](./doc/solidityAPI/index.md) | -| Toolchain | [doc/TOOLCHAIN.md](./doc/TOOLCHAIN.md) | -| Surya report | [doc/surya](./doc/surya/) | +| Document | Link/Files | +| ------------------------------------- | ------------------------------------------------------ | +| Specification & technical choice | [doc/README.md](./doc/README.md) | +| Solidity API (docgen) | [doc/solidityAPI/index.md](./doc/solidityAPI/index.md) | +| Toolchain | [doc/TOOLCHAIN.md](./doc/TOOLCHAIN.md) | +| Surya report | [doc/surya](./doc/surya/) | See also [Taurus - Equity Tokenization: How to Pay Dividend On-Chain Using CMTAT](https://www.taurushq.com/blog/equity-tokenization-how-to-pay-dividend-on-chain-using-cmtat/) @@ -53,7 +80,13 @@ The project is developed with [Foundry](https://book.getfoundry.sh) You must first initialize the submodules, with ``` -forge install +git submodule update --init --recursive +``` + +The upgrade safety validation performed by the [OpenZeppelin Foundry Upgrades](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades) plugin requires `@openzeppelin/upgrades-core`: + +``` +npm install ``` See also the command's [documentation](https://book.getfoundry.sh/reference/forge/forge-install). @@ -70,10 +103,10 @@ See also the command's [documentation](https://book.getfoundry.sh/reference/forg ### Compilation -The official documentation is available in the Foundry [website](https://book.getfoundry.sh/reference/forge/build-commands) +The official documentation is available in the Foundry [website](https://book.getfoundry.sh/reference/forge/build-commands) ``` - forge build --contracts src/IncomeVault.sol + forge build --contracts src/deployment/IncomeVault.sol ``` ### Testing @@ -81,9 +114,26 @@ The official documentation is available in the Foundry [website](https://book.ge You can run the tests with ``` -forge test +make test +``` + +`make help` lists every target. Use `make test` rather than `forge test` directly: + +> The OpenZeppelin Foundry Upgrades plugin validates upgrade safety from Foundry's build-info and **rejects the output of an incremental compile**. Running `forge test --ffi` straight after editing a contract therefore fails *every* test with `Failed to run upgrade safety validation: … Build info file … is not from a full compilation`, which names neither the cause nor the fix. `make test` does the full build first. (`--ffi` is required for the same reason: the plugin shells out to `@openzeppelin/upgrades-core`.) + +Other useful targets: + +``` +make install # submodules + npm dependencies +make coverage # line/branch/function coverage of src/ +make coverage-report # the same, as HTML in doc/coverage +make gas # gas report +make fmt-check lint # formatting and lint +make doc # regenerate the UML and Surya diagrams ``` +`npm run test`, `npm run build`, `npm run coverage` and `npm run lint` delegate to the same targets, so there is one definition rather than two. + To run a specific test, use ``` @@ -94,24 +144,23 @@ See also the test framework's [official documentation](https://book.getfoundry.s #### Coverage -> Unfortunately, tests are performed with a proxy deployment and the coverage command does not work currently in this configuration. - -* Perform a code coverage - ``` -forge coverage --ffi +make coverage # summary table in the terminal +make coverage-report # HTML in doc/coverage, needs lcov + genhtml ``` -* Generate LCOV report +Both targets do the full build first, for the same reason `make test` does, and scope the measurement to `src/` — tests, mocks and `script/` are excluded: ``` -forge coverage --ffi --report lcov +forge coverage --ffi --exclude-tests --no-match-coverage '(test|mocks?|script)/' ``` -- Generate `index.html` +`doc/coverage/` is committed, so regenerate it in the same commit as any change under `src/`. `make coverage-report` deletes and recreates that directory each run. -```bash -forge coverage --ffi --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage -``` +Two files report 0% and that is expected rather than a gap: `IncomeVaultSnapshotCore` and `IncomeVaultValidationCore` declare hooks with no bodies, so there is nothing in them to execute. + +See [Solidity Coverage in VS Code with Foundry](https://mirror.xyz/devanon.eth/RrDvKPnlD-pmpuW7hQeR5wWdVjklrpOgPCOA-PJkWFU) & [Foundry forge coverage](https://www.rareskills.io/post/foundry-forge-coverage) + +## Tooling -See [Solidity Coverage in VS Code with Foundry](https://mirror.xyz/devanon.eth/RrDvKPnlD-pmpuW7hQeR5wWdVjklrpOgPCOA-PJkWFU) & [Foundry forge coverage](https://www.rareskills.io/post/foundry-forge-coverage) +> Parts of this project were written with the help of AI coding assistants, principally Claude Code (Anthropic). diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..cfd6db4 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,801 @@ +# IncomeVault — Specification + +The `IncomeVault` is a prototype to perform coupon-payment dividend with a token supporting on-chain snapshots, typically a [CMTAT](https://github.com/CMTA/CMTAT) bound to a [SnapshotEngine](https://github.com/CMTA/SnapshotEngine). + + + +- [Introduction](#introduction) +- [Coverage of the CMTAT Distribution module](#coverage-of-the-cmtat-distribution-module) +- [Snapshot source](#snapshot-source) + - [Replacing the snapshot source](#replacing-the-snapshot-source) +- [Access control](#access-control) + - [Deployment variants](#deployment-variants) + - [Capability table](#capability-table) + - [Depositing for several periods](#depositing-for-several-periods) +- [Segregated Deposit](#segregated-deposit) +- [ValidationModule](#validationmodule) + - [Freezing the vault itself](#freezing-the-vault-itself) + - [RuleEngine](#ruleengine) +- [Operation](#operation) + - [Claim dividends](#claim-dividends) + - [Claim restriction](#claim-restriction) + - [Schema](#schema) +- [Claiming on behalf of a holder](#claiming-on-behalf-of-a-holder) + - [Authorising by signature (ERC-7741)](#authorising-by-signature-erc-7741) +- [Per-period residue](#per-period-residue) +- [Withdraw funds](#withdraw-funds) +- [Distribute dividend](#distribute-dividend) + - [Best-effort distribution](#best-effort-distribution) +- [Comparison with ERC-4626 / ERC-7540 vaults](#comparison-with-erc-4626--erc-7540-vaults) + - [Why ERC-4626 does not fit a dividend](#why-erc-4626-does-not-fit-a-dividend) + - [What ERC-7540 changes, and what it does not](#what-erc-7540-changes-and-what-it-does-not) + - [When a 4626 vault is the right tool](#when-a-4626-vault-is-the-right-tool) + - [A place the two could meet](#a-place-the-two-could-meet) +- [Paying the dividend in the security token itself](#paying-the-dividend-in-the-security-token-itself) + - [The vault would hold the token it is dividing](#the-vault-would-hold-the-token-it-is-dividing) + - [Minting after the record date does not avoid it](#minting-after-the-record-date-does-not-avoid-it) + - [A share dividend is not a pot divided pro-rata](#a-share-dividend-is-not-a-pot-divided-pro-rata) + - [And the payout becomes a transfer of the security](#and-the-payout-becomes-a-transfer-of-the-security) + - [What would be needed](#what-would-be-needed) +- [Source layout](#source-layout) +- [The stated API: IIncomeVault](#the-stated-api-iincomevault) +- [Embedding the distribution logic in a token](#embedding-the-distribution-logic-in-a-token) +- [Improvement](#improvement) +- [Deployment](#deployment) + - [Deployment scripts](#deployment-scripts) +- [Threat model & FAQ](#threat-model--faq) + - [Claim dividend several times](#claim-dividend-several-times) + - [New dividend after claim](#new-dividend-after-claim) + - [Transfer fails](#transfer-fails) +- [Technical choice](#technical-choice) + - [Functionality](#functionality) + - [Schema](#schema-2) + - [Inheritance](#inheritance) + - [Graph](#graph) + - [Report](#report) + + + +## Introduction + +![IncomeVault architecture](./schema/plantuml/incomevault-architecture.png) + +_Diagram source: [doc/schema/plantuml/incomevault-architecture.puml](./schema/plantuml/incomevault-architecture.puml). This is the overview; the detailed flow of the same process is [further down](#snapshot-source)._ + + \0. On the snapshot source (e.g. a `SnapshotEngine` bound to a CMTAT), the admin registers the dividend `time` to perform a snapshot and store the holder’s balance at this specified time. + +1. An authorized address perform a deposit in the `IncomeVault` for a specific `time` +2. An authorized address open the claim for this specific `time` +3. Holder claims his dividends by calling the function `claimDividend` + +## Coverage of the CMTAT Distribution module + +`IncomeVault` implements the optional **Distribution module** of the CMTA framework functional specifications (June 2026), section 3.2.4, functionalities 27 to 32. + +| # | Specification | Status | +| --- | --- | --- | +| 27 | Distribution create parameters | ◑ partial — the settlement token is fixed per vault, not per distribution | +| 28 | Distribution set eligibility | ◑ different mechanism — evaluated at payout time, not a stored flag | +| 29 | Distribution set deposit | ● `deposit`, `depositBatch` | +| 30 | Distribution claim deposit | ● `claimDividend`, `claimDividendBatch` | +| 31 | Distribution schedule *(debt)* | ○ not implemented | +| 32 | Distribution unschedule *(debt)* | ○ not implemented | + +Legend: ● implemented, ◑ partial or answered differently, ○ not implemented. + +Beyond the specification, the vault adds a **claim window**, **issuer recovery** of what is left unclaimed, a **push** counterpart to the pull claim, **claim delegation** (ERC-7540 / ERC-7741) and per-period accounting — each answering a question the specification leaves open, such as how long a holder may claim for and where the rounding residue goes. + +Functionalities 31 and 32 would need **no new state** if specified: the record dates already exist as `uint256[]` in the Snapshot module (`getNextSnapshots()`), and the terms already exist in the Debt module (`couponPaymentFrequency`, `interestScheduleFormat`, `currencyContract`) — though as strings, so a contract cannot act on them. + +> **The full comparison lives in [`doc/cmtat-standard/CMTAT-Distribution-impl.md`](./cmtat-standard/CMTAT-Distribution-impl.md)**: the functionality-by-functionality table, what the vault adds and why, the analysis of 31/32, and **twelve changes we would propose to the specification**. Those have their own files, so they can be taken to the specification without the implementation comparison attached and with the relevant specification text quoted in place: nine amendments to functionalities that already exist in [`CMTAT-Distribution-Amendments.md`](./cmtat-standard/CMTAT-Distribution-Amendments.md), and three additions the specification does not describe at all in [`CMTAT-Distribution-Additions.md`](./cmtat-standard/CMTAT-Distribution-Additions.md). The question behind C-9 — what a distribution's deposit may be held as while it waits to be claimed — has its own file, [`CMTAT-Distribution-ERC4626.md`](./cmtat-standard/CMTAT-Distribution-ERC4626.md), which is about the **deposit**, not about whether this vault should have been a 4626 one (that is [below](#comparison-with-erc-4626--erc-7540-vaults)). It also argues that the specification should stay snapshot-*oriented* without being snapshot-*only* — balances pinned off-chain at a block height fix a record date just as well — and asks what the specification should say about holding a deposit as ERC-4626 shares. + +## Snapshot source + +The vault never talks to the token directly. It holds a single reference, `snapshotEngine`, typed with **`ISnapshotSource`** (`src/interfaces/ISnapshotSource.sol`) — the three functions it actually calls, and nothing else: + +| Function | Used by | +| --- | --- | +| `snapshotInfo(uint256 time, address tokenHolder)` | `claimDividend` | +| `snapshotInfoBatch(uint256[] times, address[] addresses)` | `claimDividendBatch` | +| `snapshotInfoBatch(uint256 time, address[] addresses)` | `distributeDividend` | + +`ISnapshotSource` is a strict subset of `ISnapshotState`, defined by the [SnapshotEngine](https://github.com/CMTA/SnapshotEngine), which declares eight functions. The signatures are copied verbatim, so **every `ISnapshotState` implementation already satisfies it** — the external `SnapshotEngine`, a token embedding the snapshot modules, or a custom provider — while a new implementation only has to write the three the vault calls, not five it would never see used. Solidity has no implicit conversion between unrelated interfaces, so pass one with an explicit cast: `ISnapshotSource(address(engine))`. + +The address is set at initialization and cannot be the zero address. + +The vault does not call it directly. `IncomeVaultSnapshotCore` declares the three questions the payout paths actually ask — one holder's balance at a `time`, many holders' balances at a `time`, and one holder's balances across many `time`s — and inherits nothing. `IncomeVaultSnapshotModule` is one *answer* to them: an `ISnapshotSource` held in its own namespace, reachable through `dividendSnapshotSource()`. A token that already records snapshots answers the same three hooks from itself, with no second contract and nothing stored. See *Embedding the distribution logic in a token*. + +### Replacing the snapshot source + +`setDividendSnapshotSource` allows a migration — a redeployed `SnapshotEngine`, or a token moving to embedded snapshot modules — but **only while no claim period is open**. The vault tracks how many dividend times currently have their claims open in `openClaimCount()`, and the setter reverts with `IncomeVault_ClaimPeriodOpen(openClaimCount)` while that is non-zero. + +The reason for the gate: dividend amounts are computed from the snapshot source **at claim time**, not fixed at deposit. Swapping the source under an open period would silently re-price every unclaimed dividend of that period. + +> **The gate narrows the hazard, it does not remove it.** Entitlements are always resolved against whichever source is configured *when the claim happens*. Re-opening a past `time` after a swap resolves that period against the **new** source. Holders who already claimed are protected — `claimedDividend` persists across the change — but holders who had not are not. Treat a swap as a migration that requires every period to be settled and closed, not as a routine configuration change. If historical periods must keep resolving against the source they were created with, that needs per-`time` pinning at deposit, which this prototype does not implement. + +> The vault does **not** verify the interface through ERC-165. The canonical `SnapshotEngine` advertises no id for it, so a guard would reject the implementation the vault is built for. And ERC-165 expresses shape, never semantics: a source returning attacker-chosen balances satisfies this interface exactly as an honest one does. Trusting the snapshot source stays a configuration decision. + +![IncomeVault global flow](./schema/plantuml/incomevault-global.png) + +_Diagram source: [doc/schema/plantuml/incomevault-global.puml](./schema/plantuml/incomevault-global.puml)._ + +## Access control + +The vault separates **what** is protected from **who** may do it. The logic contracts declare one `internal view virtual` authorization hook per capability, invoked by a modifier; each deployment contract overrides the hooks with the policy it wants. Because the hooks are declared without a body, the compiler refuses to deploy a vault that has not answered the question. + +```solidity +// IncomeVaultRestricted — declares the capability +modifier onlyWithdrawManager() { _authorizeWithdraw(); _; } +function withdraw(...) public virtual onlyWithdrawManager { ... } +function _authorizeWithdraw() internal view virtual; + +// IncomeVault — declares the policy +function _authorizeWithdraw() internal view virtual override onlyRole(INCOME_VAULT_WITHDRAW_ROLE) {} +``` + +### Deployment variants + +Two deployments ship. **The choice is made at deployment and cannot be changed afterwards** — they are different contracts, not a setting, and a deployed proxy cannot be swapped from one to the other. + +| Contract | Access control | `initialize` first argument | +| --- | --- | --- | +| `IncomeVault` | Role-based, CMTAT `AccessControlModule` (`AccessControlUpgradeable`) | `address admin` | +| `IncomeVaultOwnable2Step` | Single owner, ERC-173 `Ownable2StepUpgradeable` | `address owner_` | + +### Capability table + +| Capability | Function(s) | Hook | `IncomeVault` | `IncomeVaultOwnable2Step` | +| --- | --- | --- | --- | --- | +| Fund the vault | `deposit`, `depositBatch` | `_authorizeDeposit` | `INCOME_VAULT_DEPOSIT_ROLE` | owner | +| Remove funds | `withdraw`, `withdrawAll` | `_authorizeWithdraw` | `INCOME_VAULT_WITHDRAW_ROLE` | owner | +| Push payouts | `distributeDividend`, `distributeDividendBestEffort` | `_authorizeDistribute` | `INCOME_VAULT_DISTRIBUTE_ROLE` | owner | +| Claim window | `setStatusClaim`, `setTimeLimitToWithdraw` | `_authorizeOperator` | `INCOME_VAULT_OPERATOR_ROLE` | owner | +| Compliance engine | `setRuleEngine` | `_authorizeRuleEngineManagement` | `DEFAULT_ADMIN_ROLE` | owner | +| Snapshot source | `setDividendSnapshotSource` | `_authorizeSnapshotSourceManagement` | `DEFAULT_ADMIN_ROLE` | owner | +| Emergency stop | `pause`, `unpause` | `_authorizePause` | `PAUSER_ROLE` | owner | +| Permanent kill | `deactivateContract` | `_authorizeDeactivate` | `DEFAULT_ADMIN_ROLE` | owner | +| Address freeze | `setAddressFrozen`, `batchSetAddressFrozen` | `_authorizeFreeze` | `ENFORCER_ROLE` | owner | + +> **The hook shares its name with CMTAT's, deliberately.** CMTAT declares `_authorizeRuleEngineManagement()` in `ValidationModuleRuleEngine`. A contract inheriting both that and this module has exactly **one** RuleEngine — both sit on the same `ValidationModuleRuleEngineInternal`, whose ERC-7201 slot is a hardcoded constant. One capability, one hook: a single override answering both declarations is the correct resolution. Renaming ours would create two hooks over one slot, each able to carry a different policy, and the weaker would win. Finding M-4. + +Role management itself (`grantRole` / `revokeRole`) is held by `DEFAULT_ADMIN_ROLE` in the role-based variant; in the single-owner variant, ownership moves through the two-step `transferOwnership` / `acceptOwnership` handover. + +> **`IncomeVaultOwnable2Step` cannot express separated duties.** Every capability collapses to the single owner, so the account that funds the vault is also the account that can empty it through `withdrawAll`. Pick it only when one key legitimately holds everything; an issuer paying dividends normally wants `IncomeVault`, where depositing and withdrawing are distinct privileges. + +> **The role-based admin is not constrained by role separation.** The CMTAT `AccessControlModule` treats `DEFAULT_ADMIN_ROLE` as implicitly holding every role, so the admin passes every check — but it does **not** appear in `getRoleMember` enumerations, so an off-chain tool listing role holders will not show it. Role separation constrains the operators, never the admin. + +> **`PAUSER_ROLE` and `ENFORCER_ROLE` are published by both variants** because they are declared by the CMTAT `PauseModule` and `EnforcementModule` the vault inherits. In `IncomeVaultOwnable2Step` they are never checked; granting them is impossible there and reading them means nothing. The vault's own four roles are declared in `IncomeVaultRolesStorage`, inherited only by `IncomeVault`, so they are not published by the variant that does not enforce them. + +### Depositing for several periods + +`depositBatch(times[], amounts[])` credits each `time` exactly as a separate `deposit` would — same accounting, one `newDeposit` event per entry — and pulls the payment token **once** for the total. Repeating a `time` accumulates, as separate calls would. The arrays must be the same non-zero length and every amount must be non-zero; otherwise the whole batch reverts and nothing is credited. + +**Where the saving actually is, measured:** *inside* a transaction the batch is the more expensive of the two — decoding two dynamic `calldata` arrays outweighs the single token transfer. For three periods: **115,604 gas batched against 113,517 for three separate calls.** The win is the intrinsic per-transaction cost, paid once instead of N times: + +| Three periods | in-call | + intrinsic | total | +| --- | --- | --- | --- | +| `depositBatch` | 115,604 | 21,000 x 1 | **136,604** | +| 3 x `deposit` | 113,517 | 21,000 x 3 | 176,517 | + +So it is worth using for two or more periods, and the advantage grows with the count — but it is a transaction-count optimisation, not a cheaper deposit. + +## Segregated Deposit + +Each deposit is segregated in its time value. A `time` is the dividends distribution date (Unix Timestamp) to the token holders. + +![IncomeVault segregated deposit](./schema/plantuml/incomevault-segregated-deposit.png) + +_Diagram source: [doc/schema/plantuml/incomevault-segregated-deposit.puml](./schema/plantuml/incomevault-segregated-deposit.puml)._ + +## ValidationModule + +A claim is considered as a transfer from the contract to the sender (token holder). This transfer can be restricted with the `IncomeVaultValidationModule`, which composes three CMTAT modules and an optional RuleEngine: + +- `EnforcementModule` — freeze/unfreeze an address (`ENFORCER_ROLE`) +- `PauseModule` — put the contract in the pause state (`PAUSER_ROLE`), or deactivate it +- an optional `IRuleEngine` for additional rules + +If any of them refuses the transfer, the function reverts with `IncomeVault_InvalidTransfer(from, to, value)`. + +The public view `canTransfer(from, to, value)` returns the same answer without reverting. `detectTransferRestriction` answers for the **whole** decision in the same order — deactivation, pause, either party frozen, then the RuleEngine — so it returns `0` exactly when `canTransfer` is true, and `messageForTransferRestriction` explains each code. Both use CMTAT's `REJECTED_CODE_BASE` numbering and CMTAT's message strings, so a console written against a CMTAT reads a refused payout exactly as it reads a refused transfer. + +### Freezing the vault itself + +`canTransfer` is always called with the vault as `from`, and `setAddressFrozen` accepts any address — so `ENFORCER_ROLE` can freeze **the vault**, which stops every payout: both `claimDividend` and `distributeDividend` revert. It is a second kill-switch alongside `pause`, reachable without holding `PAUSER_ROLE`. + +Know its limits before reaching for it: + +- **It is not visible through `paused()`**, which stays `false`. A monitor watching the pause flag sees a healthy vault. +- **The revert is the ordinary `IncomeVault_InvalidTransfer`**, the same error a blocked holder gets. Distinguish the two by checking the `AddressFrozen` logs for the vault's own address. +- **It does not protect the funds.** Deposits still succeed, and `INCOME_VAULT_WITHDRAW_ROLE` can still drain the contract with `withdrawAll`. It stops holders being paid; it is not a safe mode. + +Use `pause` for an emergency stop. Freezing the vault is the compliance-officer lever, and the pause state is the one an operator should monitor. + +### RuleEngine + +As for the CMTAT, there is the possibility to configure a ruleEngine with rules to perform transfer rectriction/verification. As relevant rules, we have: + +- Whitelist +- Blacklist +- Sanctionlist +- ConditionalTransfer + +The vault is **not** a token bound to the RuleEngine. It only uses the *view* entry point `IRuleEngine.canTransfer(from, to, value)`: + +- `transferred(...)` is restricted to bound tokens by the RuleEngine and would revert here; +- a dividend payout is a movement of the *payment* token, not of the security token, so it must not update the stateful rules of the engine. + +The RuleEngine can be changed at any time with `setRuleEngine` (`DEFAULT_ADMIN_ROLE`), and set to the zero address to disable the rule checks. + + + + + +## Operation + +### Claim dividends + +The distribution of dividends is not automatic. A token holder has to claim his dividends by calling the function `claimDividend`, similar to the Lido protocol. When he claims his dividends, he precises the defined `time`. + +Therefore, a token holder has to know the different `time` when a deposit has been performed. + + + +A function `claimDividend` in batch is also available to claim dividends for several different time. + +### Claim restriction + +An holder can not claim its dividends if: + +a. The claim time is in the future (`IncomeVault_TooEarlyToWithdraw`) + +b. The claim time is too far in the past, specified by `timeLimitToWithdraw` (`IncomeVault_TooLateToWithdraw`) + + `timeLimitToWithdraw` must be **greater than zero**: a limit of zero would collapse the window `[time, time + limit]` to the single instant `block.timestamp == time`, making the period unclaimable. Both `initialize` and `setTimeLimitToWithdraw` reject it with `IncomeVault_TimeLimitToWithdrawZeroNotAllowed`. Any positive value is accepted — a short settlement window can be deliberate. + +c. Claim is not enabled for this specific `time` (`IncomeVault_ClaimNotActivated`) + +d. Holder has already claim its dividends (`IncomeVault_DividendAlreadyClaimed`) + +e. There is no dividend to claim (`IncomeVault_NoDividendToClaim`) + +For the batch function, `claimDividendBatch`, `d` and `e` don't generate an error but instead, there is just no dividends distributed for this specific time. + +### Schema + +This schema describes the different smart contracts called when a token holder claims his dividends. + +![Contracts called on a claim](./schema/plantuml/incomevault-ruleengine.png) + +_Diagram source: [doc/schema/plantuml/incomevault-ruleengine.puml](./schema/plantuml/incomevault-ruleengine.puml)._ + +#### Formula + +The computation of dividends is performing according to the following formula + +``` +senderDividend = (senderCMTATBalance * dividendTotalSupply) / TokenTotalSupply; +``` + +The sender dividend will be rounded to the inferior integer. Thus, the issuer should put a “limit” date to claim his dividend in order to withdraw the staying funds (due to rounding) from the smart contract. + +Example with USDC (6 decimal) and a CMTAT (0 decimal) + +tokenSupply CMTAT = 12’351 + +The sender has 4221 tokens. + +21’555.50 $ in USDC are deposited corresponding to a value of 21555500000 tokens since USDC has 6 decimals. + + We have: + +senderDividend = 4221 * 21555500000 / 12351 = 7366671969.880981297 = 7366671969 which correspond to **7366.671969**$ + +#### Schema + +Schema without the `ValidationModule` (see next paragraph) + +![claimDividend flow](./schema/plantuml/incomevault-claimdividend.png) + +_Diagram source: [doc/schema/plantuml/incomevault-claimdividend.puml](./schema/plantuml/incomevault-claimdividend.puml)._ + + + +## Claiming on behalf of a holder + +A holder can authorise another address to trigger their claims, using the shape ERC-7540 defines: + +```solidity +vault.setOperator(custodian, true); // the holder authorises +vault.claimDividendFor(holder, time); // the custodian triggers +vault.claimDividendBatchFor(holder, times); +vault.isOperator(holder, custodian); // -> true +``` + +**The operator can never receive the dividends.** They always go to the holder; the operator pays the gas and chooses the moment. Authorisation is per holder, revocable at any time with `setOperator(operator, false)`, and every other rule is unchanged — the claim window, the already-claimed check, the pause, the freeze and the RuleEngine all apply exactly as for `claimDividend`. `OperatorSet(controller, operator, approved)` matches ERC-7540, so tooling written for that standard can index it. + +This closes the gap noted in the ERC-7540 comparison below: a custodian can now claim for the holders it serves, and a holder without gas can have someone claim for them. + +The three members whose signatures are the standard's are declared in their own interface, `src/interfaces/IERC7540Operator.sol`, so the compatibility is stated in the type system rather than in a comment: + +```solidity +interface IERC7540Operator { + event OperatorSet(address indexed controller, address indexed operator, bool approved); + function setOperator(address operator, bool approved) external returns (bool success); + function isOperator(address controller, address operator) external view returns (bool status); +} +``` + +It inherits nothing, so `type(IERC7540Operator).interfaceId` is exactly the XOR of the two selectors and equals **`0xe3bc4e65`** — the value ERC-7540 assigns to "the operator methods that all ERC-7540 Vaults implement". `testOperatorInterfaceIdMatchesTheStandard` asserts that equality, so changing either signature breaks the build rather than silently breaking a custodian's integration. + +> **The vault does not answer `true` for `0xe3bc4e65` from `supportsInterface`, on purpose.** Sharing the operator methods does not make it an asynchronous vault: a caller discovering that id would reasonably expect the ERC-7540 request lifecycle and ERC-7575's `share()`, none of which exists here. `testDoesNotClaimToBeAnErc7540Vault` pins the under-claim so it stays a decision rather than becoming an oversight. + +### Authorising by signature (ERC-7741) + +`setOperator` needs the holder to send a transaction. [ERC-7741](https://eips.ethereum.org/EIPS/eip-7741) removes that: the holder **signs** an EIP-712 message and anyone — a custodian, a relayer — submits it and pays the gas. + +```solidity +vault.authorizeOperator(controller, operator, approved, nonce, deadline, signature); +vault.invalidateNonce(nonce); // burn a nonce you no longer want honoured +vault.authorizations(controller, nonce); // has this nonce been spent? +vault.DOMAIN_SEPARATOR(); // EIP-712 domain +``` + +The signed message is exactly the standard's: + +``` +AuthorizeOperator(address controller,address operator,bool approved,bytes32 nonce,uint256 deadline) +``` + +Four details worth knowing: + +- **Smart-contract wallets work.** Signatures go through OpenZeppelin's `SignatureChecker`, so an [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) wallet authorises exactly as an EOA does — which matters, because institutional holders of a security token are usually contracts. +- **Nonces are `bytes32` and unordered**, as the standard specifies, so a holder can prepare several independent authorisations without imposing an order on them. +- **The nonce is spent before the signature is checked**, so no path can replay it. +- **The EIP-712 domain version stays `"1"` across releases.** Bumping it would silently invalidate every signature already issued. + +Unlike the ERC-7540 operator id, the vault **does** advertise `0xa9e50872` through `supportsInterface` in both variants: ERC-7741 requires it, and unlike ERC-7540 this interface is implemented in full. + +> ERC-7741 warns that "operators have significant control over users and the signed message can lead to undesired outcomes". Keep `deadline` short: a signature that leaks later remains usable until it expires or its nonce is burned with `invalidateNonce`. + +## Per-period residue + +Dividends round down, so a period keeps a residue: the rounding dust plus whatever was never claimed. Two views report it without any off-chain reconstruction: + +| View | Meaning | +| --- | --- | +| `paidDividend(time)` | how much has actually been paid out for `time` | +| `unclaimedDividend(time)` | `segregatedDividend(time) - paidDividend(time)` — what is still held for `time` | + +**`segregatedDividend` is not that number.** It is the pro-rata denominator and stays fixed at the deposit for the whole period, otherwise each claim would shrink the share of the next claimant. Only `unclaimedDividend` tells you what remains. + +`withdraw` is bounded by `unclaimedDividend`, so a sweep cannot take another period's funds. Before this bound existed, a period whose holders had all claimed still reported its full deposit in `segregatedDividend`, and sweeping it drained the money deposited for a different period — leaving that period's holders unpayable with no error raised. + +> The bound stops the damage spreading between periods. It does **not** make an early sweep safe: withdrawing before the claim window closes takes money the remaining holders of *that* period are entitled to, and lowers `segregatedDividend`, re-pricing every claim that has not happened yet. A claim that the period can no longer fund now **reverts** with `IncomeVault_NotEnoughAmount` rather than being paid out of another period's deposit. The holder is not silently short-changed and the other periods stay whole, but the swept period is genuinely unable to pay — the sweep, not the revert, is the mistake. + +## Withdraw funds + +An authorized user can call the following functions to withdraw funds from the vault: + +``` +1. withdraw(uint256 time, uint256 amount, address withdrawAddress) public onlyRole(INCOME_VAULT_WITHDRAW_ROLE) +``` + +and + +``` +2. withdrawAll(uint256 amount, address withdrawAddress) public onlyRole(INCOME_VAULT_WITHDRAW_ROLE) +``` + +With the function 1, the funds are withdrawn only from the specific time. + +The second function allows to withdraw funds without a specific time, which can lead to an “unstable” state with the different pool of dividend. To be used only in case of emergency or if the vault is closed. + + + +## Distribute dividend + +An authorized user can also decide to distribute the dividend for a given time and a given list of addresses. + +In this situation, the token holder can not decide if he wants to receive his dividends (he is forced to accept) and can not choose the address where he wants to receive his dividends. + + + +Since the function is restricted by access control (`INCOME_VAULT_DISTRIBUTE_ROLE`), it is not possible to use Chainlink Automation to perform an automatic call and distribute the dividends. Moreover, the list of token holders has to be provided by the transaction’s sender. + +`distributeDividend` is subject to the **same claim window** as a holder-driven claim: the claims must be open for that `time`, `time` must have passed, and the withdraw limit must not have expired. Without the "too early" bound the distribution would read the *live* balances — `ISnapshotState` falls back to them when no snapshot has been recorded yet — and would consume each holder's claim for that period at the wrong amount. + +It also goes through the **ValidationModule**, exactly like a claim: the vault must not be paused, neither the vault nor the holder may be frozen, and the RuleEngine must allow the payout. A holder the RuleEngine refuses cannot be paid by the issuer either. + +One blocked holder **reverts the whole distribution** rather than being skipped, so a compliance failure can never be silently dropped from a payout the operator believes succeeded. The revert carries `IncomeVault_InvalidTransfer(from, to, value)`, which names the offending address: remove it from the list and retry. + +### Best-effort distribution + +`distributeDividendBestEffort` is the alternative for a large payout run that one non-compliant address must not block. It computes the same amounts and applies the same claim window and transfer restrictions, but a holder whose payout is refused is **skipped** instead of reverting the call: + +```solidity +(uint256 paidCount, address[] memory skipped) = + vault.distributeDividendBestEffort(addresses, time); +``` + +Each skip emits `DividendDistributionSkipped(time, tokenHolder, reason)` carrying the **raw revert data**, so the cause — a freeze, a RuleEngine refusal, a payment-token failure — can be decoded off-chain. + +Choose between the two by what a partial payout means for you: + +| | `distributeDividend` | `distributeDividendBestEffort` | +| --- | --- | --- | +| One holder refused | the whole call reverts | that holder is skipped, the rest are paid | +| Use when | the distribution must be all-or-nothing | one bad address must not block the run | +| Reporting | the revert names the first offender | every skip is evented and returned | + +**A skipped holder is left completely untouched.** The payout is attempted through an external self-call wrapped in `try`/`catch`, which gives per-holder atomicity: either the holder is marked claimed *and* paid, or neither. A holder who was skipped is not marked as claimed and can still claim themselves, or be included in a later distribution. + +> The helper that call targets, `transferDividendSelf`, carries no access control of its own — it reverts `IncomeVault_OnlySelfCall` for every caller other than the vault. That check is its only protection against an unauthorized payout, and it deliberately reads `msg.sender` rather than `_msgSender()` so an ERC-2771 forwarder can never present itself as the vault. `catch` also cannot distinguish a refused payout from an out-of-gas failure. The only contracts that can consume gas there — the payment token and the RuleEngine — are admin-set and already trusted. + +## Comparison with ERC-4626 / ERC-7540 vaults + +The `IncomeVault` is called a vault, but it is **not** an [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) tokenized vault and deliberately does not implement that standard. The two solve different problems, and the difference comes down to one question: **where does the entitlement come from?** + +| | `IncomeVault` | ERC-4626 vault | +| --- | --- | --- | +| Entitlement | fixed by a **snapshot at a record date** — who held the token at `time` | continuous — whoever holds shares **now** owns a pro-rata claim | +| Unit of account | the security token (CMTAT), issued and governed elsewhere | the vault's own ERC-20 share, minted on deposit | +| How value reaches the holder | a **transfer** of a *different* token (e.g. USDC) | the **share price rises**; value is extracted by redeeming | +| Effect on the position | none — the holder keeps every token | `redeem`/`withdraw` **burns shares** | +| Periods | many, segregated by `time`, each with its own deadline | one pooled `totalAssets()` | +| Compliance on payout | RuleEngine, pause and freeze checked on every payout | no hook in the standard; `maxWithdraw` must return `0` rather than revert | +| Undistributed funds | swept by the issuer after `timeLimitToWithdraw` | remain in `totalAssets()`, accruing to holders | + +### Why ERC-4626 does not fit a dividend + +Four of those rows are not preferences, they are blockers: + +1. **There is no record date in ERC-4626.** Entitlement follows the share. A buyer who acquires the token *after* the record date but before the payout would capture the dividend, and a seller who sold after the record date would lose it. That inverts the corporate-action semantics a coupon or dividend is meant to have — which is what the snapshot exists to pin down. +2. **The security token would have to *be* the share.** ERC-4626's share is the vault contract's own ERC-20. A CMTAT is already issued, with its own register, transfer restrictions and identifier; its supply is set by the issuer, not by deposits. Making it 4626-compliant is not possible, and the alternative — holders depositing the CMTAT to receive vault shares — puts a *different* token into circulation and splits the register. +3. **A dividend is not a redemption.** ERC-4626 offers exactly one way to extract value, and it burns shares. Paying a coupon must not reduce the holder's stake in the instrument. The standard has no operation for "pay out without reducing the claim". +4. **Two different tokens.** `asset()` is the single token shares are redeemed for. The vault pays USDC to holders of a CMTAT — shares of X, paid in Y — which is outside the standard's model. + +### What ERC-7540 changes, and what it does not + +[ERC-7540](https://eips.ethereum.org/EIPS/eip-7540) extends ERC-4626 with **asynchronous** flows: `requestDeposit` / `requestRedeem` queue an intent, an operator fulfils it at a price decided at fulfilment, and the controller then claims. It exists because real-world-asset and cross-chain vaults cannot settle atomically. + +That solves a **settlement-timing** problem, not an **entitlement** problem. The claim is still share-price based and still continuous, so none of the four blockers above is removed by adopting it. + +It does bring things this vault does not have, and they are worth knowing about: + +- a standard **request lifecycle** any 7540-aware interface can drive, instead of this project's bespoke `deposit` → `setStatusClaim` → `claimDividend` sequence; +- `setOperator` delegation (extended by [ERC-7741](https://eips.ethereum.org/EIPS/eip-7741) for signed authorisation), where the vault has none — a holder cannot appoint someone to claim on their behalf; +- specified **cancellation** of a pending request ([ERC-7887](https://eips.ethereum.org/EIPS/eip-7887)); +- multi-asset share tokens ([ERC-7575](https://eips.ethereum.org/EIPS/eip-7575)). + +Two ERC-7540 rules show how different the model is: `preview*` functions **must revert** in an async flow, since no honest quote exists before fulfilment — whereas this vault can always compute a claim exactly from the snapshot; and `requestId = 0` has a defined meaning rather than signalling "no request". + +### When a 4626 vault *is* the right tool + +If the instrument is **accumulating** rather than distributing — the holder's claim grows continuously and they realise it by redeeming — then ERC-4626 is the correct standard and reimplementing it here would be a mistake. A money-market fund share, a staking wrapper, or a fund whose NAV simply rises all fit that shape. Use ERC-7540 on top when settlement cannot be atomic. + +The dividing line is whether the payout is **discrete and dated** (this vault) or **continuous and embedded in the price** (ERC-4626). + +### A place the two could meet + +Payment tokens deposited for a `time` sit idle in this contract from `deposit` until each holder claims — potentially months. A future version could hold that float as shares of a 4626 vault and redeem on each payout, so the undistributed dividend earns yield instead of nothing. + +It is deliberately **not** implemented: the vault owes a *fixed nominal amount* per period, while 4626 shares carry share-price risk. A loss in the underlying vault would leave the contract unable to pay the amount it recorded at `deposit`, turning a bookkeeping contract into one that can be short. Doing it safely needs a buffer policy and an explicit rule for who absorbs a shortfall — a materially larger design than the one this prototype implements. + +## Paying the dividend in the security token itself + +A **scrip** or **stock dividend** pays holders in more of the same security rather than in cash. The vault does not support it, and nothing in the code stops an issuer trying: `_setERC20TokenPayment` rejects only the zero address, so a vault can be pointed at the CMTAT it distributes for and will deploy. The failure is in the arithmetic, not in a guard. + +### The vault would hold the token it is dividing + +To pay in the security token the vault must hold a stock of it. Those tokens are part of the token's total supply, and the pro-rata formula divides by exactly that: + +``` +holderDividend = snapshotBalanceOf(holder, time) * segregatedDividend[time] / snapshotTotalSupply(time) +``` + +If the vault holds `X` at the record date, `snapshotTotalSupply(time)` includes `X`. Summing over every holder other than the vault: + +``` +total paid out = segregatedDividend[time] * (totalSupply - X) / totalSupply < segregatedDividend[time] +``` + +Holders collectively receive **less than the amount deposited**, short by the fraction the vault itself holds — they are diluted by their own dividend. The shortfall stays in the vault, and the vault has a snapshot balance of its own, so it is nominally entitled to a share of the distribution it is administering. The pot sits inside the denominator it is divided by. + +This is silent. Every call succeeds and the numbers look plausible; only the totals are wrong. + +### Minting after the record date does not avoid it + +Both figures come from the **snapshot at the record date**, never from live balances. So minting new tokens *after* `time` does not affect that period's computation at all. + +What it does affect is every *later* record date: the vault's undistributed stock is in the supply for each of them, and it shrinks as holders claim, so the dilution differs per period and moves as claims arrive. Minting *before* the record date inflates that period's denominator directly. + +### A share dividend is not a pot divided pro-rata + +Even with the accounting fixed, the formula is the wrong shape. A scrip dividend is normally declared as a **ratio** — one new share for every `N` held — which is `floor(balance / N)`, not `balance * pot / totalSupply`. The two agree only if the issuer back-computes a pot from the ratio and the supply, and they diverge on rounding: dust in a stablecoin is a rounding error, dust in shares is a fractional entitlement an issuer usually has to settle in cash. + +### And the payout becomes a transfer of the security + +`safeTransfer` on a CMTAT runs **the token's own** pause, freeze and RuleEngine checks, which are independent of the vault's. So the vault must itself be an eligible holder of the security, every recipient must be eligible at claim time, and a claim can revert inside the token even after the vault's own validation passed. Two compliance layers, neither aware of the other. Unlike the dilution, this half fails closed — the claim reverts rather than paying the wrong amount. + +### What would be needed + +Not a payment-token swap. A share distribution needs the entitlement expressed as a ratio, the newly issued shares excluded from the denominator (or minted only as each holder claims), and a stated policy for fractional entitlements. That is a different mechanism from segregating a pot and dividing it, which is what this vault is. + +## Source layout + +Each directory says what its files **are**, following the convention CMTAT uses: + +| Path | Holds | +| --- | --- | +| `src/IncomeVaultBase.sol` | the composition root: the distribution logic, no meta-transaction policy | +| `src/IncomeVaultBaseERC2771.sol` | the same plus the ERC-2771 context — what the shipped deployments inherit | +| `src/deployment/` | the two deployable contracts, and nothing abstract | +| `src/public/` | the external surface, split by **who may call it** | +| `src/modules/` | the abstract capability mixins, one per capability | +| `src/interfaces/` | interfaces | +| `src/storage/` | declaration-only contracts: errors, events, role constants | + +**`src/public/` is split by authorization, on purpose.** Every function in `IncomeVaultOpen` is permissionless; every function in `IncomeVaultRestricted` is gated by an authorization hook. The first question anyone asks of a contract holding other people's dividends is *what can an arbitrary address do to it?* — and here that is answered by opening one file, not by auditing which modifier each function carries. The two are not to be merged into a single "distribution module". + +## The stated API: `IIncomeVault` + +`src/interfaces/IIncomeVault.sol` declares everything an integrator calls — claiming, funding, pushing payouts, claim administration, and the state getters — so a caller imports one interface rather than a concrete contract and the whole graph behind it (CMTAT, the RuleEngine, the upgrade plumbing). + +It is inherited by `IncomeVaultInternal`, the common base of both payout paths, so **the compiler** keeps it in step with the implementation rather than a convention doing it. That also means an embedded host presents the same API as the standalone vault — the two deployments are one interface, not two. + +Both deployment variants advertise `type(IIncomeVault).interfaceId` through `supportsInterface`. + +Three things are deliberately outside it: + +| Left out | Why | +| --- | --- | +| `setOperator` / `isOperator`, and the signed variant | They belong to `IERC7540Operator` and `IERC7741`, implemented alongside. Restating a standardised name would fork it. | +| `transferDividendSelf` | `public` only because `try`/`catch` needs an external call; it rejects every caller but the contract itself. | +| Pause, freeze, `setRuleEngine`, the snapshot setter | Those belong to the standalone deployment's own modules, not to the distribution API an embedded host implements. | + +The enum `TIME_ERROR_CODE` lives on the interface, because `validateTimeCode` returns it: a caller holding only the interface must be able to interpret the answer. + +## Embedding the distribution logic in a token + +`IncomeVault` is the standalone answer: a separate contract holding the payment token, reading balances from an external snapshot source, and running its own pause/freeze/RuleEngine stack. It is not the only one. A token that **already** has a validation stack and **already** records snapshots — a `CMTATUpgradeableInternalSnapshot`, for instance — can inherit `IncomeVaultOpen` and `IncomeVaultRestricted` directly and pay its own dividends, with no second contract, no second copy of the compliance rules and no snapshot address to keep in sync. + +Two abstract contracts make that possible. Both **inherit nothing**, so a host answering them adds no bases of its own and cannot fail to linearize. + +| Contract | Declares | The standalone answer | A CMTAT host's answer | +| --- | --- | --- | --- | +| `IncomeVaultValidationCore` | `_validateTransfer` | `IncomeVaultValidationModule`, built on the CMTAT `PauseModule`, `EnforcementModule` and RuleEngine | its own `canTransfer` | +| `IncomeVaultSnapshotCore` | `_snapshotInfo`, `_snapshotInfoBatch` (x2) | `IncomeVaultSnapshotModule`, an `ISnapshotSource` in storage | its own snapshot records | + +The host then supplies the four `_authorize*` hooks with whatever access-control policy it already uses. + +Neither split is cosmetic — each removed a hard compile failure: + +- **Inheriting the policy** meant `IncomeVaultOpen` and `IncomeVaultRestricted` dragged `PauseModule` and `EnforcementModule` in transitively. A host that already had them could not linearize at all: `Error (5005)`, which no `override` list can repair. +- **Storing the source** meant a public `snapshotEngine()` getter. CMTAT declares a function with that exact name and the same parameters but a **different return type**, and Solidity cannot reconcile two functions that differ only in return type — again unresolvable by any override. + +Renaming the getter to `dividendSnapshotSource()` and moving the source into its own ERC-7201 namespace removes both the collision and the storage slot, so a host that answers the hooks from itself never allocates one. + +`test/mocks/CMTATDividendHostMock.sol` is a `CMTATUpgradeableInternalSnapshot` with the distribution logic embedded, and `test/mocks/EmbeddedDividendHostMock.sol` is the same without any CMTAT at all. Both exist only to **compile**: re-couple either dependency and they stop compiling. + +## Improvement + +- An automatic distribution of dividend could be performed through [Chainlink Automation](https://docs.chain.link/chainlink-automation) but it requires several changes to allow that. +- Only ERC20 tokens are supported. We could extends this to support direct native (e.g ether) too. + +## Deployment + +The contract has to be deployed with a transparent proxy and the contract is compatible with the standard [ERC-2771](https://eips.ethereum.org/EIPS/eip-2771) for meta transactions. + +``` +initialize( + address admin, // `owner_` on IncomeVaultOwnable2Step + IERC20 ERC20TokenPayment_, + ISnapshotSource snapshotEngine_, + IRuleEngine ruleEngine_, + uint256 timeLimitToWithdraw_ +) +``` + +### Deployment scripts + +One script per variant, in [script](../script): + +| Script | Deploys | +| --- | --- | +| `script/DeployIncomeVault.s.sol` | the role-based `IncomeVault` | +| `script/DeployIncomeVaultOwnable2Step.s.sol` | the single-owner `IncomeVaultOwnable2Step` | + +Both use the same `Upgrades` plugin the tests use, so the deployment path is the tested one. + +```bash +forge clean && forge build # a FULL build: the upgrade-safety validation requires it + +export PROXY_ADMIN=0x... # owner of the ProxyAdmin, i.e. who may upgrade +export VAULT_ADMIN=0x... # receives DEFAULT_ADMIN_ROLE (VAULT_OWNER for the Ownable variant) +export PAYMENT_TOKEN=0x... # the ERC-20 dividends are paid in +export SNAPSHOT_ENGINE=0x... # the ISnapshotSource +export TIME_LIMIT_TO_WITHDRAW=31536000 +export FORWARDER=0x... # optional, ERC-2771; omit to disable gasless support +export RULE_ENGINE=0x... # optional; omit for no transfer restrictions + +forge script script/DeployIncomeVault.s.sol --rpc-url --broadcast --ffi +``` + +`--ffi` is required, as it is for the tests. + +Before spending gas the script rejects a configuration the **contract cannot check for itself**: that `PAYMENT_TOKEN`, `SNAPSHOT_ENGINE` and any `RULE_ENGINE` are actually contracts. A mistyped address, or one copied from another chain, otherwise initializes cleanly and only reverts on the first claim. + +`deploy(config)` is separated from the environment reading in `run()`, so `test/script/Deploy.t.sol` drives the same code an operator runs — including an end-to-end check that a vault the script produced actually pays a dividend. + + + +## Threat model & FAQ + +### Claim dividend several times + +> What if a holder tries to claim the same dividend several times? + +When a holder claims his dividends for a specific time, a boolean is set to true to indicate the claiming dividend. + +``` + claimedDividend[tokenHolder][time] = true; +``` + +This boolean is set inside the internal function `_transferDividend` + +Moreover, the functions to claim are protected against reentrancy attacks with the modifier `nonReentrant` from OpenZeppelin. + +### New dividend after claim + +> What happens if the authorized address deposit dividend after that a token holder has already claimed his dividends ? + +A token holder can not claim his dividends if the claim status is not opened. Moreover, you can not deposit new dividends if the status is on open (=true). + +The function `setStatusClaim` allows to open (true) or close(false) the claims for a specific time. + +If you close the claim (claim status = false) and deposit new dividends, the previous token holders will be penalized since the dividends total supply for this specific time has improved for all token holders which have not already claimed their dividends, + +In summary, when you have opened the claim, you should not deposit new dividends in the vault for a specific time. + +### Transfer fails + +> What happens if the token transfer fails? + +In this case, the whole transaction is reverted, and the smart contract still considers that dividends have not been claimed by the token holder (sender). + +## Technical choice + +> Parts of this project were written with the help of AI coding assistants, principally Claude Code (Anthropic). + +### Functionality + +#### Upgradeable + +The `IncomeVault` is upgradeable and can be deployed with a Transparent Proxy. + +#### Version + +Every deployment exposes its release version through the ERC-3643 `version()` view, the same way the CMTAT, the RuleEngine and the SnapshotEngine do: + +```solidity +IERC3643Version(address(vault)).version() // "2.0.0" +``` + +The value is the compile-time constant `VERSION` in `src/modules/VersionModule.sol`. Bump it together with the `CHANGELOG.md` heading of the release — the changelog checklist lists it as the first task. + +#### Storage (ERC-7201) + +The state of the vault is held in a single [ERC-7201](https://eips.ethereum.org/EIPS/eip-7201) namespaced storage struct, the pattern used by OpenZeppelin Upgradeable and by the CMTAT: + +```solidity +// keccak256(abi.encode(uint256(keccak256("IncomeVault.storage.IncomeVaultInternal")) - 1)) & ~bytes32(uint256(0xff)) +bytes32 private constant IncomeVaultInternalStorageLocation = 0xe4f8b033bcfc537db031b0e68e3c1ab0f1de86cf03893d031b6590510b0c0c00; + +/// @custom:storage-location erc7201:IncomeVault.storage.IncomeVaultInternal +struct IncomeVaultInternalStorage { + ISnapshotState _snapshotEngine; + IERC20 _ERC20TokenPayment; + mapping(address tokenHolder => mapping(uint256 time => bool claimed)) _claimedDividend; + mapping(uint256 time => uint256 dividend) _segregatedDividend; + mapping(uint256 time => bool status) _segregatedClaim; + uint256 _timeLimitToWithdraw; +} +``` + +Because the namespace is derived from a hash, it cannot collide with the storage of the inherited CMTAT and OpenZeppelin modules, which use their own namespaces. Consequences: + +- there is **no** `uint256[50] private __gap` anywhere, and the contract declares no sequential storage slot at all; +- a new field can simply be appended to the struct in a later version; +- the fields are read through the public getters `ERC20TokenPayment()`, `claimedDividend()`, `segregatedDividend()`, `segregatedClaim()` and `timeLimitToWithdraw()`, so the external interface is the same as if they were public state variables; +- **it is not the only namespace.** One capability owns one module and one namespace: + +| Namespace | Owned by | Holds | +| --- | --- | --- | +| `IncomeVault.storage.IncomeVaultInternal` | `IncomeVaultInternal` | the distribution state: payment token, per-period deposits, claim flags, paid totals, open-period count, claim window | +| `IncomeVault.storage.SnapshotSource` | `IncomeVaultSnapshotModule` | the external `ISnapshotSource` | +| `IncomeVault.storage.Operator` | `IncomeVaultOperatorModule` | the claim-delegation authorisations | +| `IncomeVault.storage.ERC7741Module` | `ERC7741Module` | the consumed signature nonces | + + A host embedding only part of the logic allocates only the namespaces it inherits — a token that is its own snapshot source never allocates the second one at all. A new capability with state gets a new namespace, never a field appended to an existing struct. + +The hardcoded slots are re-derived from their namespaces, checked to be disjoint, and compared against what the proxy really stores in `test/IncomeVaultStorage.t.sol`. + +#### Urgency mechanism + +Through the `PauseModule`, the contract can be put in pause (`PAUSER_ROLE`), forbidding all claims. A paused contract can also be permanently deactivated with `deactivateContract` (`DEFAULT_ADMIN_ROLE`). + +#### Token agnostic + +The vault reads the holder balances and the total supply through the `ISnapshotState` interface of the [SnapshotEngine](https://github.com/CMTA/SnapshotEngine), so it works with any contract implementing it and not only with the CMTAT. See [Snapshot source](#snapshot-source). + +#### Reentrancy + +`claimDividend` and `claimDividendBatch` are protected with `ReentrancyGuardTransient` (EIP-1153 transient storage). `ReentrancyGuardUpgradeable` was removed from OpenZeppelin Contracts Upgradeable v5.7.0; the transient variant is storage-free and therefore proxy safe. + + +#### Gasless support + +> The gasless integration was not part of the audit performed by ABDK on the version [1.0.1](https://github.com/CMTA/RuleEngine/releases/tag/1.0.1) + +The `IncomeVault` contract supports client-side gasless transactions using the [Gas Station Network](https://docs.opengsn.org/#the-problem) (GSN) pattern, the main open standard for transfering fee payment to another account than that of the transaction issuer. The contract uses the CMTAT `ERC2771Module`, a thin wrapper around the OpenZeppelin contract `ERC2771ContextUpgradeable`, which allows a contract to get the original client with `_msgSender()` instead of the fee payer given by `msg.sender` . + +At deployment, the parameter `forwarder` inside the contract constructor has to be set with the defined address of the forwarder. Please note that the forwarder can not be changed after deployment. + +Please see the OpenGSN [documentation](https://docs.opengsn.org/contracts/#receiving-a-relayed-call) for more details on what is done to support GSN in the contract. + +**Gasless support is a deployment decision, not a property of the distribution logic.** `IncomeVaultBase` states what the vault does and knows nothing about forwarders; `IncomeVaultBaseERC2771` adds the ERC-2771 context on top of it and resolves the `ERC2771ContextUpgradeable` / `ContextUpgradeable` diamond. Both shipped deployments inherit the latter, so they behave exactly as described above. A deployment that does not want a trusted forwarder inherits `IncomeVaultBase` directly and carries none of the machinery — not an immutable forwarder address, not the calldata-suffix handling, not the `isTrustedForwarder` entry point. Before this split the only way to decline was to pass the zero address and pay for it anyway. + +This matters beyond taste: **a trusted forwarder can name any `_msgSender()`**, so it is as privileged as every role behind it. A deployment with no need for meta-transactions should not carry one. + +### Schema + +> The diagrams below are generated from the sources. Regenerate them with `npm run uml` (UML class diagram) and the three scripts in [doc/script](./script) — they rebuild the full per-contract set under [doc/surya](./surya): call graphs, inheritance graphs and markdown reports. + +#### UML + +![uml](./schema/classDiagram.svg) + +### Inheritance + +#### IncomeVault + +![surya_inheritance_IncomeVault](./surya/surya_inheritance/surya_inheritance_IncomeVault.sol.png) + +#### IncomeVaultOwnable2Step + +The other deployment variant, for comparison: the same distribution logic under a single ERC-173 owner instead of roles. + +![surya_inheritance_IncomeVaultOwnable2Step](./surya/surya_inheritance/surya_inheritance_IncomeVaultOwnable2Step.sol.png) + +#### IncomeVaultBaseERC2771 + +The base both deployments inherit. `IncomeVaultBase` sits below it and carries no meta-transaction policy at all. + +![surya_inheritance_IncomeVaultBaseERC2771](./surya/surya_inheritance/surya_inheritance_IncomeVaultBaseERC2771.sol.png) + +#### IncomeVaultValidationModule + +![surya_inheritance_IncomeVaultValidationModule](./surya/surya_inheritance/surya_inheritance_IncomeVaultValidationModule.sol.png) + +### Graph + +#### IncomeVault + +![surya_graph_IncomeVault](./surya/surya_graph/surya_graph_IncomeVault.sol.png) + +#### IncomeVaultOpen + +![surya_graph_IncomeVaultOpen](./surya/surya_graph/surya_graph_IncomeVaultOpen.sol.png) + +#### IncomeVaultRestricted + +![surya_graph_IncomeVaultRestricted](./surya/surya_graph/surya_graph_IncomeVaultRestricted.sol.png) + +#### IncomeVaultValidationModule + +![surya_graph_IncomeVaultValidationModule](./surya/surya_graph/surya_graph_IncomeVaultValidationModule.sol.png) + +### Report + +A markdown report per contract (functions, visibility, modifiers) is available in [doc/surya/surya_report](./surya/surya_report) — one per file under `src/`, 21 in total. The call graphs and inheritance graphs in [doc/surya](./surya) cover the same 21; only a few are embedded above. diff --git a/doc/TOOLCHAIN.md b/doc/TOOLCHAIN.md index 7ca1282..504bdaa 100644 --- a/doc/TOOLCHAIN.md +++ b/doc/TOOLCHAIN.md @@ -1,16 +1,35 @@ # TOOLCHAIN -[TOC] + + +- [Dependencies](#dependencies) +- [Node.JS package](#nodejs--package) + - [Dev](#dev) +- [Submodule](#submodule) +- [Generate documentation](#generate-documentation) + - [docgen](#docgen) + - [sol2uml](#sol2uml) + - [Surya](#surya) + - [Slither](#slither) +- [Coverage](#coverage) +- [Code style guidelines](#code-style-guidelines) + + ## Dependencies -The toolchain includes the following components, where the versions -are the latest ones that we tested: +The toolchain includes the following components, where the versions are the latest ones that we tested: -- Solidity 0.8.22 (via solc-js) -- OpenZeppelin Contracts (submodule) [v5.0.2](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/releases/tag/v5.0.2) -- CMTAT [v2.4.0](https://github.com/CMTA/CMTAT/releases/tag/v2.4.0) -- RuleEngine [v2.0.0](https://github.com/CMTA/RuleEngine/releases/tag/v2.0.0) +- Solidity 0.8.36, EVM target `prague` +- OpenZeppelin Contracts (submodule) [v5.7.0](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.7.0) +- OpenZeppelin Contracts Upgradeable (submodule) [v5.7.0](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/releases/tag/v5.7.0) +- OpenZeppelin Foundry Upgrades (submodule) [v0.4.2](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/releases/tag/v0.4.2) +- CMTAT [v3.3.0-rc3](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0-rc3) +- RuleEngine [v3.0.0-rc5](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc5) +- SnapshotEngine [v0.5.0](https://github.com/CMTA/SnapshotEngine/releases/tag/v0.5.0) +- forge-std [v1.16.1](https://github.com/foundry-rs/forge-std/releases/tag/v1.16.1) + +> CMTAT v3.3.0 and RuleEngine v3.0.0 are still release candidates. They are the versions the CMTA ecosystem is aligned on: RuleEngine v3.0.0-rc5 pins CMTAT v3.3.0-rc3, and SnapshotEngine v0.5.0 pins CMTAT v3.3.0-rc1. Pin them to a stable release as soon as one is published. ## Node.JS package @@ -24,8 +43,7 @@ This section concerns the packages installed in the section `devDependencies` of [Hardhat](https://hardhat.org/) plugin for integration with Foundry -**[Ethlint](https://github.com/duaraghav8/Ethlint)** -Solidity static analyzer. +**[Ethlint](https://github.com/duaraghav8/Ethlint)** Solidity static analyzer. **[prettier-plugin-solidity](https://github.com/prettier-solidity/prettier-plugin-solidity)** @@ -49,12 +67,10 @@ Utility tool for smart contract systems. ## Submodule -**[OpenZeppelin Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts)** -OpenZeppelin Contracts -The version of the library used is available in the [READEME](../README.md) +**[OpenZeppelin Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts)** OpenZeppelin Contracts The version of the library used is available in the [READEME](../README.md) -Warning: -- Submodules are not automatically updated when the host repository is updated. +Warning: +- Submodules are not automatically updated when the host repository is updated. - Only update the module to a specific version, not an intermediary commit. @@ -83,39 +99,43 @@ npm run-script uml:test Or only specified contracts ``` -npx sol2uml class -i -c src/IncomeVault.sol +npx sol2uml class -i -c src/deployment/IncomeVault.sol ``` -The related component can be installed with `npm install` (see [package.json](./package.json)). +The related component can be installed with `npm install` (see [package.json](../package.json)). ### [Surya](https://github.com/ConsenSys/surya) -Several bash scripts are available to generate the documentation in [./script ](./script ). - -#### Graph - -To generate graphs with Surya, you can run the following command +Three bash scripts in [doc/script](./script) regenerate the whole documentation set — one call graph, one inheritance graph and one markdown report per `.sol` file under `src/`: ```bash -npm run-script surya:graph +npm run-script surya:graph # doc/script/script_surya_graph.sh +npm run-script surya:inheritance # doc/script/script_surya_inheritance.sh +npm run-script surya:report # doc/script/script_surya_report.sh ``` -OR +Run `surya:graph` **first**: it creates the scratch directory `docOut/` with `mkdir -p`, which the two other scripts expect to already exist. + +The output lands in `docOut/{surya_graph,surya_inheritance,surya_report}` at the repository root. Replace the committed directories under [doc/surya](./surya) with it, deleting the old ones first so the diagrams of a renamed or removed contract do not survive: ```bash - npx surya graph src/IncomeVault.sol | dot -Tpng > surya_graph_IncomeVault.png +rm -rf doc/surya/surya_graph doc/surya/surya_inheritance doc/surya/surya_report +mkdir -p doc/surya/surya_graph doc/surya/surya_inheritance doc/surya/surya_report +mv docOut/surya_graph/* doc/surya/surya_graph/ +mv docOut/surya_inheritance/* doc/surya/surya_inheritance/ +mv docOut/surya_report/* doc/surya/surya_report/ +rm -rf docOut ``` - -#### Report +Graphviz (`dot`) is required — without it the scripts silently produce 0-byte PNGs. To generate a single graph by hand: ```bash -npm run-script surya:report +npx surya graph src/deployment/IncomeVault.sol | dot -Tpng > surya_graph_IncomeVault.png ``` - +> Known `surya graph` bug: it crashes on a contract calling `super.()` when the base is declared in another file, and because the scripts pipe into `dot` the crash shows up as an empty PNG rather than an error. Check for `find doc/surya -name '*.png' -size 0` after regenerating. ### [Slither](https://github.com/crytic/slither) @@ -127,6 +147,27 @@ npm run-script surya:report +## Coverage + +```bash +make coverage # summary table in the terminal +make coverage-report # HTML in doc/coverage (needs lcov + genhtml) +``` + +`doc/coverage/` is **generated and committed**. Because it is generated, it is only as good as the last run: regenerate it with `make coverage-report` in the same commit as any change under `src/`. This repository already carried another project's coverage output — `RuleEngine.sol`, `RuleWhitelist.sol`, `RuleSanctionList.sol` — for long enough that it read as authoritative, and a report describing the wrong codebase is worse than no report; keeping the tracked one current is what prevents that, not ignoring it. `make coverage-report` also drops a `README.md` into that directory, copied from `doc/script/coverage-README.md`, since genhtml recreates the directory each run. + +Current figures are roughly 96% of lines and 98% of branches. + +Two files report **0%**, which is expected rather than a gap: `IncomeVaultSnapshotCore` and `IncomeVaultValidationCore` declare hooks with no bodies, so there is nothing in them to execute. Function coverage is the least useful of the four figures for the same reason — it counts the empty `_authorize*` overrides, whose whole purpose is to carry a modifier. + +Scope is `src/` only: + +``` +forge coverage --ffi --exclude-tests --no-match-coverage '(test|mocks?|script)/' +``` + +`--ffi` is required, as everywhere else: the OpenZeppelin Upgrades plugin shells out to `@openzeppelin/upgrades-core`. + ## Code style guidelines We use the following tools to ensure consistent coding style: @@ -146,4 +187,4 @@ npm run-script lint:sol:test npm run-script lint:sol:test:fix ``` -The related components can be installed with `npm install` (see [package.json](./package.json)). +The related components can be installed with `npm install` (see [package.json](../package.json)). diff --git a/doc/audits/AUDIT_OVERVIEW.md b/doc/audits/AUDIT_OVERVIEW.md new file mode 100644 index 0000000..1349875 --- /dev/null +++ b/doc/audits/AUDIT_OVERVIEW.md @@ -0,0 +1,64 @@ +# Audit and analysis overview + +> **The contracts are NOT audited.** No third party has reviewed this code. Everything below is self-assessment and tool output. Do not deploy to production without an audit. + +## Scope + +`src/` only — 21 Solidity files, 982 nSLOC. Tests, mocks and deployment scripts are out of scope for static analysis; `script/` carries deliberate string `require` messages for operator diagnostics and is excluded from the style check by project convention. + +## Analyses + +| Analysis | Version | Report | Triage | +| --- | --- | --- | --- | +| Slither | v2.0.0 | [`tools/v2.0.0/slither-report.md`](./tools/v2.0.0/slither-report.md) | [feedback](./tools/v2.0.0/slither-report-feedback.md) | +| Aderyn | v2.0.0 | [`tools/v2.0.0/aderyn-report.md`](./tools/v2.0.0/aderyn-report.md) | [feedback](./tools/v2.0.0/aderyn-report-feedback.md) | +| Code-quality review (AI, not a security audit) | v2.0.0 | [`tools/v2.0.0/CLAUDE_ANALYSIS.md`](./tools/v2.0.0/CLAUDE_ANALYSIS.md) | — | +| Slither | v1.0.0 | [`tools/v1.0.0/slither-report.md`](./tools/v1.0.0/slither-report.md) | — | + +## Static-analysis results, v2.0.0 + +| Tool | High | Medium | Low | Informational | Anything to fix? | +| --- | --- | --- | --- | --- | --- | +| Slither 0.11.5 | 0 | 5 | 6 | 23 | **No** — all false positives or documented design decisions | +| Aderyn 0.6.5 | 0 | — | 10 | — | **No** — the two unused imports were removed; L-9's four remaining instances are `@inheritdoc` false positives | + +Both runs were scope-checked: neither report cites `lib/` or `node_modules/`, so no vendored dependency inflates the counts. + +### The two real findings — fixed + +| Location | Finding | Status | +| --- | --- | --- | +| `src/modules/Ownable2StepERC165Module.sol:7` | unused `IERC165` import | removed | +| `src/public/IncomeVaultRestricted.sol:11` | unused `ISnapshotSource` import, left over from finding M-2 | removed | + +Aderyn was re-run after the removal: L-9 fell from 6 instances to 4. That drop is the evidence the fix landed. Neither import contributed bytecode, so this is source hygiene rather than a behaviour change; 214 tests pass unchanged. + +The four remaining L-9 instances are **false positives**: they are consumed by `@inheritdoc`, which requires the base imported by name and which Aderyn does not parse. Deleting them fails the build. + +### One open decision + +`ERC7741Module.invalidateNonce` changes state without emitting an event (Aderyn L-10). ERC-7741 defines no event for it, so the contract is conformant — but an indexer cannot observe a nonce burned outside a signature use. Cheaper to decide before the ABI is frozen than after. + +## Substantive findings that were fixed + +These came from the code-quality review and the modularity review, not from the static analyzers. Static analysis found none of them, which is the honest measure of what these tools do and do not cover. + +| id | Finding | Where | +| --- | --- | --- | +| H-1 | `distributeDividend` ignored the claim window, so a push payout could be computed from **live** balances instead of the snapshot | `IncomeVaultRestricted` | +| H-2 | `distributeDividend` bypassed pause, freeze and the RuleEngine, so a push could pay a blocked holder | `IncomeVaultValidationModule` | +| E-3 | `withdraw` was bounded by `segregatedDividend`, letting a fully-claimed period drain another period's funds | `IncomeVaultInternal` | +| A-1 | A zero `timeLimitToWithdraw` produced a one-second claim window | `IncomeVaultInternal` | +| — | `INCOME_VAULT_DISTRIBUTE_ROLE` hashed the deposit role's string, so the two roles collided | `IncomeVaultRolesStorage` | +| — | A claim made after a mid-window sweep could be funded from another period's deposit (found by the invariant suite) | `IncomeVaultInternal` | +| M-1, M-2 | The payout logic could not be embedded in a CMTAT at all (`Error 5005`, then an irreconcilable `snapshotEngine()` return-type collision) | `modules/` | + +## Reproducing + +```bash +slither . --checklist --filter-paths "node_modules,lib,test" \ + > doc/audits/tools/v2.0.0/slither-report.md +aderyn -x mocks --output doc/audits/tools/v2.0.0/aderyn-report.md +``` + +Use `lib` rather than a list of dependency names: this is a Foundry project, and a name-based filter fails open when a dependency is added whose directory is not enumerated. After any run, check `grep -c 'lib/\|node_modules/' ` is 0 before trusting the counts. diff --git a/doc/audits/tools/slither-report.md b/doc/audits/tools/v1.0.0/slither-report.md similarity index 99% rename from doc/audits/tools/slither-report.md rename to doc/audits/tools/v1.0.0/slither-report.md index 77a6c97..413ece5 100644 --- a/doc/audits/tools/slither-report.md +++ b/doc/audits/tools/v1.0.0/slither-report.md @@ -250,5 +250,4 @@ Confidence: High - [ ] ID-22 [IncomeVault.__gap](src/IncomeVault.sol#L112) is never used in [IncomeVault](src/IncomeVault.sol#L13-L113) -src/IncomeVault.sol#L112 - +src/IncomeVault.sol#L112 \ No newline at end of file diff --git a/doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS.md b/doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS.md new file mode 100644 index 0000000..3bbd45c --- /dev/null +++ b/doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS.md @@ -0,0 +1,369 @@ +# IncomeVault — Code Quality Review + +| | | +| --- | --- | +| Scope | `src/` (11 contracts, 547 code lines excluding NatSpec) | +| Commit | working tree on branch `update`, after the ERC-7201 and access-control-hook refactors | +| Compiler | Solidity 0.8.36, EVM target `prague`, optimizer 200 runs | +| Date | 2026-08-19 | +| Produced with | Claude Code | + +**This is a code-quality review, not a security audit.** Nothing in this report is a vulnerability. No finding here lets an unauthorized party move value, bypass a restriction, or brick a contract. The findings with real behavioural weight (H-1 and H-2, both since fixed) required a **privileged role** to reach, so they are compliance and consistency defects rather than exploitable ones — but they are the two worth a maintainer decision, and they are described in full. + +The contracts remain **unaudited**; this review does not change that. + +--- + +## Disposition summary + +| ID | Finding | Outcome | Where | +| --- | --- | --- | --- | +| A-1 | Batch time validation re-read one slot per element and copied `calldata` to `memory` | ✅ fixed | `IncomeVaultOpen.sol` | +| A-2 | `unchecked { ++i }` on the loop counters | ⬜ deliberately not applied | — | +| A-3 | Unbounded iteration over caller-supplied arrays | ⬜ left as is | — | +| B-1 | Repeated storage reads across an external call | ⬜ none found | — | +| C-1 | `setStatusClaim` wrote the claim switch with no event | ✅ fixed | `IncomeVaultInternal.sol` | +| C-2 | `_setERC20TokenPayment` silent while its sibling `_setSnapshotEngine` emitted | ✅ fixed | `IncomeVaultInternal.sol` | +| C-3 | `_setTimeLimitToWithdraw` silent | ✅ fixed | `IncomeVaultInternal.sol` | +| C-4 | `withdraw` / `withdrawAll` moved funds out with no event | ✅ fixed | `IncomeVaultRestricted.sol` | +| C-5 | One emit site per event, all inside a `_setX` helper | ⬜ already correct | — | +| D-1 | Context disambiguation block byte-identical in both deployments | ⬜ left as is, constraint explained | — | +| E-1 | Five `public` functions missing `virtual` against project convention | ✅ fixed + compile guard | `IncomeVaultOpen.sol` | +| F-1 | ERC-173 / Ownable2Step interface ids | ⬜ recomputed, both correct | — | +| F-2 | Neither variant advertises `IERC3643Version` via ERC-165 | ⬜ decide | — | +| G-1 | `VERSION = "1.1.0"` contradicts the CHANGELOG heading and the project's own MAJOR rule | ⚠️ **outstanding** | — | +| G-2 | CHANGELOG release checklist points at two directories that do not exist | ✅ fixed | `CHANGELOG.md` | +| G-3 | NatSpec block length | ⬜ measured, healthy, no action | — | +| G-4 | One production comment referencing a `.md` file | ⬜ left as is, reasoning below | — | +| H-1 | `distributeDividend` ignored the claim window, so it could pay on **live** balances | ✅ fixed | `IncomeVaultRestricted.sol`, `IncomeVaultInternal.sol` | +| H-2 | `distributeDividend` bypassed pause / freeze / RuleEngine | ✅ fixed | `IncomeVaultRestricted.sol` | +| H-3 | The vault checks whether it has frozen *itself* | ⬜ keep, reasoning below | — | +| H-4 | `withdrawAll` leaves the per-time accounting stale | ⬜ already documented | — | +| I-1 | The vault required 8 interface functions and called 3 | ✅ fixed | `interfaces/ISnapshotSource.sol` | + +Counted from the rows above: **10 fixed**, **12 deliberately left as is**, **1 needing a decision**. + +## Outstanding + +| ID | Item | Why it is still open | +| --- | --- | --- | +| G-1 | Version string vs release heading | ✅ fixed — `VERSION` bumped to `2.0.0` with its four mirrors | +| F-2 | ERC-165 for `IERC3643Version` | Cosmetic; no consumer known to filter on it | + +--- + +## A. Loops and iteration + +### A-1. Batch time validation re-read one storage slot per element — ✅ fixed + +`validateTimeBatch` looped over `validateTime(times[i])` → `validateTimeCode(times[i])`, and every iteration re-read `timeLimitToWithdraw` — the **same slot** each time — plus paid a `public` → `public` dispatch. Both batch entrypoints also declared `uint256[] memory` where `calldata` works. + +The fix keeps the public surface identical and adds two internal helpers, `_timeCode(...)` taking the storage pointer and the already-read limit, and `_revertOnInvalidTime(code)`: + +```solidity +function validateTimeBatch(uint256[] calldata times) public view virtual { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + // `_timeLimitToWithdraw` is the same slot for every element: read it once + uint256 timeLimit = $._timeLimitToWithdraw; + for(uint256 i = 0; i < times.length; ++i){ + _revertOnInvalidTime(_timeCode($, times[i], timeLimit)); + } +} +``` + +**Measured**, same harness toggled in place, warm-up call before each measurement, `gasleft()` deltas: + +| | before | after | delta | +| --- | --- | --- | --- | +| `validateTimeBatch(1)` | 15,626 | 15,364 | −262 (−1.7%) | +| `validateTimeBatch(8)` | 35,654 | 33,705 | **−1,949 (−5.5%)** | +| marginal cost per element | 2,861 | 2,620 | −241 | + +**This is why the rule is measure, not estimate.** The prediction from opcode arithmetic was ~100 gas per element (one warm `SLOAD`). The real figure is 241 — 2.4× larger — because the refactor also removes an internal dispatch per element and the `calldata` → `memory` copy. Reporting the predicted figure would have understated the win by more than half. + +Guarded by `testValidateTimeBatchStillRejectsEachCode` and `testValidateTimeBatchMatchesValidateTime`, which pin all three error codes and the agreement between the batch and single paths. + +### A-2. `unchecked { ++i }` — ⬜ deliberately not applied + +All four loops already use `++i`. Adding `unchecked` would buy **nothing**: this project compiles with **0.8.36**, and since **0.8.22** the compiler elides the overflow check on a loop counter it can prove bounded. Recommending it here would be cargo-cult. Recorded so the next reviewer does not raise it. + +### A-3. Unbounded iteration over caller input — ⬜ left as is + +`claimDividendBatch(times)` and `distributeDividend(addresses)` both iterate a caller-supplied array with no cap. For `claimDividendBatch` the caller pays their own gas and the only victim of an oversized array is the caller. For `distributeDividend` the caller holds `INCOME_VAULT_DISTRIBUTE_ROLE`. Neither is a griefing vector against a third party. A cap would add a configuration knob and a failure mode for no benefit. + +## B. Storage reads + +### B-1. Reads across an external call — ⬜ none found + +Checked every path that crosses an external call (`snapshotInfo`, the RuleEngine view call, `safeTransfer`/`safeTransferFrom`) for a slot read on both sides. There are none. Note the ERC-7201 `$` pointer is *not* a storage read — `_getIncomeVaultInternalStorage()` only assigns a compile-time constant slot — so the repeated `$` loads across the codebase cost nothing and must not be "optimised". The one genuine repeated read was A-1, and it is inside a loop rather than across a call. + +No hand-caching is recommended anywhere else: the optimizer already forwards a stored value to a later load when nothing intervenes, so adding a local there would be a pessimisation. + +## C. Events + +Four writes changed state with no event. The claim switch (C-1) is the significant one: `setStatusClaim` is the operational control that opens a distribution, and an indexer had no way to observe it. + +### C-1 to C-4 — ✅ fixed + +| Write | Was | Now | +| --- | --- | --- | +| `setStatusClaim` | silent | `ClaimStatusSet(time, status)` | +| `_setERC20TokenPayment` | silent | `ERC20TokenPaymentSet(token)` | +| `_setTimeLimitToWithdraw` | silent | `TimeLimitToWithdrawSet(delay)` | +| `withdraw` | silent | `Withdraw(time, withdrawAddress, amount)` | +| `withdrawAll` | silent | `WithdrawAll(withdrawAddress, amount)` | + +C-2 is the sibling-inconsistency case worth naming: `_setSnapshotEngine` and `_setERC20TokenPayment` sit on adjacent lines of the same initializer, set the two external dependencies of the vault, and only the first emitted. + +Every one of these is written through an internal `_setX` helper that owns the write **and** the event, rather than an inline `emit` at the call site. The consequence is that they now also fire during `initialize`, so a vault configured once at deployment has a complete on-chain trail — `testInitializeEmitsBothEngineEvents` asserts exactly that. + +### C-5. Emit sites per event — ⬜ already correct + +``` +newDeposit 1 DividendClaimed 1 SnapshotEngineSet 1 ERC20TokenPaymentSet 1 +ClaimStatusSet 1 TimeLimitToWithdrawSet 1 Withdraw 1 WithdrawAll 1 +``` + +One emit site each, and each inside the helper that performs the write. "Every write emits" is held **structurally**, not by convention, so a future write path cannot silently skip the event. No action. + +## D. Duplication + +### D-1. Identical Context disambiguation in both deployments — ⬜ left as is + +`IncomeVault` and `IncomeVaultOwnable2Step` contain a **byte-identical** 24-line block overriding `_msgSender`, `_msgData` and `_contextSuffixLength` (verified with `diff`; ~12 code lines each). + +**The reason it is not shared, which makes extraction non-actionable:** the ambiguity only exists in the concrete contract. Each deployment inherits `ContextUpgradeable` twice — once through `IncomeVaultBase` and once through its access-control base (`AccessControlModule` or `Ownable2StepUpgradeable`) — and Solidity requires the override in the contract where the C3 linearization is ambiguous. A shared parent cannot resolve a diamond that does not exist until its own child adds the second path. Moving the block up would not compile. + +## E. `virtual` convention + +### E-1. Five `public` functions were not overridable — ✅ fixed + +`CLAUDE.md` requires hooks to be `internal view virtual` and the codebase marks essentially every public function `virtual`. `IncomeVaultOpen` was the exception — `claimDividend`, `claimDividendBatch`, `validateTimeCode`, `validateTime` and `validateTimeBatch` all lacked it, while the sibling `IncomeVaultRestricted` marked all six of its public functions `virtual`. **The inconsistency is the evidence**: whichever is right, the two files disagreeing is a defect. + +Consequence: a deployment variant could not specialise the claim entrypoint — the single most likely place to want it (an extra restriction, a different reentrancy strategy). + +Guarded by `test/mocks/IncomeVaultOverrideMock.sol`, which overrides `claimDividend` and `validateTimeCode`. **Verified the guard guards:** removing `virtual` from `claimDividend` fails the build with `Error (4334): Trying to override non-virtual function`. The mock also increments a counter and calls `super`, so a silently shadowed override would be caught rather than merely compiling. + +`virtual` on an internal function is resolved statically and is free; the A-1 measurements were taken with the new `virtual` helpers in place and still show a net saving, which is the practical confirmation. + +## F. ERC / specification conformance + +### F-1. Interface identifiers — ⬜ recomputed, both correct + +`Ownable2StepERC165Module` hardcodes two ids. Both were recomputed from the selectors rather than trusted: + +| Constant | Declared | Recomputed | | +| --- | --- | --- | --- | +| `IERC173_INTERFACE_ID` | `0x7f5828d0` | `owner()` ^ `transferOwnership(address)` = `0x7f5828d0` | ✔ | +| `IOWNABLE2STEP_INTERFACE_ID` | `0x9ab669ef` | `acceptOwnership()` ^ `pendingOwner()` = `0x9ab669ef` | ✔ | + +The `type(I).interfaceId` inheritance trap does not apply: these are literals, and the comment above each states the derivation. Hardcoding is correct here because OpenZeppelin ships no `IERC173`. + +### F-2. `IERC3643Version` is not advertised — ⬜ decide + +Both variants now implement `IERC3643Version` but neither answers `true` for its id in `supportsInterface`. Purely cosmetic — no consumer is known to filter on it, and `version()` is callable regardless. Listed rather than fixed because adding it is a public-surface change for no known consumer. + +## G. Code / documentation mismatch + +### G-1. The version string contradicts the changelog — ⚠️ outstanding + +`src/modules/VersionModule.sol` sets `VERSION = "1.1.0"`. `CHANGELOG.md`'s release heading is `## 2.0.0`, and the file's own policy section states: + +> MAJOR version when the new version makes: incompatible proxy **storage** change […] a significant change in external APIs (public/external functions) or in the internal architecture + +This release does all three — the ERC-7201 migration is an incompatible storage change, `initialize` changed signature, and the architecture was split into a base plus two deployment variants. By the project's own written rule the next version is MAJOR. + +`1.1.0` was set on explicit instruction, so it is left in place; the conflict is recorded here and in the changelog entry rather than silently resolved. One line settles it either way: change `VERSION` to `"2.0.0"` (and `EXPECTED_VERSION` in `test/VersionModule.t.sol`), or change the heading to `## 1.1.0`. + +### G-2. The release checklist points at directories that do not exist — ✅ fixed + +The checklist named `./doc/coverage` and `./doc/security/audits/tools`. The actual paths are `doc/test/coverage` and `doc/audits/tools`. Anyone following the checklist would have written output into new empty directories beside the real ones. + +### G-3. NatSpec block length — ⬜ measured, healthy + +| blocks | median | p90 | max | +| --- | --- | --- | --- | +| 78 | 4 lines | 9 lines | 15 lines | + +Exactly one block reaches 15 lines (the `IncomeVaultValidationModule` header, which carries a genuine design constraint: why the vault uses the RuleEngine view path). There is no long tail and no header that has become a document. **No action** — reported because "the comments are fine" is only credible with the distribution attached. + +### G-4. Production comment referencing a `.md` file — ⬜ left as is + +One occurrence: `VersionModule.sol` says to bump `VERSION` together with the `CHANGELOG.md` entry. It is cited by **bare filename** at the repository root, it is a maintenance instruction rather than a warning whose substance lives elsewhere, and a reader on a block explorer loses nothing by not having the file. Removing it would delete the one thing that ties the constant to the release process. Keep. + +## H. Weird behaviour — correct but at odds with the purpose + +Both findings here concern `distributeDividend`, the issuer-driven push path. Both are reachable only with `INCOME_VAULT_DISTRIBUTE_ROLE` (or the owner, in the single-owner variant), so neither is exploitable by an outsider — but both make the push path behave differently from the pull path in ways that undercut the vault's stated purpose. **Both have since been fixed.** + +### H-1. `distributeDividend` ignored the claim window — ✅ fixed + +`claimDividend` calls `validateTime(time)`, which rejects a claim that is too early or too late. `distributeDividend` checked **only** `segregatedClaim[time]`: + +```solidity +if(!$._segregatedClaim[time]){ revert IncomeVault_ClaimNotActivated(); } +(uint256[] memory tokenHolderBalance, uint256 totalSupply) = $._snapshotEngine.snapshotInfoBatch(time, addresses); +``` + +**Why the missing "too early" check mattered, verified against the upstream source.** `SnapshotBase._snapshotBalanceOf` is: + +```solidity +return snapshotted ? value : ownerBalance; +``` + +When no snapshot value exists at `time` it falls back to the **live** balance. So a distribution before `time` computed every payout from current balances instead of the recorded ones, and set `claimedDividend[holder][time] = true`, permanently consuming the holder's claim for that period at the wrong amount. + +**Fix.** `distributeDividend` now applies the same three checks as the pull path: + +```solidity +// Same window as a holder-driven claim: the claims must be open, `time` must have passed so the +// snapshot is recorded, and the withdraw limit must not have expired. +_revertOnInvalidTime(_timeCode($, time, $._timeLimitToWithdraw)); +``` + +> **⚠️ Correction to this report.** The original text said *"the fix is one line — call `validateTime(time)` instead of the bare `segregatedClaim` check"*. **That was wrong.** `validateTime` is declared in `IncomeVaultOpen`, which is a **sibling** of `IncomeVaultRestricted` (both inherit `IncomeVaultValidationModule` and `IncomeVaultInternal`; neither inherits the other), so `distributeDividend` cannot call it. The real fix moved the enum `TIME_ERROR_CODE` and the internal helpers `_timeCode` / `_revertOnInvalidTime` down into the shared parent `IncomeVaultInternal`, where both paths can reach them. The public surface of `IncomeVaultOpen` is unchanged — `validateTime`, `validateTimeCode` and `validateTimeBatch` stay exactly where they were — and no logic was duplicated, which inlining the three checks into `IncomeVaultRestricted` would have done. + +**Tests, written before the fix and confirmed to fail against the old code** (`next call did not revert as expected`, 3 failures): + +| Test | Asserts | +| --- | --- | +| `testCannotDistributeBeforeTheDividendTime` | reverts `TooEarlyToWithdraw`; nothing paid, claim still available | +| `testCannotDistributeAfterTheWithdrawLimit` | reverts `TooLateToWithdraw` | +| `testCannotDistributeWhenTheClaimIsNotActivated` | still reverts `ClaimNotActivated` | +| `testDistributeInsideTheWindowStillWorks` | inside the window the payout is unchanged | +| `testPushAndPullAgreeOnTheWindow` | push and pull reject with the **same** error at the same instant | + +The "too late" bound came along with the fix. That is coherent: after the withdraw limit the funds are meant to return to the issuer through `withdraw`, so a push at that point would contradict the limit. An issuer who wants to pay later extends `timeLimitToWithdraw` rather than bypassing it. + +### H-2. `distributeDividend` bypassed the ValidationModule — ✅ fixed + +`claimDividend` and `claimDividendBatch` both called `_validateTransfer(...)` — pause, address freeze, and the RuleEngine. `distributeDividend` called `_transferDividend` directly and performed none of them. + +The vault failed **closed** on the pull path and **open** on the push path. A holder who was frozen, or whom the RuleEngine's allow-list rejected, could not claim — but could be paid. Pausing the vault did not stop a distribution either. Since the RuleEngine integration exists precisely to enforce transfer compliance on payouts, a privileged path that skipped it removed the property the module is there to provide. + +**Fix.** The same check the claim paths use, inside the loop, before the transfer: + +```solidity +// Same transfer restriction as a holder-driven claim: pause, freeze and RuleEngine. +_validateTransfer(address(this), addresses[i], tokenHolderDividend[i]); +_transferDividend(time, addresses[i], tokenHolderDividend[i]); +``` + +**The design decision worth recording: one blocked holder reverts the whole distribution**, rather than being skipped. Skipping was the alternative and it is worse — `distributeDividend` would return successfully having quietly not paid part of the list, and the operator would have no signal. Reverting matches `claimDividendBatch`, and `IncomeVault_InvalidTransfer(from, to, value)` names the offending address so it can be removed from the list and the call retried. The cost is that a single non-compliant address in a large batch blocks the batch, which is the correct trade for a compliance control. + +**Tests, written before the fix and confirmed to fail against the old code** (four failures, all `next call did not revert as expected`): + +| Test | Asserts | +| --- | --- | +| `testCannotDistributeToAFrozenHolder` | reverts; nothing paid, claim not consumed | +| `testCannotDistributeWhilePaused` | pause now stops the push path | +| `testOneBlockedHolderRevertsTheWholeDistribution` | the allowed holder is rolled back too, error names the blocked one | +| `testDistributeStillWorksWhenEveryHolderIsAllowed` | the normal payout is unchanged | +| `testCannotDistributeToANonWhitelistedHolder` (RuleEngine suite) | the compliance case that motivated the finding | +| `testCanDistributeWhenBothAddressesWhitelisted` | still works once whitelisted | + +Note the pause and freeze slots are now read once per holder rather than once per call. That is consistent with `claimDividendBatch`, which does the same, and was not optimised: correctness of the control comes first, and a per-holder read is what makes a mid-batch state change impossible to miss. + +### H-3. The vault checks whether it has frozen itself — ⬜ keep, with a correction + +`_validateTransfer(address(this), holder, amount)` reaches, inside `canTransfer`: + +```solidity +if(EnforcementModule.isFrozen(from) || EnforcementModule.isFrozen(to)){ + return false; +} +``` + +`from` is **always the vault** — every call site passes `address(this)` — so the `from` half asks whether the vault has frozen *itself*. `EnforcementModule.setAddressFrozen` (inherited from CMTAT) accepts any address with no exclusion for `address(this)`, so `ENFORCER_ROLE` can freeze the vault. + +> **⚠️ Correction to this report.** The original entry said removing the check would cost "no gas worth measuring". **That was an estimate and it was wrong.** Toggled in place and measured on `claimDividend`: **70,688 gas with the check, 68,426 without — 2,262 gas, 3.2% of a claim.** It is a *cold* `SLOAD`: the vault's own frozen slot is touched nowhere else in a payout, so it never gets warmed. The verdict below is unchanged, but it is now a trade with a real price rather than a free one. + +#### What it does today, verified + +Each of these was confirmed with a temporary test, not reasoned about: + +| Behaviour | Result | +| --- | --- | +| Freeze the vault, then `claimDividend` | reverts `IncomeVault_InvalidTransfer` | +| Freeze the vault, then `distributeDividend` | reverts (since the H-2 fix, the push path checks too) | +| `paused()` while the vault is frozen | **`false`** | +| `deposit` while the vault is frozen | **succeeds** | +| `withdrawAll` while the vault is frozen | **succeeds — the vault can be fully drained** | + +#### The risk as it stands + +Three consequences follow, and the second is the one that would surprise an operator: + +1. **A second global kill-switch, held by a different role.** `ENFORCER_ROLE` — intended for freezing *holders* — can halt every payout by freezing one address, without holding `PAUSER_ROLE`. Whether that is separation of duties or an unintended capability depends on how the roles are staffed. It is not documented anywhere as a way to stop the vault. +2. **It is invisible to anything watching the pause flag.** `paused()` stays `false`, so a monitor polling the pause state sees a healthy vault while every claim reverts. And the revert is the generic `IncomeVault_InvalidTransfer(vault, holder, amount)` — **identical to the error a blocked holder gets**. A support desk cannot tell "you are on a sanction list" from "the vault itself is halted" without reading the `AddressFrozen` logs and noticing the address is the vault. This is the concrete operational cost of leaving it as is. +3. **It protects nobody's funds.** Freezing the vault stops holders being paid; it does **not** stop `deposit`, and it does **not** stop `INCOME_VAULT_WITHDRAW_ROLE` draining the contract with `withdrawAll`. It is a payout switch, not a safe mode. Anyone reaching for it as an emergency measure should know that. + +#### Why it is kept + +**The decisive argument is what removal would do, not what the check buys.** `setAddressFrozen` comes from the inherited CMTAT `EnforcementModule` and cannot be un-inherited. Drop the `from` half and freezing the vault becomes a **silent no-op that looks like it worked**: the call succeeds, an `AddressFrozen` event is emitted, `isFrozen(vault)` returns `true` — and payouts continue. An enforcer would believe they had stopped the vault. A check that is redundant is a much smaller problem than a control that reports success and does nothing. + +Two secondary reasons: + +- **Symmetry with `to`, and with CMTAT semantics.** In the CMTAT a frozen address is blocked as sender *and* receiver. Checking only `to` would make the vault's rule differ from the token's for no stated reason. +- **It composes with the RuleEngine.** `canTransfer(vault, holder, value)` is also handed to the engine with the vault as `from`, so a rule that allow-lists senders already treats the vault as a participant. Removing the local `from` check while the engine still sees `from` would leave the two layers disagreeing about whether the vault is a party to the transfer. + +#### What could be implemented instead + +| Option | Effect | Verdict | +| --- | --- | --- | +| **A. Keep, and document it** | The capability is stated in the spec so an operator knows the lever exists and what it does not cover | **recommended, and done** — `doc/README.md` now describes it | +| **B. Distinguishable error** | Revert `IncomeVault_VaultFrozen()` when `from` is the vault, instead of the generic `IncomeVault_InvalidTransfer`. Closes risk 2 at the cost of one comparison on the failure path only | **worth doing if operators will run a support desk**; not done, no runtime behaviour depends on it | +| **C. Reject freezing the vault** | Override `setAddressFrozen` to revert on `address(this)`. Removes the ambiguity entirely — but also removes the enforcer's lever, and overriding an inherited control to forbid something is a bigger statement than it looks | not recommended | +| **D. Remove the `from` check** | Saves the measured 2,262 gas per claim, and creates the silent no-op described above | **do not** | + +The 2,262 gas is the price of option A over option D. That is the trade to weigh: ~3% of a claim, paid by every holder, to keep an enforcement control honest. + +### H-4. `withdrawAll` leaves the per-time accounting stale — ⬜ already documented + +`withdrawAll` moves tokens without decrementing any `segregatedDividend[time]`, so after it the per-time buckets no longer sum to the vault balance. This is already stated in `doc/README.md` ("can lead to an 'unstable' state […] to be used only in case of emergency or if the vault is closed"). Behaviour and documentation agree; no finding. The new `WithdrawAll` event (C-4) makes it observable. + +## I. Interface granularity + +### I-1. The vault required 8 interface functions and called 3 — ✅ fixed + +`ISnapshotState` declares **8** functions. The vault calls **3**: + +| Declared | Used by the vault | +| --- | --- | +| `snapshotInfo(uint256,address)` | ✔ `claimDividend` | +| `snapshotInfoBatch(uint256[],address[])` | ✔ `claimDividendBatch` | +| `snapshotInfoBatch(uint256,address[])` | ✔ `distributeDividend` | +| `snapshotExists`, `snapshotBalanceOf`, `snapshotBalanceOfExact`, `snapshotTotalSupply`, `snapshotTotalSupplyExact` | ✘ | + +**Fix.** `src/interfaces/ISnapshotSource.sol` declares exactly the three, with signatures copied verbatim from `ISnapshotState`, and the vault is typed against it throughout. + +**Be precise about what moved**, because "nothing changed" would be sloppy: + +| | | +| --- | --- | +| Storage layout | **identical** — both variants, verified from `--extra-output storageLayout` (`[]`, all state is ERC-7201) | +| ABI | **identical** — 194 entries for `IncomeVault`, 184 for `IncomeVaultOwnable2Step`, diffed before/after | +| `SnapshotEngineSet` topic | **unchanged** — the event is `SnapshotEngineSet(address)` either way | +| Solidity types | **changed** — `initialize`, `snapshotEngine()` and `_setSnapshotEngine` now name `ISnapshotSource` | +| Callers | must cast: `ISnapshotSource(address(engine))`, since Solidity has no implicit conversion between unrelated interfaces | + +**No ERC-165 guard was added, deliberately.** This is the step most often got wrong: every existing implementer would have to advertise the new id, and the canonical `SnapshotEngine` declares no `supportsInterface` of its own — a guard would reject the implementation the vault is built for, which is a self-inflicted outage rather than a fix. The report's own warning applied to the report's own recommendation. + +**And the limit, restated so the change is not read as more than it is.** ERC-165 expresses shape, never semantics, and this change adds no runtime check at all. A snapshot source returning attacker-chosen balances satisfies `ISnapshotSource` exactly as an honest one does. What was bought is documentation and least-privilege value: a third-party provider now writes three functions instead of five stubs it will never see called. Trusting the source remains configuration discipline. + +**Tests.** + +| Test | Asserts | +| --- | --- | +| `testAThreeFunctionSourceIsAccepted` | `MinimalSnapshotSourceMock` implements *only* the three; the vault initializes against it and a claim pays 100/400 of the deposit | +| `testTheRealSnapshotEngineStillSatisfiesIt` | the real `ISnapshotState` engine still works through the narrower type — the compatibility half | + +The mock is the evidence: it compiles and the vault works against it, so the other five were never required. + +--- + +## Notes on this review + +- Every gas figure was measured with a temporary harness (own contract, warm-up call, `gasleft()` deltas, same harness toggled in place), which has been **deleted**. Test count went 79 → 85: the six additions are the `CodeQuality.t.sol` regressions plus the override mock, and no benchmark is left behind. +- Each event fix was validated by removing the `emit` and confirming the matching test fails (`ClaimStatusSet`, `ERC20TokenPaymentSet` and `Withdraw` were each sabotaged in turn; each failed only its own test). The `virtual` fix was validated by removing the keyword and confirming the build breaks. +- The style checker reports two `[missing-param]` / `[missing-return]` hits for the parameter named `$`. Both are **false positives** in the checker, which parses return variables with a pattern including `$` but extracts tag names with `(\w+)`, which excludes it. `$` is the OpenZeppelin/CMTAT convention for the ERC-7201 accessor and is kept. +- What was reasoned about rather than executed: nothing load-bearing. H-1's fallback behaviour was confirmed by reading `SnapshotBase._snapshotBalanceOf` in the vendored dependency **and** by the characterisation test; D-1's "cannot be hoisted" claim follows from C3 linearization and was not attempted as a build. diff --git a/doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS_SECOND.md b/doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS_SECOND.md new file mode 100644 index 0000000..be64619 --- /dev/null +++ b/doc/audits/tools/v2.0.0/CLAUDE_ANALYSIS_SECOND.md @@ -0,0 +1,343 @@ +# IncomeVault — Code Quality Review (second pass) + +Produced with Claude Code. Scope: `src/` at commit `265bac1`, solc 0.8.36, EVM `prague`, 21 files / 982 nSLOC. Tests, mocks and `script/` are out of scope. + +**This is a code-quality review, not a security audit. Nothing here is a vulnerability.** No finding lets an unauthorized party move value, bypass a restriction or brick a contract. The two static analyzers were run separately and independently report nothing to fix — see [`slither-report.md`](./slither-report.md) and [`aderyn-report.md`](./aderyn-report.md). + +This is the **second** pass. The first is [`CLAUDE_ANALYSIS.md`](./CLAUDE_ANALYSIS.md) in this directory; its findings are not repeated here, and its two outstanding items (G-1, F-2) remain outstanding. Everything below is either new code since that review, or something the first pass missed. + +## Disposition summary + +| ID | Finding | Outcome | +| --- | --- | --- | +| A-1 | Loops and iteration | ⬜ nothing to change — verified, see below | +| B-1 | `_transferDividend` re-reads `_paidDividend[time]` — **209 gas** measured | ✅ **fixed**, in a better shape than proposed — **167 gas** | +| C-1 | The deposit write, its zero-check and its event are duplicated across two paths | ✅ **fixed** | +| E-1 | Three core internal functions are not `virtual` while five siblings in the same file are | ✅ **fixed** | +| G-1 | Three production comments point at `doc/` paths that have already moved twice | ✅ **fixed**, and the rule was tightened past what this finding proposed | +| G-2 | NatSpec block length | ⬜ **no change needed** — measured; corrects a worry raised during development | +| H-1 | `detectTransferRestriction` answers 0 for a payout that `canTransfer` rejects | ✅ **fixed** | +| I-1 | The vault demands `IRuleEngine`, whose `transferred` it must never call | ⬜ **keep as is** — the reason is recorded so it is not re-opened | +| J-1 | Modularity | ⬜ already covered exhaustively elsewhere; not duplicated here | + +Nine rows: two are explicit "do not change" verdicts, one is "nothing found", six were recommendations. **All six recommendations — B-1, C-1, E-1, G-1, H-1 and the carried-forward version bump — have since been implemented.** What remains are the two deliberate "keep as is" verdicts (I-1, G-2), the "nothing found" row (A-1), and the modularity pointer (J-1). + +## Outstanding, carried from the first pass + +| ID | Item | Why it is still open | +| --- | --- | --- | +| F-2 (first pass) | `IERC3643Version` not advertised through ERC-165 | Cosmetic; no consumer known to filter on it | + +The first pass's own G-1 — `VERSION = "1.1.0"` against a `## 2.0.0` changelog heading — is **closed**. `VERSION` is now `2.0.0`, with its four mirrors updated and a sabotage confirming a lone bump fails three suites. The audit directory was renamed `v1.1.0` to `v2.0.0` to match, which was a correction rather than a follow-on: the changelog has only `2.0.0` and `1.0.0` headings, so no `1.1.0` release ever existed. + +--- + +## A. Loops and iteration + +### A-1. Nothing to change — and specifically not `unchecked` + +Checked and found clean: no `i++` anywhere (`grep -rn 'i++' src/` returns nothing), every loop uses `++i`, and every external array parameter is `calldata`. + +There are **no `unchecked` blocks in `src/`, and none should be added.** Since Solidity 0.8.22 the compiler elides the overflow check on a bounded loop counter, so `unchecked { ++i }` on `for (uint256 i = 0; i < n; ++i)` buys nothing at 0.8.36. The first pass recorded this as A-2, "deliberately not applied"; re-confirming it here so the next reviewer does not propose it a third time. + +The two `address[] memory` parameters (`IncomeVaultSnapshotCore._snapshotInfoBatch`, and its implementation) are **internal hooks, not external entry points**, so `calldata` is not available: the caller in `IncomeVaultOpen` builds a one-element array in memory. This is documented at the declaration. + +**Verdict: leave.** Unbounded iteration over caller-supplied arrays remains by design (first pass, A-3) — the caller chooses the batch size and pays for it. + +## B. Storage reads + +### B-1. `_transferDividend` reads `_paidDividend[time]` twice — 209 gas, measured + +`IncomeVaultInternal._transferDividend`: + +```solidity +if (tokenHolderDividend > unclaimedDividend(time)) { // reads _segregatedDividend + _paidDividend + revert IncomeVault_NotEnoughAmount(); +} +$._paidDividend[time] += tokenHolderDividend; // reads _paidDividend AGAIN, then writes +``` + +`unclaimedDividend` is a **public** view called internally, and the optimizer does not forward its `_paidDividend` load into the `+=` on the next line. This is the read-modify-write shape the guidance says reliably pays off, as opposed to two plain adjacent loads where hand-caching is a pessimisation. + +**Measured, not estimated.** Baseline `claimDividend`, toggled in place on the real contract and re-run against the same test (`testHolderCanClaimWithDepositAndOneHolder`, `forge test --gas-report`): + +| Variant | `claimDividend` gas | Delta | +| --- | --- | --- | +| Current | 118,924 | — | +| Cache `paid` inline, duplicating the saturating expression | 118,715 | **−209** | +| Shared `_unclaimedDividend($, time)` helper returning `(unclaimed, paid)` | 118,782 | **−142** | + +**This came out higher than predicted.** I expected roughly one warm SLOAD (~100 gas); the measurement says 209. The extra is the second `_getIncomeVaultInternalStorage()` and the call frame of the public view, neither of which the optimizer collapsed. Recording the discrepancy because it is the reason to measure rather than reason from opcode costs. + +**Recommended: the helper, not the inline version**, giving up 67 gas to keep one source of truth. The saturating rule (`segregated > paid ? segregated - paid : 0`) exists for a documented reason — a mid-window sweep can push `paid` above `segregated` — and duplicating it means the next person to change it must find both copies. + +The shape already has house precedent: `_timeCode(IncomeVaultInternalStorage storage $, ...)` takes the storage pointer for exactly this reason, so a batch reads the limit once instead of once per element. + +```solidity +function _unclaimedDividend(IncomeVaultInternalStorage storage $, uint256 time) + internal view virtual returns (uint256 unclaimed, uint256 paid) +{ + uint256 segregated = $._segregatedDividend[time]; + paid = $._paidDividend[time]; + unclaimed = segregated > paid ? segregated - paid : 0; +} +``` + +`unclaimedDividend(time)` then returns the first value, `_transferDividend` uses both. **Verified: implemented, all 214 tests pass, then reverted** — this pass reports rather than applies. + +**Verdict: implement the helper variant**, on the hottest path in the contract. 142 gas on every single claim. + +## C. Events + +### C-1. The deposit write, its validation and its event are duplicated across two paths + +`grep -rn 'emit newDeposit' src/` returns **two** sites, and each carries its own copy of the zero-check and the storage write: + +| | `deposit` (L73-81) | `depositBatch` (L100-116) | +| --- | --- | --- | +| zero-amount check | `if (amount == 0) revert` | `if (amounts[i] == 0) revert` | +| storage write | `$._segregatedDividend[time] += amount` | `$._segregatedDividend[times[i]] += amounts[i]` | +| event | `emit newDeposit(...)` | `emit newDeposit(...)` | + +The invariant *"every deposit validates the amount, writes the period, and emits"* is held **by convention**, not structurally. A third funding path — and this codebase has grown one already — has nothing forcing it to do all three. + +This is the mirror image of the first pass's C-1/C-2/C-3, which fixed silent writes by routing them through `_setX` helpers. `depositBatch` was added *after* that review, and reintroduced the pattern those findings removed. The first pass's C-5 recorded "one emit site per event, all inside a helper" as already correct; that is no longer true. + +**Proposed:** + +```solidity +function _deposit(IncomeVaultInternalStorage storage $, address sender, uint256 time, uint256 amount) + internal virtual +{ + if (amount == 0) { + revert IncomeVault_NoAmountSend(); + } + $._segregatedDividend[time] += amount; + emit newDeposit(time, sender, amount); +} +``` + +**The validation moving into the helper is the part that changes behaviour**, and it is a feature: the zero-check then guards every path that can write a period, including any added later. + +**The single ERC-20 transfer must stay outside the helper.** `depositBatch` deliberately does one `safeTransferFrom` for the whole batch — that is the documented reason it exists, and folding the transfer into `_deposit` would undo it. The helper owns validate-write-emit and nothing else. + +**Verdict: implement.** ✅ **Done.** `_deposit($, sender, time, amount)` lives in `IncomeVaultInternal` beside the other state-writing internals, and takes the storage pointer as {_timeCode} does so a batch acquires it once. `grep -rn 'emit newDeposit' src/` now returns **one** site, and one `+=` writer of `_segregatedDividend`. + +**In the event, the widened validation turned out to change nothing**, because both existing paths already carried the zero-check. The report presented it as the behaviour-changing part; it is not, and the value is entirely structural — a third funding path now inherits all three behaviours instead of having to remember them. + +**Both sabotages fail tests on *both* paths, which is the property being bought:** + +| Sabotage | Failures | +| --- | --- | +| drop `emit newDeposit` from the helper | `testDepositRoleCanPerformDeposit` **and** `testDepositBatchPullsTheTokenOnceAndEventsEachEntry` | +| drop the zero-amount check | `testCannotDepositZeroAmount` **and** `testCannotDepositBatchWithAZeroAmount` | + +One change to the shared helper breaks the single path and the batch path together — before, each had its own copy and its own tests, and a change to one would have left the other silently intact. + +The batch path measured **136,263** gas in-call afterwards against 136,546 before, so the extraction is 283 gas cheaper rather than a cost. `doc/README.md` and `CHANGELOG.md` are updated to the new figure. + +## D. Duplication + +Nothing beyond C-1, which is the only new instance. The first pass's D-1 — the context-disambiguation block repeated in both deployment contracts — is unchanged and still correctly left alone: `override(IncomeVaultBaseERC2771, ContextUpgradeable)` names contract types that differ per variant, so the block cannot be hoisted. + +## E. `virtual` convention + +### E-1. Three core internal functions are not `virtual` while five siblings in the same file are + +Within `IncomeVaultInternal`: + +| `virtual` | not `virtual` | +| --- | --- | +| `_setERC20TokenPayment`, `_setTimeLimitToWithdraw`, `_setStatusClaim`, `_revertOnInvalidTime`, `_timeCode` | **`_transferDividend`, `_computeDividend`, `_computeDividendBatch`** | + +**The inconsistency inside one file is the evidence** — whichever is right, five-against-three in the same contract is a defect. This is the same argument the first pass used for E-1 on `IncomeVaultOpen`, and the same conclusion. + +Prioritised by consequence, `_transferDividend` is the one that matters. It is the payout routine, and `CLAUDE.md` states that the way to extend it is explicitly **not** to add another public entry point: + +> `transferDividendSelf` is a self-call helper with no role check … never add another public entry point to `_transferDividend`. + +If the sanctioned extension route is not a new entry point, it is an override — and the function cannot be overridden. A variant wanting a withholding deduction, a payout fee or a different transfer strategy has nowhere to put it. + +The four `_getXStorage` accessors are **also** non-virtual, and that is correct: all four are consistent, and OpenZeppelin declares the equivalent `private`. Not a finding. + +**`virtual` on an internal function is free — measured, not asserted.** Two single-function contracts, identical bodies, identical warm-up, `gasleft()` deltas: + +| Variant | gas | +| --- | --- | +| `internal` | 23,032 | +| `internal virtual` | 23,021 | + +An 11-gas difference *favouring* virtual, i.e. layout noise. Internal calls are resolved statically, so there is no dispatch to pay for. + +**Verdict: add `virtual` to all three.** ✅ **Done.** All nine internal functions in the file are now `virtual` except `_getIncomeVaultInternalStorage`, which stays non-virtual with its three sibling accessors — OpenZeppelin declares the equivalent `private`, and a slot accessor is not an extension point. + +`IncomeVaultOverrideMock` now overrides all three, and **`test/OverrideMock.t.sol` drives a real deposit-and-claim through it**, which the mock previously lacked: it was compile-only, while its own NatSpec claimed `claimCount` proved the override was reached. Nothing called it, so that claim was false. It is true now. + +**Both failure modes verified, because they are different:** + +| Sabotage | Caught by | Result | +| --- | --- | --- | +| remove `virtual` from `_transferDividend` | the compiler | `Error (4334): Trying to override non-virtual function` | +| override present but not observably reached | the counter | `internal _transferDividend override was not reached: 0 != 1` | + +The second is the one a compile-only guard misses, and it is why the counters exist rather than a bare override. + +**An honest limit of the technique**, now stated in the mock: a `view` override cannot increment a counter, so `_computeDividend` and `_computeDividendBatch` stay compile-guarded only. Their `virtual` is pinned by `Error (4334)`, not by an assertion that they ran. + +## F. ERC / specification conformance + +No new conformance finding. The interface ids are unchanged and still asserted by tests; `IIncomeVault` was added since the first pass and is advertised by both deployment variants, while ERC-7540's operator id remains deliberately unadvertised with a test pinning that. First-pass F-2 stays outstanding. + +The behaviour worth raising is a semantics question rather than an id question, so it is under H. + +## G. Code / documentation mismatch + +### G-1. Three production comments point at documentation paths that have already moved + +``` +src/interfaces/IERC7540Operator.sol:10 "Comparison with ERC-4626 / ERC-7540 vaults" in `doc/README.md` +src/interfaces/IIncomeVault.sol:22 See the capability table in `doc/README.md`. +src/modules/VersionModule.sol:13 Bump `VERSION` together with the `CHANGELOG.md` +``` + +**The evidence this matters is in this repository's own recent history**: documentation has been reorganised twice — a new `doc/cmtat-standard/` directory, and the audit reports moved from `doc/audits/` into `doc/audits/tools/vX.Y.Z/`. A path baked into a contract is a stale link waiting to happen, and unlike a link in a markdown file it **cannot be fixed after deployment**: the comment lives in the verified source forever. + +The second problem is the one that bites. Someone reading the verified source on a block explorer has the contract and nothing else. A pointer to `doc/README.md` is a reference they cannot follow. + +**The fix is not to delete the sentences.** Both `doc/README.md` pointers trail a complete thought — `IIncomeVault:22` already says *"chosen by the deployment contract, not by this interface"* before pointing at the table — so the pointer comes out cleanly and the substance stays. + +**`VersionModule.sol:13` is different and should be left alone.** It cites `CHANGELOG.md` by **bare filename**, which survives any reorganisation, and the instruction it gives ("bump these together") is actionable without opening the file. + +**Verdict: drop the two `doc/README.md` path pointers, keep the sentences, keep the `CHANGELOG.md` reference.** ✅ **Done — and then taken further, on the maintainer's instruction.** + +### What was actually applied + +The rule adopted is stricter than this finding proposed: **`CHANGELOG.md` is the only file reference permitted in a contract comment.** `src/` now contains exactly one, in `VersionModule`. + +Nine references were removed, in four groups. This finding had spotted three of them and had explicitly *exempted* two of the groups, so the correction is worth recording rather than glossing: + +| Group | Count | This finding said | What was done | +| --- | --- | --- | --- | +| `doc/README.md` path pointers | 2 | remove | removed | +| `CHANGELOG.md` | 1 | keep | kept | +| Audit findings cited by id (`finding H-1 of CLAUDE_ANALYSIS_SECOND.md`) | 2 | **exempt** — an immutable record, and the bare filename survives a move | **removed** | +| Test-file pointers (`asserted in test/Operator.t.sol`, four ERC-7201 slot comments) | 6 | not raised at all | **removed** | + +**The exemption I argued for does not survive contact with the actual reader.** A finding id means nothing to someone reading verified source on a block explorer, and it was ambiguous even internally — both this file and `CLAUDE_ANALYSIS.md` have an `H-1`. The same applies to a test pointer: *"asserted in `test/Operator.t.sol`"* tells a reader that a guarantee exists somewhere they cannot look. + +In each case the pointer was replaced by the thing it was standing in for: + +| Was | Now | +| --- | --- | +| "finding H-1 of `CLAUDE_ANALYSIS_SECOND.md`" | "consulting only the RuleEngine here would report a paused vault or a frozen holder as unrestricted, and the claim would then revert" | +| "asserted in `test/Operator.t.sol`" | "change either one and the id no longer matches what ERC-7540 assigns" | +| "The derivation is re-checked in `test/IncomeVaultStorage.t.sol`" | "Recompute it with `SlotDerivation.erc7201Slot()` before trusting a change to it" | +| "Finding C-1 of `CLAUDE_ANALYSIS_SECOND.md`" | "Each path carrying its own copy is what lets them diverge, so a new funding path must call this rather than repeat it" | + +Every replacement is actionable without leaving the file, which is the property the original finding was reaching for and only half-applied. `CLAUDE.md`/`AGENTS.md` carry the tightened rule, including the instruction not to cite the test that asserts a property — give the property and the consequence of breaking it. + +### G-2. NatSpec block length — no change needed + +Raised during development as a worry that the contracts were accumulating essay-length comment blocks. **Measured across `src/`, the worry is unfounded:** + +| | | +| --- | --- | +| blocks | 164 | +| median | **5 lines** | +| p90 | 13 lines | +| max | 26 lines | +| blocks ≥ 20 lines | **3** | + +The three outliers are `distributeDividendBestEffort` (26), and the `ISnapshotSource` (21) and `IIncomeVault` (20) interface headers — all three carrying a design constraint or a safety precondition rather than restating the code. + +For contrast, the pattern this check exists to catch looks like a median of 4 with a dozen blocks between 24 and 44. This codebase is nowhere near it. + +**Verdict: leave. Explicitly recorded so the worry is not re-raised** — it was mine, and the data does not support it. + +## H. Weird behaviour — correct but at odds with the purpose + +### H-1. `detectTransferRestriction` returns "no restriction" for a payout that will revert + +`IncomeVaultValidationModule.detectTransferRestriction` consults **only** the RuleEngine: + +```solidity +IRuleEngine ruleEngine_ = ruleEngine(); +if (address(ruleEngine_) == address(0)) { + return 0; // "no restriction" +} +return IRuleEngineERC1404(address(ruleEngine_)).detectTransferRestriction(from, to, value); +``` + +`canTransfer`, on the same contract, checks **three** things: pause, freeze of either party, then the RuleEngine. + +So for a **paused** vault, or a **frozen** holder, `detectTransferRestriction` answers `0` while `canTransfer` answers `false` and the claim reverts with `IncomeVault_InvalidTransfer`. Two views on one contract disagree about the same payout, and the one with the ERC-1404 name is the one that is wrong. + +An integrator pre-flighting a claim reaches for the function whose entire purpose is "tell me why this would fail". It tells them nothing is wrong. + +**The NatSpec already documents the gap** — *"The pause and freeze states are not reflected here, only the rules: use `canTransfer` for the complete answer"* — which is honest, and is why this is a quality finding rather than a security one. But documenting a misleading return value is weaker than not returning one. + +**The fix is well-specified, because CMTAT already defines the codes.** `REJECTED_CODE_BASE` in `draft-IERC1404.sol`: + +``` +TRANSFER_OK = 0 +TRANSFER_REJECTED_DEACTIVATED = 1 +TRANSFER_REJECTED_PAUSED = 2 +TRANSFER_REJECTED_FROM_FROZEN = 3 +TRANSFER_REJECTED_TO_FROZEN = 4 +``` + +Returning `TRANSFER_REJECTED_PAUSED` when paused and `TRANSFER_REJECTED_FROM_FROZEN` / `TO_FROZEN` when either party is frozen, before falling through to the RuleEngine, makes the two views agree and uses the ecosystem's own numbering rather than inventing any. + +**One honest limit:** the vault does **not** advertise `IERC1404` through `supportsInterface` and does not inherit it, so it is not claiming ERC-1404 conformance today. That lowers the severity — but the function carries the ERC-1404 name and signature, and no integrator reads a `supportsInterface` result before trusting a function that is right there. + +**Verdict: implement.** ✅ **Done.** + +`detectTransferRestriction` now evaluates deactivation, pause, either party frozen, then the RuleEngine — the same order and the same conditions as `canTransfer` — returning CMTAT's `REJECTED_CODE_BASE` codes. Deactivation is tested before pause because deactivating requires the pause state, so the more specific code wins. + +`messageForTransferRestriction` was fixed in the same change, and it needed it: it answered `"No restriction"` for **every** code when no RuleEngine was set, including codes that mean something. It now answers for each code the vault can issue, delegates anything else to the RuleEngine, and returns `"UnknownCode"` when there is no RuleEngine to ask. The strings are CMTAT's `ValidationModuleERC1404` verbatim (`EnforcedPause`, `AddrFromIsFrozen`, …), so an operator console written against a CMTAT reads a payout refusal exactly as it reads a transfer refusal. + +**`canTransfer` was deliberately left calling `ruleEngine_.canTransfer` rather than being rewritten as `detectTransferRestriction(...) == 0`.** The latter would make agreement structural instead of tested, which is tempting — but it changes *which* RuleEngine entry point the payout path calls on every claim. A third-party engine is free to implement the two differently, so that is a behaviour change against an external contract in exchange for tidiness. Agreement is pinned by a test instead. + +`test/TransferRestrictionCode.t.sol`, 6 tests. **Verified the guards guard:** reverting `detectTransferRestriction` to the RuleEngine-only body fails three of them with `0 != 2`, `0 != 4` and `0 != 1` — the exact defect; reverting the message half fails the other two with `No restriction != EnforcedPause`. One existing test, `EdgeCases.testErc1404ViewsWithoutARuleEngine`, was asserting the old strings and was updated: it had been pinning the defect. + +## I. Interface granularity + +### I-1. The vault requires `IRuleEngine` but calls three read-only functions — keep it anyway + +The vault calls exactly three members, all views: + +``` +IncomeVaultValidationModule.sol:100 ruleEngine_.canTransfer(...) +IncomeVaultValidationModule.sol:123 IRuleEngineERC1404(...).detectTransferRestriction(...) +IncomeVaultValidationModule.sol:138 IRuleEngineERC1404(...).messageForTransferRestriction(...) +``` + +`IRuleEngine` declares `transferred` directly and inherits the ERC-3643 compliance and ERC-1404 surfaces. So the required interface **mandates `transferred`** — which `CLAUDE.md` states the vault must never call, because it is not a bound token and the call would revert. + +That is the check-I smell in an unusually pure form: the demanded interface requires the one function whose invocation is forbidden. Read narrowly, it rejects a read-only compliance oracle, which would have to expose a `transferred` that reverts purely to satisfy the type — a stub advertising a capability the contract does not have. + +**Do not fix it, and here is why the obvious precedent does not apply.** The first pass's I-1 narrowed the snapshot dependency to `ISnapshotSource`, and it is tempting to do the same here. The two cases are not alike: + +- The snapshot source was a **project-owned** reference. The vault declared its own storage and its own getter, so narrowing the type cost nothing. +- The RuleEngine reference is **not the project's**. It lives in CMTAT's `ValidationModuleRuleEngineInternal`, at a hardcoded ERC-7201 slot, typed `IRuleEngine` by `ruleEngine()`. Narrowing means leaving that base — which would give up the property that a CMTAT host and the embedded vault logic share **one** RuleEngine, the whole substance of modularity finding M-4. + +The cost of the current design is that an implementer must satisfy a wider interface than the vault uses. The cost of narrowing it is two RuleEngines in a composed contract. The second is worse. + +**Verdict: keep. Recorded here specifically so the `ISnapshotSource` precedent is not applied to it by a future reviewer** — including me, since I proposed exactly that reasoning for the snapshot side. + +## J. Modularity + +A separate modularity review ran over this codebase and is closed. Six findings were implemented — the validation and snapshot dependencies became hooks, claim delegation and the snapshot source each got their own ERC-7201 namespace, `src/` was reorganised by what each file *is*, `IIncomeVault` was added, and ERC-2771 moved out of the base. Three more were closed as **not defects** after being checked: `version()` and `_authorizeRuleEngineManagement` collide with CMTAT's by design (one shared hardcoded storage slot, so one override is the correct answer), and splitting `IncomeVaultValidationModule` so an embedded copy reuses the host's RuleEngine is already the case for the same reason. The working document was removed once the work landed; the changelog entries carry what changed. Current state, as compile results rather than opinion: + +- `test/mocks/CMTATDividendHostMock.sol` — a `CMTATUpgradeableInternalSnapshot` embedding the distribution logic — **compiles**. Before M-1 it failed `Error (5005)`; after M-1 it still failed on a `snapshotEngine()` return-type collision, which M-2 removed. +- `test/mocks/EmbeddedDividendHostMock.sol` and `test/mocks/NoForwarderVaultMock.sol` compile, covering the non-CMTAT host and the deployment without ERC-2771. + +The one open item there is **M-3b**: SnapshotEngine vendors its own CMTAT at `v3.3.0-rc1` while this project pins `v3.3.0-rc3`, so mixing the project's CMTAT-derived modules with SnapshotEngine's CMTAT contracts fails with nine duplicate-identifier errors. It is contained only because `IncomeVaultValidationModule` is the sole CMTAT-derived module and M-1 removed it from the embeddable path. + +**Since tested, and the recorded fix was wrong.** Aligning the nested CMTAT to `v3.3.0-rc3` so both trees hold identical source leaves the same nine errors: Solidity keys a contract by its source unit, not its contents, so two paths are two contracts even byte-identical. Remapping cannot help either, because SnapshotEngine imports CMTAT by *relative* path and remappings only rewrite non-relative prefixes. There is no fix available inside this repository. Keep this project's CMTAT-derived modules and SnapshotEngine's CMTAT-derived contracts out of the same linearization — which M-1 already ensures, and which is why nothing is broken today. + +--- + +## What the static analyzers did not find + +Worth recording, because it calibrates what the tools are for. Slither (34 results) and Aderyn (10) each report **nothing to fix** on this codebase. Every finding above came from reading the code against the project's own conventions and from measuring; none of them appears in either tool's output — and neither tool found any of the substantive defects listed in `doc/audits/AUDIT_OVERVIEW.md` either. diff --git a/doc/audits/tools/v2.0.0/aderyn-report-feedback.md b/doc/audits/tools/v2.0.0/aderyn-report-feedback.md new file mode 100644 index 0000000..0c0c9b9 --- /dev/null +++ b/doc/audits/tools/v2.0.0/aderyn-report-feedback.md @@ -0,0 +1,47 @@ +# Aderyn feedback — IncomeVault v2.0.0 + +Triage of every finding in [`aderyn-report.md`](./aderyn-report.md). Each dismissal was checked against the cited `file:line`, not assumed. + +```bash +aderyn -x mocks --output doc/audits/tools/v2.0.0/aderyn-report.md +``` + +Scope: `src/` — 21 files, 982 nSLOC, 87 detectors, mocks excluded. 0 citations of `lib/` or `node_modules/`. + +> **Post-processing needed.** Aderyn writes each `Found in` link as a path containing the absolute location of the repository on the machine that ran it (`../../../../../home//.../src/...`). Those resolve for nobody else and put a local path into a committed file. They are rewritten here to repo-relative form; do the same after any future run: `sed -i 's|\.\./\.\./\.\./\.\./\.\.//|../../../../|g' aderyn-report.md` + +## Summary + +| ID | Finding | Instances | Disposition | Reason | +| --- | --- | --- | --- | --- | +| L-1 | Centralization Risk | 18 | By design | Deposit, withdraw, distribute, claim administration, pause, freeze and the two setters are issuer operations and are meant to be privileged. Who holds each is chosen at deployment and documented in the capability table of `doc/README.md`. The pattern organises this power; it does not claim to reduce it. | +| L-2 | Unspecific Solidity Pragma | 21 | By design | `^0.8.24` is required: these contracts are meant to be **inherited** by a host that pins its own version. The deployed artefact is pinned — `foundry.toml` compiles with 0.8.36. A caret pragma on a library is correct; on a final deployable it would not be. | +| L-3 | Public Function Not Used Internally | 2 | By design | Both are `initialize`. It is invoked through the proxy with `abi.encodeCall`, and `public` is the OpenZeppelin upgradeable convention for initializers. | +| L-4 | PUSH0 Opcode | 21 | Environment | `foundry.toml` sets `evm_version = "prague"`, where PUSH0 has existed since Shanghai. Relevant only if deploying to a chain that has not forked past Paris — a deployment decision, not a code defect. | +| L-5 | Modifier Invoked Only Once | 2 | Cosmetic | `onlyRuleEngineManager` and `onlySnapshotSourceManager` each guard exactly one setter. That is the authorization-hook pattern: one capability, one hook, one modifier. Inlining them would save a line and lose the symmetry that makes the capability table readable. | +| L-6 | Empty Block | 18 | By design | Every instance is an `_authorize*` override. **The empty body is the pattern** — the check rides on the modifier (`onlyRole(...)` / `onlyOwner`), and a body would be dead code. Documented in `CLAUDE.md` under the authorization-hook convention. | +| L-7 | Loop Contains `require`/`revert` | 4 | By design | In `claimDividendBatch` and `distributeDividend`, one blocked holder **must** revert the whole call, so a compliance failure cannot be silently dropped. `distributeDividendBestEffort` is the deliberate opposite and skips instead. | +| L-8 | Costly operations inside loop | 3 | Accepted | The storage writes are the per-period accounting (`_segregatedDividend`, `_paidDividend`, `_claimedDividend`). They cannot be hoisted because each iteration targets a different key. | +| L-9 | Unused Import | 4 | False positive | All four are consumed by `@inheritdoc`. Was 6 — the two genuinely unused imports have been removed; see below. | +| L-10 | State Change Without Event | 1 | Consider | `ERC7741Module.invalidateNonce` writes `$._authorizations[...] = true` and emits nothing. ERC-7741 defines no event for it, so the contract is conformant, but an indexer cannot see a nonce burned outside a signature use. Worth adding if the ABI is not yet frozen. | + +## L-9 in detail — the two real instances, now fixed + +| Location | Verdict | +| --- | --- | +| `src/deployment/IncomeVault.sol` L21 `IncomeVaultRestricted`, L22 `IncomeVaultSnapshotModule` | **False positive.** Used by `@inheritdoc IncomeVaultRestricted` / `@inheritdoc IncomeVaultSnapshotModule` on the hook overrides. Solidity requires the base to be imported **by name** for `@inheritdoc` even when it is already in scope through inheritance; removing the import fails the build with *"references inexistent contract"*. Aderyn parses code references, not NatSpec. | +| `src/deployment/IncomeVaultOwnable2Step.sol` L19, L20 | **False positive**, same reason. | +| `src/modules/Ownable2StepERC165Module.sol` L7 `IERC165` | **Was real — removed.** `IERC165` appeared on the import line and nowhere else. | +| `src/public/IncomeVaultRestricted.sol` L11 `ISnapshotSource` | **Was real — removed.** Its only other occurrence is inside a comment, which creates no compile dependency. Left over from finding M-2, when the snapshot source moved into its own module. | + +Both were deleted; `forge build` compiles and all 214 tests pass. Re-running Aderyn afterwards took L-9 from 6 instances to 4, which is the check that the fix landed and that the remaining four really are the `@inheritdoc` ones. Neither removal changes bytecode — an unused import contributes no code. + +## Delta from v1.0.0 + +v1.0.0 has no Aderyn report; this is the first. No delta is possible, and none is implied by the counts. + +## Executive triage + +**Nothing exploitable, and nothing left to fix.** The two real findings — unused `IERC165` and `ISnapshotSource` imports — have been removed, and Aderyn re-run to confirm: L-9 went from 6 instances to 4, all of them `@inheritdoc` false positives. + +One item worth a decision rather than a fix: **L-10**, an event on `invalidateNonce`. ERC-7741 does not require one, so adding it is a choice about off-chain observability, and it is easier to make before the ABI is frozen than after. diff --git a/doc/audits/tools/v2.0.0/aderyn-report.md b/doc/audits/tools/v2.0.0/aderyn-report.md new file mode 100644 index 0000000..38e04bd --- /dev/null +++ b/doc/audits/tools/v2.0.0/aderyn-report.md @@ -0,0 +1,789 @@ +# Aderyn report — IncomeVault v2.0.0 + +> The contracts are **NOT audited**. Static analysis is not an audit; these are leads, not findings. + +| | | +| --- | --- | +| Tool | Aderyn 0.6.5 | +| Date | 2026-08-20 | +| Scope | `src/` — 21 files, 87 detectors | +| Mocks | **excluded** (`-x mocks`) | + +```bash +aderyn -x mocks --output doc/audits/tools/v2.0.0/aderyn-report.md +``` + +**Scope verified:** 0 citations of `lib/` or `node_modules/`. Aderyn reads `foundry.toml` and scopes to +`src` by itself, so no path filter is needed beyond the mock exclusion. + +## Result: 0 High · 10 Low + +| ID | Finding | Instances | Assessment | +| --- | --- | --- | --- | +| L-1 | Centralization Risk | 18 | By design — issuer operations are role-gated; the capability table documents who holds what | +| L-2 | Unspecific Solidity Pragma | 21 | By design — `^0.8.24` so the logic can be embedded in a host; the deployed build pins 0.8.36 | +| L-3 | Public Function Not Used Internally | 2 | By design — `initialize` is the proxy initializer, `public` per the OpenZeppelin pattern | +| L-4 | PUSH0 Opcode | 21 | Environment — EVM target is `prague`; only relevant when deploying to a pre-Shanghai chain | +| L-5 | Modifier Invoked Only Once | 2 | Cosmetic — one modifier per capability is the authorization-hook pattern | +| L-6 | Empty Block | 18 | By design — an empty `_authorize*` override body **is** the pattern; the check rides on the modifier | +| L-7 | Loop Contains `require`/`revert` | 4 | By design — one blocked holder must revert the batch, so a compliance failure cannot be dropped | +| L-8 | Costly operations inside loop | 3 | Accepted — per-period storage writes are inherent to per-period accounting | +| L-9 | Unused Import | 4 | False positive — all four are consumed by `@inheritdoc`, which Aderyn does not parse. Was 6; the two genuinely unused imports were removed | +| L-10 | State Change Without Event | 1 | Consider — `invalidateNonce` emits nothing; ERC-7741 defines no event, but one would help indexers | + +**Nothing left to fix.** The two real findings of the first v2.0.0 run — unused `IERC165` and +`ISnapshotSource` imports — were removed, taking L-9 from 6 instances to 4. The four that remain are +false positives: Solidity requires a base imported by name for `@inheritdoc`, and deleting those +imports fails the build. + +The one open **decision** is L-10, an event on `invalidateNonce`: not required by ERC-7741, so this is +about off-chain observability rather than conformance. Per-finding verification in +[`aderyn-report-feedback.md`](./aderyn-report-feedback.md); see also +[`doc/audits/AUDIT_OVERVIEW.md`](../../AUDIT_OVERVIEW.md). + +--- + +# Aderyn Analysis Report + +This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a static analysis tool built by [Cyfrin](https://cyfrin.io), a blockchain security company. This report is not a substitute for manual audit or security review. It should not be relied upon for any purpose other than to assist in the identification of potential security vulnerabilities. +# Table of Contents + +- [Summary](#summary) + - [Files Summary](#files-summary) + - [Files Details](#files-details) + - [Issue Summary](#issue-summary) +- [Low Issues](#low-issues) + - [L-1: Centralization Risk](#l-1-centralization-risk) + - [L-2: Unspecific Solidity Pragma](#l-2-unspecific-solidity-pragma) + - [L-3: Public Function Not Used Internally](#l-3-public-function-not-used-internally) + - [L-4: PUSH0 Opcode](#l-4-push0-opcode) + - [L-5: Modifier Invoked Only Once](#l-5-modifier-invoked-only-once) + - [L-6: Empty Block](#l-6-empty-block) + - [L-7: Loop Contains `require`/`revert`](#l-7-loop-contains-requirerevert) + - [L-8: Costly operations inside loop](#l-8-costly-operations-inside-loop) + - [L-9: Unused Import](#l-9-unused-import) + - [L-10: State Change Without Event](#l-10-state-change-without-event) + + +# Summary + +## Files Summary + +| Key | Value | +| --- | --- | +| .sol Files | 21 | +| Total nSLOC | 980 | + + +## Files Details + +| Filepath | nSLOC | +| --- | --- | +| src/IncomeVaultBase.sol | 30 | +| src/IncomeVaultBaseERC2771.sol | 35 | +| src/deployment/IncomeVault.sol | 72 | +| src/deployment/IncomeVaultOwnable2Step.sol | 78 | +| src/interfaces/IERC7540Operator.sol | 6 | +| src/interfaces/IERC7741.sol | 14 | +| src/interfaces/IIncomeVault.sol | 35 | +| src/interfaces/ISnapshotSource.sol | 6 | +| src/modules/ERC7741Module.sol | 71 | +| src/modules/IncomeVaultInternal.sol | 152 | +| src/modules/IncomeVaultOperatorModule.sol | 41 | +| src/modules/IncomeVaultSnapshotCore.sol | 18 | +| src/modules/IncomeVaultSnapshotModule.sol | 70 | +| src/modules/IncomeVaultValidationCore.sol | 4 | +| src/modules/IncomeVaultValidationModule.sol | 77 | +| src/modules/Ownable2StepERC165Module.sol | 11 | +| src/modules/VersionModule.sol | 8 | +| src/public/IncomeVaultOpen.sol | 72 | +| src/public/IncomeVaultRestricted.sol | 141 | +| src/storage/IncomeVaultInvariantStorage.sol | 32 | +| src/storage/IncomeVaultRolesStorage.sol | 7 | +| **Total** | **980** | + + +## Issue Summary + +| Category | No. of Issues | +| --- | --- | +| High | 0 | +| Low | 10 | + + +# Low Issues + +## L-1: Centralization Risk + +Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds. + +
18 Found Instances + + +- Found in src/deployment/IncomeVault.sol [Line: 129](../../../../src/deployment/IncomeVault.sol#L129) + + ```solidity + function _authorizeDeposit() internal view virtual override onlyRole(INCOME_VAULT_DEPOSIT_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 132](../../../../src/deployment/IncomeVault.sol#L132) + + ```solidity + function _authorizeWithdraw() internal view virtual override onlyRole(INCOME_VAULT_WITHDRAW_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 135](../../../../src/deployment/IncomeVault.sol#L135) + + ```solidity + function _authorizeDistribute() internal view virtual override onlyRole(INCOME_VAULT_DISTRIBUTE_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 138](../../../../src/deployment/IncomeVault.sol#L138) + + ```solidity + function _authorizeOperator() internal view virtual override onlyRole(INCOME_VAULT_OPERATOR_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 141](../../../../src/deployment/IncomeVault.sol#L141) + + ```solidity + function _authorizeSnapshotSourceManagement() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 144](../../../../src/deployment/IncomeVault.sol#L144) + + ```solidity + function _authorizeRuleEngineManagement() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 147](../../../../src/deployment/IncomeVault.sol#L147) + + ```solidity + function _authorizePause() internal view virtual override(PauseModule) onlyRole(PAUSER_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 150](../../../../src/deployment/IncomeVault.sol#L150) + + ```solidity + function _authorizeDeactivate() internal view virtual override(PauseModule) onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 153](../../../../src/deployment/IncomeVault.sol#L153) + + ```solidity + function _authorizeFreeze() internal view virtual override(EnforcementModule) onlyRole(ENFORCER_ROLE) {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 133](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L133) + + ```solidity + function _authorizeDeposit() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 136](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L136) + + ```solidity + function _authorizeWithdraw() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 139](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L139) + + ```solidity + function _authorizeDistribute() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 142](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L142) + + ```solidity + function _authorizeOperator() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 145](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L145) + + ```solidity + function _authorizeSnapshotSourceManagement() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 148](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L148) + + ```solidity + function _authorizeRuleEngineManagement() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 151](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L151) + + ```solidity + function _authorizePause() internal view virtual override(PauseModule) onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 154](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L154) + + ```solidity + function _authorizeDeactivate() internal view virtual override(PauseModule) onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 157](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L157) + + ```solidity + function _authorizeFreeze() internal view virtual override(EnforcementModule) onlyOwner {} + ``` + +
+ + + +## L-2: Unspecific Solidity Pragma + +Consider using a specific version of Solidity in your contracts instead of a wide version. For example, instead of `pragma solidity ^0.8.0;`, use `pragma solidity 0.8.0;` + +
21 Found Instances + + +- Found in src/IncomeVaultBase.sol [Line: 3](../../../../src/IncomeVaultBase.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/IncomeVaultBaseERC2771.sol [Line: 3](../../../../src/IncomeVaultBaseERC2771.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 3](../../../../src/deployment/IncomeVault.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 3](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/interfaces/IERC7540Operator.sol [Line: 3](../../../../src/interfaces/IERC7540Operator.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/interfaces/IERC7741.sol [Line: 3](../../../../src/interfaces/IERC7741.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/interfaces/IIncomeVault.sol [Line: 3](../../../../src/interfaces/IIncomeVault.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/interfaces/ISnapshotSource.sol [Line: 3](../../../../src/interfaces/ISnapshotSource.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/ERC7741Module.sol [Line: 3](../../../../src/modules/ERC7741Module.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultInternal.sol [Line: 3](../../../../src/modules/IncomeVaultInternal.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultOperatorModule.sol [Line: 3](../../../../src/modules/IncomeVaultOperatorModule.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultSnapshotCore.sol [Line: 3](../../../../src/modules/IncomeVaultSnapshotCore.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultSnapshotModule.sol [Line: 3](../../../../src/modules/IncomeVaultSnapshotModule.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultValidationCore.sol [Line: 3](../../../../src/modules/IncomeVaultValidationCore.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultValidationModule.sol [Line: 3](../../../../src/modules/IncomeVaultValidationModule.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/Ownable2StepERC165Module.sol [Line: 3](../../../../src/modules/Ownable2StepERC165Module.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/VersionModule.sol [Line: 3](../../../../src/modules/VersionModule.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/public/IncomeVaultOpen.sol [Line: 3](../../../../src/public/IncomeVaultOpen.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/public/IncomeVaultRestricted.sol [Line: 3](../../../../src/public/IncomeVaultRestricted.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/storage/IncomeVaultInvariantStorage.sol [Line: 3](../../../../src/storage/IncomeVaultInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/storage/IncomeVaultRolesStorage.sol [Line: 3](../../../../src/storage/IncomeVaultRolesStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +
+ + + +## L-3: Public Function Not Used Internally + +If a function is marked public but is not used internally, consider marking it as `external`. + +
2 Found Instances + + +- Found in src/deployment/IncomeVault.sol [Line: 58](../../../../src/deployment/IncomeVault.sol#L58) + + ```solidity + function initialize( + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 64](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L64) + + ```solidity + function initialize( + ``` + +
+ + + +## L-4: PUSH0 Opcode + +Solc compiler version 0.8.20 switches the default target EVM version to Shanghai, which means that the generated bytecode will include PUSH0 opcodes. Be sure to select the appropriate EVM version in case you intend to deploy on a chain other than mainnet like L2 chains that may not support PUSH0, otherwise deployment of your contracts will fail. + +
21 Found Instances + + +- Found in src/IncomeVaultBase.sol [Line: 3](../../../../src/IncomeVaultBase.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/IncomeVaultBaseERC2771.sol [Line: 3](../../../../src/IncomeVaultBaseERC2771.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 3](../../../../src/deployment/IncomeVault.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 3](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/interfaces/IERC7540Operator.sol [Line: 3](../../../../src/interfaces/IERC7540Operator.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/interfaces/IERC7741.sol [Line: 3](../../../../src/interfaces/IERC7741.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/interfaces/IIncomeVault.sol [Line: 3](../../../../src/interfaces/IIncomeVault.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/interfaces/ISnapshotSource.sol [Line: 3](../../../../src/interfaces/ISnapshotSource.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/ERC7741Module.sol [Line: 3](../../../../src/modules/ERC7741Module.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultInternal.sol [Line: 3](../../../../src/modules/IncomeVaultInternal.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultOperatorModule.sol [Line: 3](../../../../src/modules/IncomeVaultOperatorModule.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultSnapshotCore.sol [Line: 3](../../../../src/modules/IncomeVaultSnapshotCore.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultSnapshotModule.sol [Line: 3](../../../../src/modules/IncomeVaultSnapshotModule.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultValidationCore.sol [Line: 3](../../../../src/modules/IncomeVaultValidationCore.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/IncomeVaultValidationModule.sol [Line: 3](../../../../src/modules/IncomeVaultValidationModule.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/Ownable2StepERC165Module.sol [Line: 3](../../../../src/modules/Ownable2StepERC165Module.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/modules/VersionModule.sol [Line: 3](../../../../src/modules/VersionModule.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/public/IncomeVaultOpen.sol [Line: 3](../../../../src/public/IncomeVaultOpen.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/public/IncomeVaultRestricted.sol [Line: 3](../../../../src/public/IncomeVaultRestricted.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/storage/IncomeVaultInvariantStorage.sol [Line: 3](../../../../src/storage/IncomeVaultInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +- Found in src/storage/IncomeVaultRolesStorage.sol [Line: 3](../../../../src/storage/IncomeVaultRolesStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.24; + ``` + +
+ + + +## L-5: Modifier Invoked Only Once + +Consider removing the modifier or inlining the logic into the calling function. + +
2 Found Instances + + +- Found in src/modules/IncomeVaultSnapshotModule.sol [Line: 26](../../../../src/modules/IncomeVaultSnapshotModule.sol#L26) + + ```solidity + modifier onlySnapshotSourceManager() { + ``` + +- Found in src/modules/IncomeVaultValidationModule.sol [Line: 39](../../../../src/modules/IncomeVaultValidationModule.sol#L39) + + ```solidity + modifier onlyRuleEngineManager() { + ``` + +
+ + + +## L-6: Empty Block + +Consider removing empty blocks. + +
18 Found Instances + + +- Found in src/deployment/IncomeVault.sol [Line: 129](../../../../src/deployment/IncomeVault.sol#L129) + + ```solidity + function _authorizeDeposit() internal view virtual override onlyRole(INCOME_VAULT_DEPOSIT_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 132](../../../../src/deployment/IncomeVault.sol#L132) + + ```solidity + function _authorizeWithdraw() internal view virtual override onlyRole(INCOME_VAULT_WITHDRAW_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 135](../../../../src/deployment/IncomeVault.sol#L135) + + ```solidity + function _authorizeDistribute() internal view virtual override onlyRole(INCOME_VAULT_DISTRIBUTE_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 138](../../../../src/deployment/IncomeVault.sol#L138) + + ```solidity + function _authorizeOperator() internal view virtual override onlyRole(INCOME_VAULT_OPERATOR_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 141](../../../../src/deployment/IncomeVault.sol#L141) + + ```solidity + function _authorizeSnapshotSourceManagement() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 144](../../../../src/deployment/IncomeVault.sol#L144) + + ```solidity + function _authorizeRuleEngineManagement() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 147](../../../../src/deployment/IncomeVault.sol#L147) + + ```solidity + function _authorizePause() internal view virtual override(PauseModule) onlyRole(PAUSER_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 150](../../../../src/deployment/IncomeVault.sol#L150) + + ```solidity + function _authorizeDeactivate() internal view virtual override(PauseModule) onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 153](../../../../src/deployment/IncomeVault.sol#L153) + + ```solidity + function _authorizeFreeze() internal view virtual override(EnforcementModule) onlyRole(ENFORCER_ROLE) {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 133](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L133) + + ```solidity + function _authorizeDeposit() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 136](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L136) + + ```solidity + function _authorizeWithdraw() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 139](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L139) + + ```solidity + function _authorizeDistribute() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 142](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L142) + + ```solidity + function _authorizeOperator() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 145](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L145) + + ```solidity + function _authorizeSnapshotSourceManagement() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 148](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L148) + + ```solidity + function _authorizeRuleEngineManagement() internal view virtual override onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 151](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L151) + + ```solidity + function _authorizePause() internal view virtual override(PauseModule) onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 154](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L154) + + ```solidity + function _authorizeDeactivate() internal view virtual override(PauseModule) onlyOwner {} + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 157](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L157) + + ```solidity + function _authorizeFreeze() internal view virtual override(EnforcementModule) onlyOwner {} + ``` + +
+ + + +## L-7: Loop Contains `require`/`revert` + +Avoid `require` / `revert` statements in a loop because a single bad item can cause the whole transaction to fail. It's better to forgive on fail and return failed elements post processing of the loop + +
4 Found Instances + + +- Found in src/public/IncomeVaultOpen.sol [Line: 95](../../../../src/public/IncomeVaultOpen.sol#L95) + + ```solidity + for (uint256 i = 0; i < times.length; ++i) { + ``` + +- Found in src/public/IncomeVaultOpen.sol [Line: 146](../../../../src/public/IncomeVaultOpen.sol#L146) + + ```solidity + for (uint256 i = 0; i < times.length; ++i) { + ``` + +- Found in src/public/IncomeVaultRestricted.sol [Line: 105](../../../../src/public/IncomeVaultRestricted.sol#L105) + + ```solidity + for (uint256 i = 0; i < times.length; ++i) { + ``` + +- Found in src/public/IncomeVaultRestricted.sol [Line: 181](../../../../src/public/IncomeVaultRestricted.sol#L181) + + ```solidity + for (uint256 i = 0; i < addresses.length; ++i) { + ``` + +
+ + + +## L-8: Costly operations inside loop + +Invoking `SSTORE` operations in loops may waste gas. Use a local variable to hold the loop computation result. + +
3 Found Instances + + +- Found in src/public/IncomeVaultOpen.sol [Line: 146](../../../../src/public/IncomeVaultOpen.sol#L146) + + ```solidity + for (uint256 i = 0; i < times.length; ++i) { + ``` + +- Found in src/public/IncomeVaultRestricted.sol [Line: 105](../../../../src/public/IncomeVaultRestricted.sol#L105) + + ```solidity + for (uint256 i = 0; i < times.length; ++i) { + ``` + +- Found in src/public/IncomeVaultRestricted.sol [Line: 181](../../../../src/public/IncomeVaultRestricted.sol#L181) + + ```solidity + for (uint256 i = 0; i < addresses.length; ++i) { + ``` + +
+ + + +## L-9: Unused Import + +Redundant import statement. Consider removing it. + +
4 Found Instances + + +- Found in src/deployment/IncomeVault.sol [Line: 21](../../../../src/deployment/IncomeVault.sol#L21) + + ```solidity + import {IncomeVaultRestricted} from "../public/IncomeVaultRestricted.sol"; + ``` + +- Found in src/deployment/IncomeVault.sol [Line: 22](../../../../src/deployment/IncomeVault.sol#L22) + + ```solidity + import {IncomeVaultSnapshotModule} from "../modules/IncomeVaultSnapshotModule.sol"; + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 19](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L19) + + ```solidity + import {IncomeVaultRestricted} from "../public/IncomeVaultRestricted.sol"; + ``` + +- Found in src/deployment/IncomeVaultOwnable2Step.sol [Line: 20](../../../../src/deployment/IncomeVaultOwnable2Step.sol#L20) + + ```solidity + import {IncomeVaultSnapshotModule} from "../modules/IncomeVaultSnapshotModule.sol"; + ``` + +
+ + + +## L-10: State Change Without Event + +There are state variable changes in this function but no event is emitted. Consider emitting an event to enable offchain indexers to track the changes. + +
1 Found Instances + + +- Found in src/modules/ERC7741Module.sol [Line: 107](../../../../src/modules/ERC7741Module.sol#L107) + + ```solidity + function invalidateNonce(bytes32 nonce) public virtual override(IERC7741) { + ``` + +
+ + + diff --git a/doc/audits/tools/v2.0.0/slither-report-feedback.md b/doc/audits/tools/v2.0.0/slither-report-feedback.md new file mode 100644 index 0000000..4d92af8 --- /dev/null +++ b/doc/audits/tools/v2.0.0/slither-report-feedback.md @@ -0,0 +1,35 @@ +# Slither feedback — IncomeVault v2.0.0 + +Triage of every finding in [`slither-report.md`](./slither-report.md). Each dismissal was checked against the cited `file:line`, not assumed. + +```bash +slither . --checklist --filter-paths "node_modules,lib,test" > doc/audits/tools/v2.0.0/slither-report.md +``` + +Scope: `src/` only, mocks and tests excluded, 90 contracts, 101 detectors. `grep -c 'lib/\|node_modules/'` on the report returns 0. + +## Summary + +| Detector | Severity | Instances | Disposition | Reason | +| --- | --- | --- | --- | --- | +| `uninitialized-local` | Medium | 2 | False positive | `distributeDividendBestEffort.skippedCount` and `depositBatch.total` are accumulators. Solidity zero-initialises locals, and both are written before any read — `skippedCount` only ever via `skippedBuffer[skippedCount++]`, `total` via `total += amounts[i]`. Slither flags the absence of an explicit `= 0`, which the language already guarantees. | +| `unused-return` | Medium | 3 | False positive | All three are in `IncomeVaultSnapshotModule`, and the body is `return dividendSnapshotSource().snapshotInfo(...)` — the value is returned straight to the caller, not discarded. Slither does not model a direct tail return of an external call's tuple. | +| `calls-loop` | Low | 4 | By design | `IRuleEngine.canTransfer` is consulted once per holder inside `claimDividendBatch` / `distributeDividend`. That is the point: a compliance decision is per payout and cannot be hoisted. The consequence — a batch large enough to exhaust gas — is real and bounded by the caller choosing the batch size, not by the contract. | +| `timestamp` | Low | 2 | By design | The claim window is *defined* in `block.timestamp`: a claim is refused before `time` and after `time + timeLimitToWithdraw`. Miner drift of a few seconds against a window measured in days is not a manipulation surface. | +| `assembly` | Informational | 4 | By design | The four ERC-7201 storage accessors. Assembly is the only way to assign `$.slot`, and it is the pattern OpenZeppelin Upgradeable and CMTAT v3 both use. | +| `dead-code` | Informational | 3 | False positive | `_msgData()` in `IncomeVault`, `IncomeVaultOwnable2Step` and `IncomeVaultBaseERC2771`. These are **required** overrides resolving the `ERC2771ContextUpgradeable` / `ContextUpgradeable` diamond; deleting any of them fails to compile. Slither sees no caller because the caller is the compiler's dispatch, not project code. | +| `naming-convention` | Informational | 16 | By design | Every instance is an upstream convention: `__X_init_unchained` (OpenZeppelin initializers), `ERC20TokenPayment_` (trailing underscore for a constructor/initializer argument), `XStorageLocation` (ERC-7201 slot constants, CapWords in OZ too), `DOMAIN_SEPARATOR` (fixed by EIP-712), `TIME_ERROR_CODE` (an ERC-1404-style code enum), and `newDeposit` — kept lowercase deliberately for v1 ABI compatibility. | + +## Delta from v1.0.0 + +The v1.0.0 report predates the CMTAT v3 migration and the whole modularity rework, so a finding-by-finding delta would compare two different architectures. What is worth recording: + +- **The v1.0.0 findings are gone by construction.** They cite `__gap`, `ICMTATSnapshot` and `IAuthorizationEngine` — none of which exist now. Storage moved to ERC-7201 (no `__gap`), the snapshot source became `ISnapshotSource`, and the authorization engine was removed by CMTAT v3. +- **Contract count roughly doubled** (one deployable plus a base, to 21 source files across `deployment/`, `modules/`, `public/`, `interfaces/`, `storage/`), yet the total is 34 findings, all informational-to-medium and none real. The new `assembly` and `naming-convention` instances are the direct, expected cost of adopting ERC-7201. +- **The filter changed** from a name list to `lib`. See the report header: the old form works today but fails open if a dependency directory is added whose name is not enumerated. + +## Executive triage + +**Nothing to fix.** No finding is exploitable, and none indicates a defect. The two Medium findings are both false positives verified against the source: locals that Solidity zero-initialises, and an external call whose result is returned rather than dropped. + +The one finding worth *remembering* rather than fixing is `calls-loop`: batch entry points are bounded by gas, so an operator building a very large `distributeDividend` batch must size it themselves. That is already the reason `distributeDividendBestEffort` exists. diff --git a/doc/audits/tools/v2.0.0/slither-report.md b/doc/audits/tools/v2.0.0/slither-report.md new file mode 100644 index 0000000..a1385cb --- /dev/null +++ b/doc/audits/tools/v2.0.0/slither-report.md @@ -0,0 +1,294 @@ +# Slither report — IncomeVault v2.0.0 + +> The contracts are **NOT audited**. Static analysis is not an audit; these are leads, not findings. + +| | | +| --- | --- | +| Tool | Slither 0.11.5 | +| Date | 2026-08-20 | +| Scope | `src/` only — 90 contracts analysed, 101 detectors | +| Mocks | **excluded** (`test/` filtered, and Slither's Foundry driver skips `./test/**` and `./script/**`) | + +```bash +slither . --checklist --filter-paths "node_modules,lib,test" \ + > doc/audits/tools/v2.0.0/slither-report.md +``` + +**Scope verified:** `grep -c 'lib/\|node_modules/'` on this report returns **0**, so no vendored +dependency is in scope. The `filter-paths` entry is `lib` because this is a Foundry project; the +name-based filter used for v1.0.0 (`openzeppelin-contracts|test|CMTAT|forge-std`) happens to match the +same paths but fails open if a dependency is added whose directory name is not listed. + +## Result: 0 High · 5 Medium · 6 Low · 23 Informational — 34 total + +| Detector | Severity | Instances | Assessment | +| --- | --- | --- | --- | +| `uninitialized-local` | Medium | 2 | False positive — counters, zero-initialised by the language and written before any read | +| `unused-return` | Medium | 3 | False positive — the value is `return`ed straight to the caller | +| `calls-loop` | Low | 4 | By design — the RuleEngine must be consulted per holder | +| `timestamp` | Low | 2 | By design — the claim window *is* defined in `block.timestamp` | +| `assembly` | Informational | 4 | By design — the four ERC-7201 storage accessors | +| `dead-code` | Informational | 3 | False positive — `_msgData()` overrides are required to resolve the ERC-2771 diamond | +| `naming-convention` | Informational | 16 | By design — OpenZeppelin and CMTAT naming conventions | + +**Nothing in this report needs fixing.** Every finding is a false positive or a documented design +decision; each is verified against the source in +[`slither-report-feedback.md`](./slither-report-feedback.md). See also +[`doc/audits/AUDIT_OVERVIEW.md`](../../AUDIT_OVERVIEW.md). + +--- + +**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results. +Summary + - [uninitialized-local](#uninitialized-local) (2 results) (Medium) + - [unused-return](#unused-return) (3 results) (Medium) + - [calls-loop](#calls-loop) (4 results) (Low) + - [timestamp](#timestamp) (2 results) (Low) + - [assembly](#assembly) (4 results) (Informational) + - [dead-code](#dead-code) (3 results) (Informational) + - [naming-convention](#naming-convention) (16 results) (Informational) +## uninitialized-local +Impact: Medium +Confidence: Medium + - [ ] ID-0 +[IncomeVaultRestricted.depositBatch(uint256[],uint256[]).total](src/public/IncomeVaultRestricted.sol#L104) is a local variable never initialized + +src/public/IncomeVaultRestricted.sol#L104 + + + - [ ] ID-1 +[IncomeVaultRestricted.distributeDividendBestEffort(address[],uint256).skippedCount](src/public/IncomeVaultRestricted.sol#L237) is a local variable never initialized + +src/public/IncomeVaultRestricted.sol#L237 + + +## unused-return +Impact: Medium +Confidence: Medium + - [ ] ID-2 +[IncomeVaultSnapshotModule._snapshotInfoBatch(uint256,address[])](src/modules/IncomeVaultSnapshotModule.sol#L117-L125) ignores return value by [dividendSnapshotSource().snapshotInfoBatch(time,addresses)](src/modules/IncomeVaultSnapshotModule.sol#L124) + +src/modules/IncomeVaultSnapshotModule.sol#L117-L125 + + + - [ ] ID-3 +[IncomeVaultSnapshotModule._snapshotInfoBatch(uint256[],address[])](src/modules/IncomeVaultSnapshotModule.sol#L128-L136) ignores return value by [dividendSnapshotSource().snapshotInfoBatch(times,addresses)](src/modules/IncomeVaultSnapshotModule.sol#L135) + +src/modules/IncomeVaultSnapshotModule.sol#L128-L136 + + + - [ ] ID-4 +[IncomeVaultSnapshotModule._snapshotInfo(uint256,address)](src/modules/IncomeVaultSnapshotModule.sol#L106-L114) ignores return value by [dividendSnapshotSource().snapshotInfo(time,tokenHolder)](src/modules/IncomeVaultSnapshotModule.sol#L113) + +src/modules/IncomeVaultSnapshotModule.sol#L106-L114 + + +## calls-loop +Impact: Low +Confidence: Medium + - [ ] ID-5 +[IncomeVaultValidationModule.canTransfer(address,address,uint256)](src/modules/IncomeVaultValidationModule.sol#L87-L103) has external calls inside a loop: [ruleEngine_.canTransfer(from,to,value)](src/modules/IncomeVaultValidationModule.sol#L100) + Calls stack containing the loop: + IncomeVaultRestricted.distributeDividend(address[],uint256) + IncomeVaultValidationModule._validateTransfer(address,address,uint256) + +src/modules/IncomeVaultValidationModule.sol#L87-L103 + + + - [ ] ID-6 +[IncomeVaultRestricted.distributeDividendBestEffort(address[],uint256)](src/public/IncomeVaultRestricted.sol#L223-L257) has external calls inside a loop: [this.transferDividendSelf(time,addresses[i],tokenHolderDividend[i])](src/public/IncomeVaultRestricted.sol#L244-L250) + +src/public/IncomeVaultRestricted.sol#L223-L257 + + + - [ ] ID-7 +[IncomeVaultValidationModule.canTransfer(address,address,uint256)](src/modules/IncomeVaultValidationModule.sol#L87-L103) has external calls inside a loop: [ruleEngine_.canTransfer(from,to,value)](src/modules/IncomeVaultValidationModule.sol#L100) + Calls stack containing the loop: + IncomeVaultOpen.claimDividendBatch(uint256[]) + IncomeVaultOpen._claimDividendBatch(address,uint256[]) + IncomeVaultValidationModule._validateTransfer(address,address,uint256) + +src/modules/IncomeVaultValidationModule.sol#L87-L103 + + + - [ ] ID-8 +[IncomeVaultValidationModule.canTransfer(address,address,uint256)](src/modules/IncomeVaultValidationModule.sol#L87-L103) has external calls inside a loop: [ruleEngine_.canTransfer(from,to,value)](src/modules/IncomeVaultValidationModule.sol#L100) + Calls stack containing the loop: + IncomeVaultOpen.claimDividendBatchFor(address,uint256[]) + IncomeVaultOpen._claimDividendBatch(address,uint256[]) + IncomeVaultValidationModule._validateTransfer(address,address,uint256) + +src/modules/IncomeVaultValidationModule.sol#L87-L103 + + +## timestamp +Impact: Low +Confidence: Medium + - [ ] ID-9 +[ERC7741Module.authorizeOperator(address,address,bool,bytes32,uint256,bytes)](src/modules/ERC7741Module.sol#L73-L104) uses timestamp for comparisons + Dangerous comparisons: + - [block.timestamp > deadline](src/modules/ERC7741Module.sol#L81) + +src/modules/ERC7741Module.sol#L73-L104 + + + - [ ] ID-10 +[IncomeVaultInternal._timeCode(IncomeVaultInternal.IncomeVaultInternalStorage,uint256,uint256)](src/modules/IncomeVaultInternal.sol#L322-L338) uses timestamp for comparisons + Dangerous comparisons: + - [block.timestamp > timeLimit + time](src/modules/IncomeVaultInternal.sol#L331) + - [block.timestamp < time](src/modules/IncomeVaultInternal.sol#L334) + +src/modules/IncomeVaultInternal.sol#L322-L338 + + +## assembly +Impact: Informational +Confidence: High + - [ ] ID-11 +[IncomeVaultSnapshotModule._getSnapshotSourceStorage()](src/modules/IncomeVaultSnapshotModule.sol#L150-L154) uses assembly + - [INLINE ASM](src/modules/IncomeVaultSnapshotModule.sol#L151-L153) + +src/modules/IncomeVaultSnapshotModule.sol#L150-L154 + + + - [ ] ID-12 +[IncomeVaultOperatorModule._getOperatorStorage()](src/modules/IncomeVaultOperatorModule.sol#L104-L108) uses assembly + - [INLINE ASM](src/modules/IncomeVaultOperatorModule.sol#L105-L107) + +src/modules/IncomeVaultOperatorModule.sol#L104-L108 + + + - [ ] ID-13 +[ERC7741Module._getERC7741ModuleStorage()](src/modules/ERC7741Module.sol#L134-L138) uses assembly + - [INLINE ASM](src/modules/ERC7741Module.sol#L135-L137) + +src/modules/ERC7741Module.sol#L134-L138 + + + - [ ] ID-14 +[IncomeVaultInternal._getIncomeVaultInternalStorage()](src/modules/IncomeVaultInternal.sol#L345-L349) uses assembly + - [INLINE ASM](src/modules/IncomeVaultInternal.sol#L346-L348) + +src/modules/IncomeVaultInternal.sol#L345-L349 + + +## dead-code +Impact: Informational +Confidence: Medium + - [ ] ID-15 +[IncomeVaultBaseERC2771._msgData()](src/IncomeVaultBaseERC2771.sol#L61-L69) is never used and should be removed + +src/IncomeVaultBaseERC2771.sol#L61-L69 + + + - [ ] ID-16 +[IncomeVaultOwnable2Step._msgData()](src/deployment/IncomeVaultOwnable2Step.sol#L114-L116) is never used and should be removed + +src/deployment/IncomeVaultOwnable2Step.sol#L114-L116 + + + - [ ] ID-17 +[IncomeVault._msgData()](src/deployment/IncomeVault.sol#L110-L112) is never used and should be removed + +src/deployment/IncomeVault.sol#L110-L112 + + +## naming-convention +Impact: Informational +Confidence: High + - [ ] ID-18 +Function [IncomeVaultInternal.ERC20TokenPayment()](src/modules/IncomeVaultInternal.sol#L67-L70) is not in mixedCase + +src/modules/IncomeVaultInternal.sol#L67-L70 + + + - [ ] ID-19 +Parameter [IncomeVaultBase.__IncomeVaultBase_init_unchained(IERC20,ISnapshotSource,uint256).ERC20TokenPayment_](src/IncomeVaultBase.sol#L54) is not in mixedCase + +src/IncomeVaultBase.sol#L54 + + + - [ ] ID-20 +Function [IERC7741.DOMAIN_SEPARATOR()](src/interfaces/IERC7741.sol#L61) is not in mixedCase + +src/interfaces/IERC7741.sol#L61 + + + - [ ] ID-21 +Parameter [IncomeVault.initialize(address,IERC20,ISnapshotSource,IRuleEngine,uint256).ERC20TokenPayment_](src/deployment/IncomeVault.sol#L60) is not in mixedCase + +src/deployment/IncomeVault.sol#L60 + + + - [ ] ID-22 +Event [IncomeVaultInvariantStorage.newDeposit(uint256,address,uint256)](src/storage/IncomeVaultInvariantStorage.sol#L20) is not in CapWords + +src/storage/IncomeVaultInvariantStorage.sol#L20 + + + - [ ] ID-23 +Function [IIncomeVault.ERC20TokenPayment()](src/interfaces/IIncomeVault.sol#L157) is not in mixedCase + +src/interfaces/IIncomeVault.sol#L157 + + + - [ ] ID-24 +Parameter [IncomeVaultOwnable2Step.initialize(address,IERC20,ISnapshotSource,IRuleEngine,uint256).ERC20TokenPayment_](src/deployment/IncomeVaultOwnable2Step.sol#L66) is not in mixedCase + +src/deployment/IncomeVaultOwnable2Step.sol#L66 + + + - [ ] ID-25 +Constant [IncomeVaultSnapshotModule.SnapshotSourceStorageLocation](src/modules/IncomeVaultSnapshotModule.sol#L37-L38) is not in UPPER_CASE_WITH_UNDERSCORES + +src/modules/IncomeVaultSnapshotModule.sol#L37-L38 + + + - [ ] ID-26 +Function [ERC7741Module.DOMAIN_SEPARATOR()](src/modules/ERC7741Module.sol#L122-L124) is not in mixedCase + +src/modules/ERC7741Module.sol#L122-L124 + + + - [ ] ID-27 +Constant [IncomeVaultInternal.IncomeVaultInternalStorageLocation](src/modules/IncomeVaultInternal.sol#L34-L35) is not in UPPER_CASE_WITH_UNDERSCORES + +src/modules/IncomeVaultInternal.sol#L34-L35 + + + - [ ] ID-28 +Function [IncomeVaultRestricted.__IncomeVaultRestricted_init_unchained(uint256)](src/public/IncomeVaultRestricted.sol#L58-L60) is not in mixedCase + +src/public/IncomeVaultRestricted.sol#L58-L60 + + + - [ ] ID-29 +Function [IncomeVaultBase.__IncomeVaultBase_init_unchained(IERC20,ISnapshotSource,uint256)](src/IncomeVaultBase.sol#L53-L65) is not in mixedCase + +src/IncomeVaultBase.sol#L53-L65 + + + - [ ] ID-30 +Enum [IIncomeVault.TIME_ERROR_CODE](src/interfaces/IIncomeVault.sol#L35-L40) is not in CapWords + +src/interfaces/IIncomeVault.sol#L35-L40 + + + - [ ] ID-31 +Constant [ERC7741Module.ERC7741ModuleStorageLocation](src/modules/ERC7741Module.sol#L48) is not in UPPER_CASE_WITH_UNDERSCORES + +src/modules/ERC7741Module.sol#L48 + + + - [ ] ID-32 +Constant [IncomeVaultOperatorModule.OperatorStorageLocation](src/modules/IncomeVaultOperatorModule.sol#L31-L32) is not in UPPER_CASE_WITH_UNDERSCORES + +src/modules/IncomeVaultOperatorModule.sol#L31-L32 + + + - [ ] ID-33 +Function [IncomeVaultValidationModule.__IncomeVaultValidation_init_unchained(IRuleEngine)](src/modules/IncomeVaultValidationModule.sol#L56-L60) is not in mixedCase + +src/modules/IncomeVaultValidationModule.sol#L56-L60 + + diff --git a/doc/cmtat-standard/CMTAT-Distribution-Additions.md b/doc/cmtat-standard/CMTAT-Distribution-Additions.md new file mode 100644 index 0000000..7cfe5c6 --- /dev/null +++ b/doc/cmtat-standard/CMTAT-Distribution-Additions.md @@ -0,0 +1,37 @@ +# Additions to the CMTAT Distribution module + +The CMTA framework functional specifications (June 2026) describe an optional **Distribution module** in section 3.2.4, with functionalities numbered 27 to 32 — see [`cmtat-framework-functional-specifications-june-2026.pdf`](./cmtat-framework-functional-specifications-june-2026.pdf). + +This document holds the **additions**: behaviour with no counterpart in section 3.2.4 at all. + +## The specification text these refer to + +Quoted from section 3.2.4 of [`cmtat-framework-functional-specifications-june-2026.pdf`](./cmtat-framework-functional-specifications-june-2026.pdf) so a proposal can be read without opening it. The PDF remains authoritative; square brackets are the specification's own. + +> **3.2.4 Distribution module** +> +> Issuers may be required to make distributions to holders of securities (e.g. dividend payments for equity securities or interest payments for debt securities). Issuers may wish to carry out distributions off-chain (i.e. by transferring fiat currencies to the securities' holders' bank account). However, if the issuer intends to carry out such distributions on-chain, this may require the distribution of new tokens to existing token holders, on the basis of a snapshot carried out at the moment the legal entitlement to the distribution arises as not all token holders may be eligible for distributions. +> +> Distribution events are typically performed according to a predefined schedule, and according to the token distribution at a given time, which can be determined by a snapshot performed with the Snapshot module. +> +> **Functionalities** +> +> 27. **Distribution create parameters**: Define settlement token (i.e. the token that is to be distributed), identify a (past or future) block time/height for distribution snapshot, and amount to be distributed. +> 28. **Distribution set eligibility**: Flag a given users' tokens as being eligible or non-eligible to receive distributions (default: eligible). +> 29. **Distribution set deposit**: Send deposit amount for claiming settlement tokens to token holders flagged as eligible. +> 30. **Distribution claim deposit**: Allow token holders to claim their share of a deposit, identified by a deposit Identification, according to the token balance at the snapshot created at the defined time/height. +> +> Additional use cases for tokens representing debt instruments: +> +> 31. **Distribution schedule**: Define a schedule for interest payments [and repayment of the par value at maturity], based on the token attributes. +> 32. **Distribution unschedule**: Cancel the previously set schedule for interest payments [and repayment of the principal amount at maturity]. + +## The additions + +| id | Proposal | Why — what implementing it exposed | +| --- | --- | --- | +| A-1 | **Recovery of what is not claimed**, once the claim period has closed | The specification never says where unclaimed funds end up. Rounding residue and the shares of holders who never claim would otherwise stay locked in the contract permanently. Pairs with C-2, which is what makes "closed" meaningful, and with C-3, which makes the residue a known quantity. `IncomeVault` bounds recovery per period by `unclaimedDividend(time)`. | +| A-2 | **A push counterpart to the pull claim of 30** | Functionality 30 is pull-only, which strands holders who never transact — custodied positions, dormant addresses, holders without gas. A distribution mechanism that requires every beneficiary to act is not one an issuer can rely on to discharge an obligation. | +| A-3 | **Delegated claiming: who may claim on a holder's behalf** | Follows from A-2. A holder who cannot pay gas, or cannot transact at all, still has to be paid. `IncomeVault` uses ERC-7540's `setOperator` and ERC-7741's signed authorisation, with the payout always going to the holder rather than the operator. The specification names no mechanism, so every implementation invents one and none of them interoperate. | + +These three are not defects in an implementation — they are questions the specification leaves unanswered, and an issuer has to answer all three to discharge a real obligation. A-1 depends on the claim period actually closing, which is C-2 in the amendments; A-3 follows from A-2, since a push payout and a delegated claim answer the same problem for the same holders. diff --git a/doc/cmtat-standard/CMTAT-Distribution-Additions.pdf b/doc/cmtat-standard/CMTAT-Distribution-Additions.pdf new file mode 100644 index 0000000..4ca199b Binary files /dev/null and b/doc/cmtat-standard/CMTAT-Distribution-Additions.pdf differ diff --git a/doc/cmtat-standard/CMTAT-Distribution-Amendments.md b/doc/cmtat-standard/CMTAT-Distribution-Amendments.md new file mode 100644 index 0000000..bcfdc6b --- /dev/null +++ b/doc/cmtat-standard/CMTAT-Distribution-Amendments.md @@ -0,0 +1,58 @@ +# Amendments to the CMTAT Distribution module + +> The contracts are **NOT audited**. Do not use them in production without an audit. + +The CMTA framework functional specifications (June 2026) describe an optional **Distribution module** in section 3.2.4, with functionalities numbered 27 to 32 — see [`cmtat-framework-functional-specifications-june-2026.pdf`](./cmtat-framework-functional-specifications-june-2026.pdf). + +This document holds the **amendments**: changes to functionalities the specification already defines. Each one constrains or clarifies existing text, so a conforming implementation may already satisfy it — the specification simply does not say so, which is what lets two conforming implementations disagree. + +## The specification text these refer to + +Quoted from section 3.2.4 of [`cmtat-framework-functional-specifications-june-2026.pdf`](./cmtat-framework-functional-specifications-june-2026.pdf) so a proposal can be read without opening it. The PDF remains authoritative; square brackets are the specification's own. + +> **3.2.4 Distribution module** +> +> Issuers may be required to make distributions to holders of securities (e.g. dividend payments for equity securities or interest payments for debt securities). Issuers may wish to carry out distributions off-chain (i.e. by transferring fiat currencies to the securities' holders' bank account). However, if the issuer intends to carry out such distributions on-chain, this may require the distribution of new tokens to existing token holders, on the basis of a snapshot carried out at the moment the legal entitlement to the distribution arises as not all token holders may be eligible for distributions. +> +> Distribution events are typically performed according to a predefined schedule, and according to the token distribution at a given time, which can be determined by a snapshot performed with the Snapshot module. +> +> **Functionalities** +> +> 27. **Distribution create parameters**: Define settlement token (i.e. the token that is to be distributed), identify a (past or future) block time/height for distribution snapshot, and amount to be distributed. +> 28. **Distribution set eligibility**: Flag a given users' tokens as being eligible or non-eligible to receive distributions (default: eligible). +> 29. **Distribution set deposit**: Send deposit amount for claiming settlement tokens to token holders flagged as eligible. +> 30. **Distribution claim deposit**: Allow token holders to claim their share of a deposit, identified by a deposit Identification, according to the token balance at the snapshot created at the defined time/height. +> +> Additional use cases for tokens representing debt instruments: +> +> 31. **Distribution schedule**: Define a schedule for interest payments [and repayment of the par value at maturity], based on the token attributes. +> 32. **Distribution unschedule**: Cancel the previously set schedule for interest payments [and repayment of the principal amount at maturity]. + +C-6 and C-8 refer to the token attributes rather than to a functionality. Those are in section 3.1.1, under *Additional attributes applicable to tokens used for debt securities*: + +> - Currency of payments (if applicable) +> - Par value (principal amount) (if applicable) +> - Maturity date (if applicable) +> - Interest rate (if applicable) +> - Coupon payment frequency (if applicable) +> - Interest schedule format (if applicable). The purpose of the interest schedule is to set, in the parameters of the smart contract, the dates on which the interest payments accrue. +> - Format A: start date/end date/period +> - Format B: start date/end date/day of period (e.g. quarter or year) +> - Format C: date 1/date 2/date 3/… +> - Interest payment date (if different from the date on which the interest payment accrues) + +## The amendments + +| id | Amends | Proposal | Why — what implementing it exposed | +| --- | --- | --- | --- | +| C-1 | **27, 30** | **Require a distribution's record date to resolve against a balance source that has already fixed those balances**, and require implementations to reject one that has not | A snapshot lookup for a time that was never scheduled does not fail — `SnapshotEngine` returns the holder's **live balance** (`_snapshotBalanceOf` ends `return snapshotted ? value : ownerBalance`). A distribution funded against a mistyped date therefore pays out pro-rata to balances *at claim time*, which anyone can change by acquiring tokens before claiming. The requirement is that the balances are **fixed at the record date and cannot change afterwards**, not that a snapshot exists — an off-chain pinning at a block height satisfies it equally. Phrasing it against a snapshot would forbid the alternatives in [*Eligibility without a snapshot*](./CMTAT-Distribution-impl.md#eligibility-without-a-snapshot); phrasing it against a free timestamp permits the failure above. | +| C-2 | **30** | **Cap the claim period** — a deadline after which a claim is refused | Functionality 30 says holders may claim; it never says until when. Without a deadline a distribution is an open liability forever, and the issuer can never close its books on a period. `IncomeVault` adds `timeLimitToWithdraw`. | +| C-3 | **30** | **State the rounding direction** for a holder's share | Pro-rata division always leaves dust. The specification is silent, so two conforming implementations can disagree on who gets it. State that shares round **down**, which makes the residue a known quantity rather than an accident. | +| C-4 | **28** | **Say whether eligibility is per distribution or per address, and when it is evaluated** | A flag set in advance and a check performed at payout behave differently: an address frozen between the record date and the claim is eligible under one reading and not the other. `IncomeVault` evaluates at payout, through the same pause / freeze / RuleEngine path a transfer takes. | +| C-5 | **29** | **Forbid, or define, topping up a distribution whose claiming is already open** | Nothing in 29 prevents a second deposit after holders have begun claiming. Those who already claimed took their share of the smaller amount; those who had not take a share of the larger. The specification should either forbid it or define the accounting. | +| C-6 | **27** + debt attributes | **Reconcile the settlement token: per distribution, or per instrument?** | Functionality 27 puts it per distribution; `DebtInstrument.currencyContract` puts it per instrument, and is already an `address` rather than a string. These are two different data models for the same thing, in one framework. | +| C-7 | **31, 32** | **Specify them as derived from the Snapshot and Debt modules**, not as a store of their own | The dates already exist as `uint256[]` in the Snapshot module and the terms as strings in the Debt module. Saying so keeps one source of truth for record dates; implying a third store invites a schedule that disagrees with the snapshots the balances are actually read from. | +| C-8 | Debt attributes | **Give the schedule fields a machine-readable form**, alongside the descriptive strings | `couponPaymentFrequency`, `interestScheduleFormat` and `interestPaymentDate` are prose. Any automation — a keeper funding coupons, a contract asserting the next payment date — must parse them off-chain and be trusted. An optional structured form would make 31 executable without removing the human-readable one. | +| C-9 | **27, 29** | **Say whether the deposit must be held as the settlement token**, and what `amount` means when that token is itself a vault share | Nothing constrains an implementation to hold the deposit idle, or the settlement token to be a plain ERC-20. Holding the float as ERC-4626 shares turns a fixed nominal obligation into one the contract may be unable to pay in full; distributing a vault share leaves "amount" ambiguous between shares and assets, with opposite rounding conventions on the two sides. See [*Holding a distribution deposit in an ERC-4626 vault*](./CMTAT-Distribution-ERC4626.md). | + +**C-1 and C-5 can cause value to move incorrectly.** A record date resolved against balances that were never fixed pays out on balances anyone can still change (C-1); a distribution topped up after claiming has opened pays two holders different rates for the same entitlement (C-5). The remaining seven leave a question open rather than a door: they permit two conforming implementations to answer differently, which is a problem for an issuer comparing them and for a holder reading one. diff --git a/doc/cmtat-standard/CMTAT-Distribution-Amendments.pdf b/doc/cmtat-standard/CMTAT-Distribution-Amendments.pdf new file mode 100644 index 0000000..05ca13e Binary files /dev/null and b/doc/cmtat-standard/CMTAT-Distribution-Amendments.pdf differ diff --git a/doc/cmtat-standard/CMTAT-Distribution-ERC4626.md b/doc/cmtat-standard/CMTAT-Distribution-ERC4626.md new file mode 100644 index 0000000..89cdc74 --- /dev/null +++ b/doc/cmtat-standard/CMTAT-Distribution-ERC4626.md @@ -0,0 +1,50 @@ +# Holding a distribution deposit in an ERC-4626 vault + +Between functionality 29 (the issuer sends the deposit) and functionality 30 (holders claim it), the settlement tokens sit idle in the contract — for a coupon with a long claim period, potentially months. Whether an implementation may put that float to work, and what happens if it does, is a question the CMTA framework functional specifications (June 2026) do not answer. + +This document is that question in full. It is the evidence behind amendment **C-9** in `CMTAT-Distribution-Amendments`; ,the three additions the specification does not describe at all are in `CMTAT-Distribution-Additions` + +Why `IncomeVault` is not itself an ERC-4626 vault is a different question, answered in IncomeVault specification. This document is only about what the **deposit** is held as. + +## The specification text this refers to + +Quoted from section 3.2.4 of [`cmtat-framework-functional-specifications-june-2026.pdf`](./cmtat-framework-functional-specifications-june-2026.pdf), so the question can be read without opening it. The float exists in the window between 29 and 30, and 27 is what names the token it is held in. + +> 27. **Distribution create parameters**: Define settlement token (i.e. the token that is to be distributed), identify a (past or future) block time/height for distribution snapshot, and amount to be distributed. +> 29. **Distribution set deposit**: Send deposit amount for claiming settlement tokens to token holders flagged as eligible. +> 30. **Distribution claim deposit**: Allow token holders to claim their share of a deposit, identified by a deposit Identification, according to the token balance at the snapshot created at the defined time/height. + +None of the three says what form the deposit is held in between 29 and 30, nor constrains the settlement token of 27 to be a plain ERC-20. + +## The ERC-4626 requirements this refers to + +Quoted from [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626), as reproduced in the OpenZeppelin `IERC4626` interface this project builds against. These four are the ones that decide the question. + +> **`asset()`** — Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing. +> - MUST be an ERC-20 token contract. +> - MUST NOT revert. + +> **`totalAssets()`** — Returns the total amount of the underlying asset that is "managed" by Vault. +> - SHOULD include any compounding that occurs from yield. + +> **`previewRedeem(shares)`** — Allows an on-chain or off-chain user to simulate the effects of their redemption at the current block, given current on-chain conditions. +> - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call in the same transaction. +> - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. +> +> NOTE: any unfavorable discrepancy between `convertToAssets` and `previewRedeem` SHOULD be considered slippage in share price or some other type of condition, meaning **the depositor will lose assets by redeeming**. + +> **`redeem(shares, receiver, owner)`** — Burns exactly shares from owner and sends assets of underlying tokens to receiver. + +`totalAssets()` including yield is the whole attraction. The other three are why the naive version is unsafe: redemption is the only way out and it burns, the amount out is bounded above but never below, and the standard's own note says the depositor can lose assets by redeeming. + +## The question + +Functionality 29 has the issuer send the deposit, and 30 has holders claim it later. Between those two the settlement tokens sit idle in the contract — for a coupon with a long claim period, potentially months. An implementation could hold that float as shares of an [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) vault and redeem on each payout, so undistributed funds earn yield instead of nothing. + +The specification says nothing about this, and it should, because the naive version is unsafe. + +**The obligation is a fixed nominal amount; 4626 shares are not.** At `deposit` the issuer records an amount owed per holder. Shares carry share-price risk, so a loss in the underlying vault leaves the contract unable to pay what it recorded. A bookkeeping contract becomes one that can run a deficit, and the holders who claim last absorb it. Doing this safely needs a buffer policy and an explicit rule for who covers a shortfall — a materially larger design than section 3.2.4 describes. `IncomeVault` deliberately does not implement it; the reasoning is in [`doc/README.md`](../README.md#comparison-with-erc-4626--erc-7540-vaults). + +**A separate case: the settlement token *is* a vault share.** Nothing stops an issuer distributing a yield-bearing token — `DebtInstrument.currencyContract` can point at an ERC-4626 vault, and functionality 27's settlement token has no constraint. Then "amount to be distributed" is ambiguous: shares or assets? A share-denominated obligation is fixed in shares and floats in value; an asset-denominated one is the reverse, and requires converting at some moment the specification would have to name. Rounding direction matters here too, since 4626 rounds in the vault's favour by design and a claim rounds in the issuer's. This is amendment **C-9**. + +**What is worth stating either way:** whether the deposit for a distribution must be held as the settlement token itself, or may be held in another form and converted at payout. The answer decides whether a shortfall is possible at all, and it is invisible to a holder reading the contract. diff --git a/doc/cmtat-standard/CMTAT-Distribution-impl.md b/doc/cmtat-standard/CMTAT-Distribution-impl.md new file mode 100644 index 0000000..4de5292 --- /dev/null +++ b/doc/cmtat-standard/CMTAT-Distribution-impl.md @@ -0,0 +1,130 @@ +# The CMTAT Distribution module, as implemented by `IncomeVault` + +> The contracts are **NOT audited**. Do not use them in production without an audit. + +The CMTA framework functional specifications (June 2026) describe an optional **Distribution module** in section 3.2.4, with functionalities numbered 27 to 32 — see [`cmtat-framework-functional-specifications-june-2026.pdf`](./cmtat-framework-functional-specifications-june-2026.pdf). + +This document is the long form of the comparison summarised in [`doc/README.md`](../README.md#coverage-of-the-cmtat-distribution-module). It states which parts of the module `IncomeVault` covers, which it answers differently, which it does not implement, what it adds that the specification does not describe, and the changes we would propose to the specification as a result of implementing it. + +## Coverage, functionality by functionality + +| # | Specification | Status | In `IncomeVault` | +| --- | --- | --- | --- | +| 27 | **Distribution create parameters** — settlement token, a past or future block time/height for the snapshot, and the amount | ◑ partial | The `time` and the amount are per distribution: `deposit(time, amount)`. The **settlement token is not** — `ERC20TokenPayment` is fixed once at `initialize` for the whole vault. One vault distributes one token. | +| 28 | **Distribution set eligibility** — flag a user's tokens eligible or non-eligible, default eligible | ◑ different mechanism | No per-distribution flag. Eligibility is evaluated **at payout time** by `IncomeVaultValidationModule`: pause state, address freeze, and an optional `IRuleEngine.canTransfer`. Default is eligible. | +| 29 | **Distribution set deposit** — send the deposit for claiming by eligible holders | ● implemented | `deposit(time, amount)`, and `depositBatch(times, amounts)` for several dates in one transaction. Gated by `_authorizeDeposit`. Funds are segregated per `time`. | +| 30 | **Distribution claim deposit** — holders claim their share of a deposit, identified by a deposit id, per the snapshot balance | ● implemented | `claimDividend(time)` / `claimDividendBatch(times)`. The deposit id **is** the `time`. Share is `balance * segregatedDividend[time] / totalSupply`, rounded down. | +| 31 | **Distribution schedule** *(debt instruments)* — a schedule for interest payments and repayment at maturity | ○ not implemented | Each distribution is funded explicitly. `depositBatch` funds several dates in one call but creates no recurring schedule. The parameters for one already exist elsewhere in the framework — see [below](#functionalities-31-and-32-need-no-new-state). | +| 32 | **Distribution unschedule** *(debt instruments)* — cancel that schedule | ○ not implemented | Nothing to cancel here. On the Snapshot module it already exists as `unscheduleSnapshotNotOptimized(time)`; in the vault the nearest operations are `setStatusClaim(time, false)` and `withdraw` / `withdrawAll`. | + +Legend: ● implemented, ◑ partial or answered differently, ○ not implemented. + +## The two gaps + +**One settlement token per vault (27).** The specification lets each distribution name its own settlement token; here the token is chosen at deployment. An issuer distributing in two currencies deploys two vaults. This is a deliberate simplification, not an oversight: making the token per-`time` would put a second address in every accounting entry and make `withdrawAll` ambiguous about which balance it sweeps. + +**No payment schedule (31, 32).** Both are listed by the specification as *additional* use cases for debt instruments, and both belong more naturally beside the Debt module than beside distribution: a coupon schedule is derived from the instrument's terms, which `IncomeVault` does not hold. The vault is the settlement half — given a date and an amount, it segregates, restricts and pays. Scheduling is left to whatever produces those dates. + +## What `IncomeVault` adds beyond the specification + +The specification defines the minimum; several behaviours here have no counterpart in it and are answers to questions it leaves open. + +| Capability | Why it exists | +| --- | --- | +| **Claim window** — `setStatusClaim`, `timeLimitToWithdraw`, `validateTime(Code\|Batch)` | The specification says when a holder becomes entitled, not for how long. Claims are refused before `time` (the snapshot would not exist, so balances would be read live and be wrong) and after `time + timeLimitToWithdraw`. | +| **Issuer recovery** — `withdraw`, `withdrawAll` | The specification is silent on unclaimed funds. Rounding dust and unclaimed shares would otherwise be locked forever. Bounded per period by `unclaimedDividend(time)`. | +| **Push distribution** — `distributeDividend`, `distributeDividendBestEffort` | Lets the issuer pay holders who never transact. The best-effort variant skips a blocked holder instead of reverting the batch. | +| **Claim delegation** — ERC-7540 `setOperator`, ERC-7741 signed authorisation | A holder who cannot pay gas, or cannot transact at all, can still be paid. Payouts always go to the holder. | +| **Per-period accounting** — `segregatedDividend`, `paidDividend`, `unclaimedDividend`, `openClaimCount` | Makes "how much of this period is left" answerable on-chain, which the pro-rata denominator alone cannot answer. | +| **Restricted payouts** | The specification treats eligibility as a flag; here a payout is a transfer and passes the same pause / freeze / RuleEngine checks a token transfer would. | + +## Functionalities 31 and 32 need no new state + +Both look like missing features, but the parameters a payment schedule needs **already exist** in the CMTAT framework — split across two other modules. What is missing is the link between them, and in one case the type. + +| A schedule needs | Where it already lives | Type | +| --- | --- | --- | +| The dates themselves | `SnapshotEngine.getAllSnapshots()` / `getNextSnapshots()` | `uint256[]` — **machine-readable** | +| Coupon frequency | `ICMTATDebt.DebtInstrument.couponPaymentFrequency` | `string` | +| Accrual schedule (formats A / B / C) | `DebtInstrument.interestScheduleFormat` | `string` | +| Payment date, when it differs from accrual | `DebtInstrument.interestPaymentDate` | `string` | +| Maturity, for the final repayment | `DebtInstrument.maturityDate` | `string` | +| Rate and par value | `DebtInstrument.interestRate`, `parValue` | `uint256` | +| **Settlement token** | `DebtInstrument.currencyContract` | `address` | + +Two consequences follow. + +**The Debt module already holds the terms, but as prose.** Every schedule field is a `string`. That is enough for a human, a prospectus or an off-chain agent, and it is deliberate — the CMTA formats A/B/C are descriptive. It is *not* enough for a contract: nothing can iterate `interestScheduleFormat` to learn that a payment falls due next Tuesday. So the Debt module can carry the parameters of functionality 31 today, and the CMTAT reference implementation would need no new storage — but a contract cannot act on them. + +**The Snapshot module already holds the dates, and in the right type.** `getNextSnapshots()` returns the scheduled record dates as sorted `uint256` timestamps. That *is* the executable half of a distribution schedule, and it already exists on-chain. Functionality **32 (unschedule)** maps almost exactly onto `unscheduleSnapshotNotOptimized(time)`: cancelling the record date cancels the distribution, as long as nothing has been deposited for it. + +So the proposal is not "add a scheduler" — see amendment C-7 in *Changes we would propose to the standard*. It is: **specify 31 and 32 as derived rather than stored** — the dates are the scheduled snapshots, the terms are the debt attributes, and a distribution schedule is the join of the two plus an amount per date. That keeps one source of truth for record dates, which matters because a second list could disagree with the snapshots the balances are actually read from. + +`IncomeVault` implements neither, and does not read the schedule at all: `ISnapshotSource` is deliberately the three balance-reading functions and nothing else. + +## Where the module lives + +The specification presents Distribution as an **optional module of a CMTAT**. This project ships it as a separate contract, so an existing token needs no upgrade and the distribution logic can be redeployed independently. Since the modularity work it is also embeddable: a CMTAT with internal snapshots can inherit `IncomeVaultOpen` and `IncomeVaultRestricted` and pay its own dividends, with no second contract — see [*Embedding the distribution logic in a token*](../README.md#embedding-the-distribution-logic-in-a-token). Both shapes use the same `IIncomeVault` API. + +Note also that the vault **never schedules snapshots**. Specification functionalities 15 to 17 (schedule / reschedule / unschedule) belong to the Snapshot module; the vault only reads, through the three functions of `ISnapshotSource`. The issuer schedules the snapshot on the snapshot source, then deposits for the same `time`. + +## Eligibility without a snapshot + +Section 3.2.4 ties distribution to the Snapshot module: functionality 27 identifies "a (past or future) block time/height for distribution snapshot", and 30 pays "according to the token balance at the snapshot created at the defined time/height". Snapshots are the right default — they are on-chain, verifiable by anyone, and need no trusted party. They should not be the only permitted mechanism. + +A record date fixes *which balances count*. Taking an on-chain snapshot is one way to answer that; it is not the only one, and for some issuers it is the wrong one: + +- **Balances pinned off-chain at a block height.** The register is read at a block, the entitlement computed off-chain, and the result published — as a list written into a contract, or as a merkle root claimed against. Nothing is snapshotted on-chain, yet the record date is exactly as well defined, because a block height is immutable. +- **Registers that are not fully on-chain.** Where part of the holder base is held through a custodian or a book-entry register, the eligible set is not the token balance and no on-chain snapshot can produce it. +- **Entitlements that are not proportional to balance.** Different share classes, a cap per holder, or a withholding rate that varies by jurisdiction. The pro-rata assumption is the Snapshot module's, not the issuer's. + +**What the specification should say.** Keep the snapshot as the recommended binding, and restate 27 and 30 in terms of a *record-date balance source* that a snapshot satisfies — rather than naming the Snapshot module as the mechanism. The obligation that matters is not "a snapshot exists" but "the balances used are fixed at the record date and cannot change afterwards". That is what makes the payout reproducible, and an off-chain pinning at a block height satisfies it as completely as a snapshot does. This is why amendment **C-1** is phrased against a resolved balance source rather than against a snapshot. + +**Where `IncomeVault` sits.** It is already source-agnostic in principle: it never calls the token, only `ISnapshotSource`, and the guide is explicit that any contract implementing those three functions works. The limit is in the shape rather than the coupling. `snapshotInfo(time, holder)` must answer **on-chain, from stored state**, and `claimDividend(uint256 time)` carries no proof argument. So: + +- balances **computed** off-chain and then **written** into a contract implementing `ISnapshotSource` work today, unchanged; +- a **merkle root** claimed against does not, because the proof would have to travel with the claim — that needs `claimDividend(time, amount, proof)`, a different entry point, not a different source. + +A specification that permits both should say which of the two it means, since they imply different claim signatures. + +## Holding the deposit in an ERC-4626 vault + +Between functionality 29 and functionality 30 the settlement tokens sit idle, potentially for months. An implementation could hold that float as [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) shares and redeem on each payout, so undistributed funds earn yield instead of nothing — but the obligation recorded at `deposit` is a fixed nominal amount and shares are not, so the naive version can leave the contract unable to pay what it recorded. A separate case is the settlement token *being* a vault share, which leaves "amount to be distributed" ambiguous between shares and assets. + +The specification says nothing about either, and it should. The full question, with the specification text and the ERC-4626 requirements that decide it, is in [`CMTAT-Distribution-ERC4626.md`](./CMTAT-Distribution-ERC4626.md); the proposal it supports is amendment **C-9**. `IncomeVault` deliberately implements neither. + +## Changes we would propose to the standard + +Twelve proposals, of two kinds, kept apart because they carry different weight. An **amendment** constrains or clarifies a functionality the specification already defines, and a conforming implementation may already satisfy it — the specification just does not say so. An **addition** describes behaviour with no counterpart in section 3.2.4 at all, so no implementation can be conforming or non-conforming today; each one is a gap every implementer has had to fill privately. + +Old proposal 2 is split across the two tables: capping the claim period amends functionality 30, but recovering what is left afterwards is a new operation. + +### Amendments to functionalities already in the specification + +The nine amendments — C-1 to C-9, each naming the functionality it changes — are in [`CMTAT-Distribution-Amendments.md`](./CMTAT-Distribution-Amendments.md), so a reader taking them to the specification is not carrying the whole comparison with them. In short: + +| id | Amends | Proposal | +| --- | --- | --- | +| C-1 | 27, 30 | Require a record date to resolve against balances that are already fixed, and reject one that is not | +| C-2 | 30 | Cap the claim period with a deadline after which a claim is refused | +| C-3 | 30 | State the rounding direction for a holder's share | +| C-4 | 28 | Say whether eligibility is per distribution or per address, and when it is evaluated | +| C-5 | 29 | Forbid, or define, topping up a distribution whose claiming is already open | +| C-6 | 27 + debt attributes | Reconcile the settlement token: per distribution, or per instrument? | +| C-7 | 31, 32 | Specify them as derived from the Snapshot and Debt modules, not as a store of their own | +| C-8 | Debt attributes | Give the schedule fields a machine-readable form alongside the descriptive strings | +| C-9 | 27, 29 | Say whether the deposit must be held as the settlement token, and what `amount` means when that token is itself a vault share | + +**C-1 and C-5 can cause value to move incorrectly**; the other seven leave a question open. The reasoning for each is in the linked document. + +### Additions — behaviour the specification does not describe at all + +The three additions — A-1 to A-3 — are in [`CMTAT-Distribution-Additions.md`](./CMTAT-Distribution-Additions.md), so a reader taking them to the specification is not carrying the whole comparison with them. In short: + +| id | Proposal | +| --- | --- | +| A-1 | Recovery of what is not claimed, once the claim period has closed | +| A-2 | A push counterpart to the pull claim of 30 | +| A-3 | Delegated claiming: who may claim on a holder's behalf | + +Neither list is a defect report against an implementation: the amendments say where two conforming implementations may legitimately disagree, the additions say what the specification leaves unanswered although an issuer must answer it to discharge a real obligation. The reasoning for each id is in the linked documents. diff --git a/doc/cmtat-standard/cmtat-framework-functional-specifications-june-2026.pdf b/doc/cmtat-standard/cmtat-framework-functional-specifications-june-2026.pdf new file mode 100644 index 0000000..00ffe8a Binary files /dev/null and b/doc/cmtat-standard/cmtat-framework-functional-specifications-june-2026.pdf differ diff --git a/doc/coverage/README.md b/doc/coverage/README.md new file mode 100644 index 0000000..532d2fd --- /dev/null +++ b/doc/coverage/README.md @@ -0,0 +1,28 @@ +# Coverage report + +Generated output. **Do not edit** — regenerate it instead: + +```bash +make coverage-report # HTML here, needs lcov/genhtml +make coverage # summary table in the terminal, no extra tooling +``` + +`make coverage-report` deletes and recreates this directory, so this note is copied back in by the Makefile from `doc/script/coverage-README.md`. Edit it there. + +This report is **committed**, so it is only as trustworthy as its last run: regenerate it in the same commit as any change under `src/`. A tracked report describing a different codebase is worse than no report at all. + +## Reading the numbers + +Two files report **0%** and that is expected, not a gap: `IncomeVaultSnapshotCore` and `IncomeVaultValidationCore` declare hooks with **no bodies**. There is no code in them to execute, so nothing can cover them. They exist to be inherited and answered elsewhere. + +Function coverage is the lowest figure and the least useful one here. It counts `internal` helpers and the `_authorize*` overrides — empty bodies whose whole purpose is to carry a modifier — so a payout path exercised end to end still leaves several "uncovered" functions behind. + +## Scope + +`src/` only. Tests, mocks and `script/` are excluded, via: + +``` +forge coverage --ffi --exclude-tests --no-match-coverage '(test|mocks?|script)/' +``` + +Without `--ffi` every test fails: the OpenZeppelin Upgrades plugin shells out to `@openzeppelin/upgrades-core`. diff --git a/doc/test/coverage/amber.png b/doc/coverage/amber.png similarity index 100% rename from doc/test/coverage/amber.png rename to doc/coverage/amber.png diff --git a/doc/test/coverage/emerald.png b/doc/coverage/emerald.png similarity index 100% rename from doc/test/coverage/emerald.png rename to doc/coverage/emerald.png diff --git a/doc/test/coverage/gcov.css b/doc/coverage/gcov.css similarity index 100% rename from doc/test/coverage/gcov.css rename to doc/coverage/gcov.css diff --git a/doc/test/coverage/glass.png b/doc/coverage/glass.png similarity index 100% rename from doc/test/coverage/glass.png rename to doc/coverage/glass.png diff --git a/doc/test/coverage/index-sort-b.html b/doc/coverage/index-sort-b.html similarity index 59% rename from doc/test/coverage/index-sort-b.html rename to doc/coverage/index-sort-b.html index ddbe342..143af66 100644 --- a/doc/test/coverage/index-sort-b.html +++ b/doc/coverage/index-sort-b.html @@ -31,27 +31,27 @@ lcov.info Lines: - 101 - 136 - 74.3 % + 391 + 408 + 95.8 % Date: - 2023-11-21 13:10:43 + 2026-08-31 13:21:44 Functions: - 31 - 39 - 79.5 % + 100 + 113 + 88.5 % Branches: - 42 - 48 - 87.5 % + 57 + 58 + 98.3 % @@ -82,64 +82,52 @@ Branches Sort by branch coverage - script + src/modules -
0.0%
+
96.8%96.8%
- 0.0 % - 0 / 28 - 0.0 % - 0 / 2 - 0.0 % - 0 / 2 + 96.8 % + 209 / 216 + 88.5 % + 46 / 52 + 97.7 % + 42 / 43 src -
95.9%95.9%
- - 95.9 % - 47 / 49 - 92.3 % - 12 / 13 - 90.0 % - 18 / 20 - - - src/rules - -
96.3%96.3%
+
81.8%81.8%
- 96.3 % - 52 / 54 - 89.5 % - 17 / 19 - 92.3 % - 24 / 26 + 81.8 % + 9 / 11 + 75.0 % + 3 / 4 + - + 0 / 0 - test/utils + src/deployment -
33.3%33.3%
+
93.0%93.0%
- 33.3 % - 1 / 3 - 33.3 % - 1 / 3 - - - 0 / 0 + 93.0 % + 53 / 57 + 93.3 % + 28 / 30 + 100.0 % + 2 / 2 - src/modules + src/public -
50.0%50.0%
+
96.8%96.8%
- 50.0 % - 1 / 2 - 50.0 % - 1 / 2 - - - 0 / 0 + 96.8 % + 120 / 124 + 85.2 % + 23 / 27 + 100.0 % + 13 / 13 diff --git a/doc/test/coverage/index-sort-f.html b/doc/coverage/index-sort-f.html similarity index 59% rename from doc/test/coverage/index-sort-f.html rename to doc/coverage/index-sort-f.html index db2b75a..13c9d65 100644 --- a/doc/test/coverage/index-sort-f.html +++ b/doc/coverage/index-sort-f.html @@ -31,27 +31,27 @@ lcov.info Lines: - 101 - 136 - 74.3 % + 391 + 408 + 95.8 % Date: - 2023-11-21 13:10:43 + 2026-08-31 13:21:44 Functions: - 31 - 39 - 79.5 % + 100 + 113 + 88.5 % Branches: - 42 - 48 - 87.5 % + 57 + 58 + 98.3 % @@ -82,64 +82,52 @@ Branches Sort by branch coverage - script - -
0.0%
- - 0.0 % - 0 / 28 - 0.0 % - 0 / 2 - 0.0 % - 0 / 2 - - - test/utils + src -
33.3%33.3%
+
81.8%81.8%
- 33.3 % - 1 / 3 - 33.3 % - 1 / 3 + 81.8 % + 9 / 11 + 75.0 % + 3 / 4 - 0 / 0 - src/modules + src/public -
50.0%50.0%
+
96.8%96.8%
- 50.0 % - 1 / 2 - 50.0 % - 1 / 2 - - - 0 / 0 + 96.8 % + 120 / 124 + 85.2 % + 23 / 27 + 100.0 % + 13 / 13 - src/rules + src/modules -
96.3%96.3%
+
96.8%96.8%
- 96.3 % - 52 / 54 - 89.5 % - 17 / 19 - 92.3 % - 24 / 26 + 96.8 % + 209 / 216 + 88.5 % + 46 / 52 + 97.7 % + 42 / 43 - src + src/deployment -
95.9%95.9%
+
93.0%93.0%
- 95.9 % - 47 / 49 - 92.3 % - 12 / 13 - 90.0 % - 18 / 20 + 93.0 % + 53 / 57 + 93.3 % + 28 / 30 + 100.0 % + 2 / 2 diff --git a/doc/test/coverage/index-sort-l.html b/doc/coverage/index-sort-l.html similarity index 59% rename from doc/test/coverage/index-sort-l.html rename to doc/coverage/index-sort-l.html index 1745e80..b746fae 100644 --- a/doc/test/coverage/index-sort-l.html +++ b/doc/coverage/index-sort-l.html @@ -31,27 +31,27 @@ lcov.info Lines: - 101 - 136 - 74.3 % + 391 + 408 + 95.8 % Date: - 2023-11-21 13:10:43 + 2026-08-31 13:21:44 Functions: - 31 - 39 - 79.5 % + 100 + 113 + 88.5 % Branches: - 42 - 48 - 87.5 % + 57 + 58 + 98.3 % @@ -82,64 +82,52 @@ Branches Sort by branch coverage - script - -
0.0%
- - 0.0 % - 0 / 28 - 0.0 % - 0 / 2 - 0.0 % - 0 / 2 - - - test/utils + src -
33.3%33.3%
+
81.8%81.8%
- 33.3 % - 1 / 3 - 33.3 % - 1 / 3 + 81.8 % + 9 / 11 + 75.0 % + 3 / 4 - 0 / 0 - src/modules + src/deployment -
50.0%50.0%
+
93.0%93.0%
- 50.0 % - 1 / 2 - 50.0 % - 1 / 2 - - - 0 / 0 + 93.0 % + 53 / 57 + 93.3 % + 28 / 30 + 100.0 % + 2 / 2 - src + src/public -
95.9%95.9%
+
96.8%96.8%
- 95.9 % - 47 / 49 - 92.3 % - 12 / 13 - 90.0 % - 18 / 20 + 96.8 % + 120 / 124 + 85.2 % + 23 / 27 + 100.0 % + 13 / 13 - src/rules + src/modules -
96.3%96.3%
+
96.8%96.8%
- 96.3 % - 52 / 54 - 89.5 % - 17 / 19 - 92.3 % - 24 / 26 + 96.8 % + 209 / 216 + 88.5 % + 46 / 52 + 97.7 % + 42 / 43 diff --git a/doc/test/coverage/index.html b/doc/coverage/index.html similarity index 59% rename from doc/test/coverage/index.html rename to doc/coverage/index.html index 83d96f4..907cdd4 100644 --- a/doc/test/coverage/index.html +++ b/doc/coverage/index.html @@ -31,27 +31,27 @@ lcov.info Lines: - 101 - 136 - 74.3 % + 391 + 408 + 95.8 % Date: - 2023-11-21 13:10:43 + 2026-08-31 13:21:44 Functions: - 31 - 39 - 79.5 % + 100 + 113 + 88.5 % Branches: - 42 - 48 - 87.5 % + 57 + 58 + 98.3 % @@ -81,65 +81,53 @@ Functions Sort by function coverage Branches Sort by branch coverage - - script - -
0.0%
- - 0.0 % - 0 / 28 - 0.0 % - 0 / 2 - 0.0 % - 0 / 2 - src -
95.9%95.9%
+
81.8%81.8%
- 95.9 % - 47 / 49 - 92.3 % - 12 / 13 - 90.0 % - 18 / 20 + 81.8 % + 9 / 11 + 75.0 % + 3 / 4 + - + 0 / 0 - src/modules + src/deployment -
50.0%50.0%
+
93.0%93.0%
- 50.0 % - 1 / 2 - 50.0 % - 1 / 2 - - - 0 / 0 + 93.0 % + 53 / 57 + 93.3 % + 28 / 30 + 100.0 % + 2 / 2 - src/rules + src/modules -
96.3%96.3%
+
96.8%96.8%
- 96.3 % - 52 / 54 - 89.5 % - 17 / 19 - 92.3 % - 24 / 26 + 96.8 % + 209 / 216 + 88.5 % + 46 / 52 + 97.7 % + 42 / 43 - test/utils + src/public -
33.3%33.3%
+
96.8%96.8%
- 33.3 % - 1 / 3 - 33.3 % - 1 / 3 - - - 0 / 0 + 96.8 % + 120 / 124 + 85.2 % + 23 / 27 + 100.0 % + 13 / 13 diff --git a/doc/test/coverage/ruby.png b/doc/coverage/ruby.png similarity index 100% rename from doc/test/coverage/ruby.png rename to doc/coverage/ruby.png diff --git a/doc/test/coverage/snow.png b/doc/coverage/snow.png similarity index 100% rename from doc/test/coverage/snow.png rename to doc/coverage/snow.png diff --git a/doc/test/coverage/script/CMTATWithRuleEngineScript.s.sol.func-sort-c.html b/doc/coverage/src/IncomeVaultBase.sol.func-sort-c.html similarity index 72% rename from doc/test/coverage/script/CMTATWithRuleEngineScript.s.sol.func-sort-c.html rename to doc/coverage/src/IncomeVaultBase.sol.func-sort-c.html index c2ad6e1..af50c25 100644 --- a/doc/test/coverage/script/CMTATWithRuleEngineScript.s.sol.func-sort-c.html +++ b/doc/coverage/src/IncomeVaultBase.sol.func-sort-c.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - script/CMTATWithRuleEngineScript.s.sol - functions + LCOV - lcov.info - src/IncomeVaultBase.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - - + + + - + - - + + @@ -65,12 +65,12 @@
Current view:top level - script - CMTATWithRuleEngineScript.s.sol (source / functions)top level - src - IncomeVaultBase.sol (source / functions) Hitlcov.info Lines:0160.0 %55100.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:0 10.0 %1100.0 %
- + - - + +

Function Name Sort by function nameFunction Name Sort by function name Hit count Sort by hit count
CMTATWithRuleEngineScript.run0IncomeVaultBase.__IncomeVaultBase_init_unchained288

diff --git a/doc/test/coverage/script/CMTATWithRuleEngineScript.s.sol.func.html b/doc/coverage/src/IncomeVaultBase.sol.func.html similarity index 72% rename from doc/test/coverage/script/CMTATWithRuleEngineScript.s.sol.func.html rename to doc/coverage/src/IncomeVaultBase.sol.func.html index ae44042..df4a1a7 100644 --- a/doc/test/coverage/script/CMTATWithRuleEngineScript.s.sol.func.html +++ b/doc/coverage/src/IncomeVaultBase.sol.func.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - script/CMTATWithRuleEngineScript.s.sol - functions + LCOV - lcov.info - src/IncomeVaultBase.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - - + + + - + - - + + @@ -66,11 +66,11 @@ - + - - + +
Current view:top level - script - CMTATWithRuleEngineScript.s.sol (source / functions)top level - src - IncomeVaultBase.sol (source / functions) Hitlcov.info Lines:0160.0 %55100.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:0 10.0 %1100.0 %

Function Name Sort by function nameHit count Sort by hit countHit count Sort by hit count
CMTATWithRuleEngineScript.run0IncomeVaultBase.__IncomeVaultBase_init_unchained288

diff --git a/doc/coverage/src/IncomeVaultBase.sol.gcov.html b/doc/coverage/src/IncomeVaultBase.sol.gcov.html new file mode 100644 index 0000000..1ccb952 --- /dev/null +++ b/doc/coverage/src/IncomeVaultBase.sol.gcov.html @@ -0,0 +1,155 @@ + + + + + + + LCOV - lcov.info - src/IncomeVaultBase.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src - IncomeVaultBase.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:55100.0 %
Date:2026-08-31 13:21:44Functions:11100.0 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
+       7                 :            : import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
+       8                 :            : import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+       9                 :            : /* ==== CMTAT === */
+      10                 :            : /* ==== Snapshot === */
+      11                 :            : import {ISnapshotSource} from "./interfaces/ISnapshotSource.sol";
+      12                 :            : /* ==== IncomeVault === */
+      13                 :            : import {IncomeVaultValidationCore} from "./modules/IncomeVaultValidationCore.sol";
+      14                 :            : import {IncomeVaultSnapshotModule} from "./modules/IncomeVaultSnapshotModule.sol";
+      15                 :            : import {IncomeVaultRestricted} from "./public/IncomeVaultRestricted.sol";
+      16                 :            : import {IncomeVaultOpen} from "./public/IncomeVaultOpen.sol";
+      17                 :            : import {VersionModule} from "./modules/VersionModule.sol";
+      18                 :            : 
+      19                 :            : /**
+      20                 :            :  * @title Income Vault to distribute dividends — logic shared by every deployment variant
+      21                 :            :  * @dev
+      22                 :            :  * The vault is not bound to a specific token implementation: the holder balances and the total
+      23                 :            :  * supply are read through the {ISnapshotSource} interface, which is implemented by the CMTA
+      24                 :            :  * `SnapshotEngine` as well as by any token embedding an equivalent snapshot module.
+      25                 :            :  *
+      26                 :            :  * This contract holds **what** the vault does. It deliberately declares neither an access-control
+      27                 :            :  * policy nor a transfer-restriction policy: the `_authorize*` hooks and
+      28                 :            :  * {IncomeVaultValidationCore-_validateTransfer} are left abstract and answered by the deployment
+      29                 :            :  * contract, so the same logic ships role-based ({IncomeVault}) or single-owner
+      30                 :            :  * ({IncomeVaultOwnable2Step}) — and can be embedded in a host that answers them from its own modules.
+      31                 :            :  *
+      32                 :            :  * It also declares **no meta-transaction policy**. Gasless support is a deployment decision, exactly
+      33                 :            :  * like the access-control model: {IncomeVaultBaseERC2771} adds the ERC-2771 context on top of this
+      34                 :            :  * contract, and the two shipped deployments inherit that. A deployment that does not want a trusted
+      35                 :            :  * forwarder inherits this contract directly and pays for none of it. Finding M-8.
+      36                 :            :  */
+      37                 :            : abstract contract IncomeVaultBase is
+      38                 :            :     IncomeVaultValidationCore,
+      39                 :            :     Initializable,
+      40                 :            :     ContextUpgradeable,
+      41                 :            :     VersionModule,
+      42                 :            :     IncomeVaultSnapshotModule,
+      43                 :            :     IncomeVaultRestricted,
+      44                 :            :     IncomeVaultOpen
+      45                 :            : {
+      46                 :            :     /* ============  Initializer Function ============ */
+      47                 :            :     /**
+      48                 :            :      * @dev calls the initialize functions of the policy-agnostic modules
+      49                 :            :      * @param ERC20TokenPayment_ ERC20 token used to perform the payment
+      50                 :            :      * @param snapshotSource_ contract implementing {ISnapshotSource}, source of the holder balances
+      51                 :            :      * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted
+      52                 :            :      */
+      53                 :        288 :     function __IncomeVaultBase_init_unchained(
+      54                 :            :         IERC20 ERC20TokenPayment_,
+      55                 :            :         ISnapshotSource snapshotSource_,
+      56                 :            :         uint256 timeLimitToWithdraw_
+      57                 :            :     ) internal onlyInitializing {
+      58                 :        288 :         _setERC20TokenPayment(ERC20TokenPayment_);
+      59                 :        287 :         _setDividendSnapshotSource(snapshotSource_);
+      60                 :            : 
+      61                 :            :         // EIP-712 domain for the ERC-7741 signed operator authorisations. The version stays "1"
+      62                 :            :         // across releases on purpose: bumping it would invalidate every signature already issued.
+      63                 :        286 :         __EIP712_init_unchained("IncomeVault", "1");
+      64                 :        286 :         __IncomeVaultRestricted_init_unchained(timeLimitToWithdraw_);
+      65                 :            :     }
+      66                 :            : 
+      67                 :            :     /*//////////////////////////////////////////////////////////////
+      68                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+      69                 :            :     //////////////////////////////////////////////////////////////*/
+      70                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/test/coverage/script/RuleEngineScript.s.sol.func-sort-c.html b/doc/coverage/src/IncomeVaultBaseERC2771.sol.func-sort-c.html similarity index 65% rename from doc/test/coverage/script/RuleEngineScript.s.sol.func-sort-c.html rename to doc/coverage/src/IncomeVaultBaseERC2771.sol.func-sort-c.html index 600ff1d..e2f8ba6 100644 --- a/doc/test/coverage/script/RuleEngineScript.s.sol.func-sort-c.html +++ b/doc/coverage/src/IncomeVaultBaseERC2771.sol.func-sort-c.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - script/RuleEngineScript.s.sol - functions + LCOV - lcov.info - src/IncomeVaultBaseERC2771.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - - + + + - + - - - + + + @@ -50,8 +50,8 @@ - - + +
Current view:top level - script - RuleEngineScript.s.sol (source / functions)top level - src - IncomeVaultBaseERC2771.sol (source / functions) Hitlcov.info Lines:0120.0 %4666.7 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:010.0 %2366.7 %
Branches: 020.0 %0-
@@ -65,13 +65,21 @@ - + - + + + + + + + + +

Function Name Sort by function nameFunction Name Sort by function name Hit count Sort by hit count
RuleEngineScript.runIncomeVaultBaseERC2771._msgData 0
IncomeVaultBaseERC2771._contextSuffixLength25001
IncomeVaultBaseERC2771._msgSender25001

diff --git a/doc/test/coverage/script/RuleEngineScript.s.sol.func.html b/doc/coverage/src/IncomeVaultBaseERC2771.sol.func.html similarity index 65% rename from doc/test/coverage/script/RuleEngineScript.s.sol.func.html rename to doc/coverage/src/IncomeVaultBaseERC2771.sol.func.html index 5a83124..9d39333 100644 --- a/doc/test/coverage/script/RuleEngineScript.s.sol.func.html +++ b/doc/coverage/src/IncomeVaultBaseERC2771.sol.func.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - script/RuleEngineScript.s.sol - functions + LCOV - lcov.info - src/IncomeVaultBaseERC2771.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - - + + + - + - - - + + + @@ -50,8 +50,8 @@ - - + +
Current view:top level - script - RuleEngineScript.s.sol (source / functions)top level - src - IncomeVaultBaseERC2771.sol (source / functions) Hitlcov.info Lines:0120.0 %4666.7 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:010.0 %2366.7 %
Branches: 020.0 %0-
@@ -66,12 +66,20 @@
Function Name Sort by function name - Hit count Sort by hit count + Hit count Sort by hit count - RuleEngineScript.run + IncomeVaultBaseERC2771._contextSuffixLength + 25001 + + + IncomeVaultBaseERC2771._msgData 0 + + IncomeVaultBaseERC2771._msgSender + 25001 +
diff --git a/doc/coverage/src/IncomeVaultBaseERC2771.sol.gcov.html b/doc/coverage/src/IncomeVaultBaseERC2771.sol.gcov.html new file mode 100644 index 0000000..87d43f6 --- /dev/null +++ b/doc/coverage/src/IncomeVaultBaseERC2771.sol.gcov.html @@ -0,0 +1,169 @@ + + + + + + + LCOV - lcov.info - src/IncomeVaultBaseERC2771.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src - IncomeVaultBaseERC2771.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:4666.7 %
Date:2026-08-31 13:21:44Functions:2366.7 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
+       6                 :            : import {ERC2771ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol";
+       7                 :            : 
+       8                 :            : import {ERC2771Module} from "CMTAT/modules/wrapper/options/ERC2771Module.sol";
+       9                 :            : 
+      10                 :            : import {IncomeVaultBase} from "./IncomeVaultBase.sol";
+      11                 :            : 
+      12                 :            : /**
+      13                 :            :  * @title {IncomeVaultBase} plus gasless support (ERC-2771)
+      14                 :            :  * @dev
+      15                 :            :  * Meta-transaction support is a **deployment decision**, in the same way the access-control model and
+      16                 :            :  * the transfer-restriction policy are. {IncomeVaultBase} states what the vault does and knows nothing
+      17                 :            :  * about forwarders; this contract adds the ERC-2771 context and resolves the
+      18                 :            :  * `ERC2771ContextUpgradeable` / `ContextUpgradeable` diamond it creates. Both shipped deployments
+      19                 :            :  * inherit it, so their behaviour is unchanged.
+      20                 :            :  *
+      21                 :            :  * A deployment that does not want a trusted forwarder inherits {IncomeVaultBase} directly. That is the
+      22                 :            :  * point of the split (finding M-8): previously the forwarder came whether it was wanted or not, and
+      23                 :            :  * opting out meant passing the zero address while still carrying the code and the calldata suffix
+      24                 :            :  * handling on every call.
+      25                 :            :  *
+      26                 :            :  * @custom:security The forwarder is set in the constructor and is **immutable** — it lives in the
+      27                 :            :  * implementation's bytecode, not in proxy storage, so it survives an upgrade only if the new
+      28                 :            :  * implementation is deployed with the same address. A trusted forwarder can name any `_msgSender()`,
+      29                 :            :  * so it is as privileged as every role behind it.
+      30                 :            :  */
+      31                 :            : abstract contract IncomeVaultBaseERC2771 is IncomeVaultBase, ERC2771Module {
+      32                 :            :     /**
+      33                 :            :      * @param forwarderIrrevocable Address of the forwarder, required for the gasless support
+      34                 :            :      */
+      35                 :            :     /// @custom:oz-upgrades-unsafe-allow constructor
+      36                 :            :     constructor(address forwarderIrrevocable) ERC2771Module(forwarderIrrevocable) {}
+      37                 :            : 
+      38                 :            :     /*//////////////////////////////////////////////////////////////
+      39                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+      40                 :            :     //////////////////////////////////////////////////////////////*/
+      41                 :            :     /* ============ ERC-2771 / Context disambiguation ============ */
+      42                 :            :     /**
+      43                 :            :      * @dev Resolves the {ERC2771ContextUpgradeable} / {ContextUpgradeable} diamond in favour of the
+      44                 :            :      * ERC-2771 answer, so a forwarded call is attributed to the original sender.
+      45                 :            :      * @return sender the forwarded sender when the call came through the trusted forwarder
+      46                 :            :      */
+      47                 :      25001 :     function _msgSender()
+      48                 :            :         internal
+      49                 :            :         view
+      50                 :            :         virtual
+      51                 :            :         override(ERC2771ContextUpgradeable, ContextUpgradeable)
+      52                 :            :         returns (address sender)
+      53                 :            :     {
+      54                 :      25001 :         return ERC2771ContextUpgradeable._msgSender();
+      55                 :            :     }
+      56                 :            : 
+      57                 :            :     /**
+      58                 :            :      * @dev Resolves the same diamond for the calldata, stripping the appended sender suffix.
+      59                 :            :      * @return The calldata with the ERC-2771 suffix removed
+      60                 :            :      */
+      61                 :          0 :     function _msgData()
+      62                 :            :         internal
+      63                 :            :         view
+      64                 :            :         virtual
+      65                 :            :         override(ERC2771ContextUpgradeable, ContextUpgradeable)
+      66                 :            :         returns (bytes calldata)
+      67                 :            :     {
+      68                 :          0 :         return ERC2771ContextUpgradeable._msgData();
+      69                 :            :     }
+      70                 :            : 
+      71                 :            :     /**
+      72                 :            :      * @dev Resolves the same diamond for the length of that suffix.
+      73                 :            :      * @return The number of trailing calldata bytes carrying the forwarded sender
+      74                 :            :      */
+      75                 :      25001 :     function _contextSuffixLength()
+      76                 :            :         internal
+      77                 :            :         view
+      78                 :            :         virtual
+      79                 :            :         override(ERC2771ContextUpgradeable, ContextUpgradeable)
+      80                 :            :         returns (uint256)
+      81                 :            :     {
+      82                 :      25001 :         return ERC2771ContextUpgradeable._contextSuffixLength();
+      83                 :            :     }
+      84                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/deployment/IncomeVault.sol.func-sort-c.html b/doc/coverage/src/deployment/IncomeVault.sol.func-sort-c.html new file mode 100644 index 0000000..c094759 --- /dev/null +++ b/doc/coverage/src/deployment/IncomeVault.sol.func-sort-c.html @@ -0,0 +1,141 @@ + + + + + + + LCOV - lcov.info - src/deployment/IncomeVault.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deployment - IncomeVault.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:262892.9 %
Date:2026-08-31 13:21:44Functions:141593.3 %
Branches:11100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVault._msgData0
IncomeVault._authorizeSnapshotSourceManagement6
IncomeVault.supportsInterface8
IncomeVault._authorizeDeactivate9
IncomeVault._authorizeRuleEngineManagement19
IncomeVault.constructor238
IncomeVault.initialize238
IncomeVault._authorizeWithdraw1224
IncomeVault._authorizeFreeze2083
IncomeVault._authorizeOperator2096
IncomeVault._authorizeDeposit2257
IncomeVault._authorizePause2393
IncomeVault._authorizeDistribute4454
IncomeVault._contextSuffixLength24949
IncomeVault._msgSender24949
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/deployment/IncomeVault.sol.func.html b/doc/coverage/src/deployment/IncomeVault.sol.func.html new file mode 100644 index 0000000..3db7904 --- /dev/null +++ b/doc/coverage/src/deployment/IncomeVault.sol.func.html @@ -0,0 +1,141 @@ + + + + + + + LCOV - lcov.info - src/deployment/IncomeVault.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deployment - IncomeVault.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:262892.9 %
Date:2026-08-31 13:21:44Functions:141593.3 %
Branches:11100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVault._authorizeDeactivate9
IncomeVault._authorizeDeposit2257
IncomeVault._authorizeDistribute4454
IncomeVault._authorizeFreeze2083
IncomeVault._authorizeOperator2096
IncomeVault._authorizePause2393
IncomeVault._authorizeRuleEngineManagement19
IncomeVault._authorizeSnapshotSourceManagement6
IncomeVault._authorizeWithdraw1224
IncomeVault._contextSuffixLength24949
IncomeVault._msgData0
IncomeVault._msgSender24949
IncomeVault.constructor238
IncomeVault.initialize238
IncomeVault.supportsInterface8
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/deployment/IncomeVault.sol.gcov.html b/doc/coverage/src/deployment/IncomeVault.sol.gcov.html new file mode 100644 index 0000000..444fbcf --- /dev/null +++ b/doc/coverage/src/deployment/IncomeVault.sol.gcov.html @@ -0,0 +1,256 @@ + + + + + + + LCOV - lcov.info - src/deployment/IncomeVault.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deployment - IncomeVault.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:262892.9 %
Date:2026-08-31 13:21:44Functions:141593.3 %
Branches:11100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+       7                 :            : import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
+       8                 :            : import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
+       9                 :            : import {IERC7741} from "../interfaces/IERC7741.sol";
+      10                 :            : import {IIncomeVault} from "../interfaces/IIncomeVault.sol";
+      11                 :            : /* ==== CMTAT === */
+      12                 :            : import {AccessControlModule} from "CMTAT/modules/wrapper/security/AccessControlModule.sol";
+      13                 :            : import {PauseModule} from "CMTAT/modules/wrapper/core/PauseModule.sol";
+      14                 :            : import {EnforcementModule} from "CMTAT/modules/wrapper/core/EnforcementModule.sol";
+      15                 :            : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
+      16                 :            : /* ==== Snapshot === */
+      17                 :            : import {ISnapshotSource} from "../interfaces/ISnapshotSource.sol";
+      18                 :            : /* ==== IncomeVault === */
+      19                 :            : import {IncomeVaultBaseERC2771} from "../IncomeVaultBaseERC2771.sol";
+      20                 :            : import {IncomeVaultValidationModule} from "../modules/IncomeVaultValidationModule.sol";
+      21                 :            : import {IncomeVaultRestricted} from "../public/IncomeVaultRestricted.sol";
+      22                 :            : import {IncomeVaultSnapshotModule} from "../modules/IncomeVaultSnapshotModule.sol";
+      23                 :            : import {IncomeVaultValidationModule} from "../modules/IncomeVaultValidationModule.sol";
+      24                 :            : import {IncomeVaultRolesStorage} from "../storage/IncomeVaultRolesStorage.sol";
+      25                 :            : 
+      26                 :            : /**
+      27                 :            :  * @title Income Vault to distribute dividends — role-based deployment
+      28                 :            :  * @dev
+      29                 :            :  * Answers **who** may do what: every authorization hook of {IncomeVaultBase} is overridden with the
+      30                 :            :  * role that gates it. Suited to institutional operations, where funding the vault, withdrawing from
+      31                 :            :  * it and running the claim window are held by different accounts.
+      32                 :            :  *
+      33                 :            :  * Note the CMTAT `AccessControlModule` treats `DEFAULT_ADMIN_ROLE` as implicitly holding every role:
+      34                 :            :  * the admin passes every `hasRole` check but does **not** appear in role enumerations, so an
+      35                 :            :  * off-chain tool listing role holders will not see them. Role separation therefore constrains the
+      36                 :            :  * operators, never the admin.
+      37                 :            :  */
+      38                 :            : contract IncomeVault is
+      39                 :            :     IncomeVaultValidationModule,
+      40                 :            :     IncomeVaultBaseERC2771,
+      41                 :            :     AccessControlModule,
+      42                 :            :     IncomeVaultRolesStorage
+      43                 :            : {
+      44                 :            :     /**
+      45                 :            :      * @param forwarderIrrevocable Address of the forwarder, required for the gasless support
+      46                 :            :      */
+      47                 :            :     /// @custom:oz-upgrades-unsafe-allow constructor
+      48                 :        238 :     constructor(address forwarderIrrevocable) IncomeVaultBaseERC2771(forwarderIrrevocable) {
+      49                 :            :         // Disable the possibility to initialize the implementation
+      50                 :        238 :         _disableInitializers();
+      51                 :            :     }
+      52                 :            : 
+      53                 :            :     /**
+      54                 :            :      * @notice
+      55                 :            :      * initialize the proxy contract
+      56                 :            :      * The calls to this function will revert if the contract was deployed without a proxy
+      57                 :            :      * @param admin Address of the contract (Access Control)
+      58                 :            :      * @param ERC20TokenPayment_ ERC20 token used to perform the payment
+      59                 :            :      * @param snapshotSource_ contract implementing {ISnapshotSource}, source of the holder balances
+      60                 :            :      * @param ruleEngine_ optional RuleEngine applied to the payouts, or the zero address
+      61                 :            :      * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted
+      62                 :            :      */
+      63                 :        238 :     function initialize(
+      64                 :            :         address admin,
+      65                 :            :         IERC20 ERC20TokenPayment_,
+      66                 :            :         ISnapshotSource snapshotSource_,
+      67                 :            :         IRuleEngine ruleEngine_,
+      68                 :            :         uint256 timeLimitToWithdraw_
+      69                 :            :     ) public initializer {
+      70            [ + ]:        237 :         if (admin == address(0)) {
+      71                 :          1 :             revert IncomeVault_AdminWithAddressZeroNotAllowed();
+      72                 :            :         }
+      73                 :        236 :         __AccessControl_init_unchained();
+      74                 :        236 :         __AccessControlModule_init_unchained(admin);
+      75                 :            :         // the validation answer this deployment chose
+      76                 :        236 :         __Pausable_init_unchained();
+      77                 :        236 :         __IncomeVaultValidation_init_unchained(ruleEngine_);
+      78                 :        236 :         __IncomeVaultBase_init_unchained(ERC20TokenPayment_, snapshotSource_, timeLimitToWithdraw_);
+      79                 :            :     }
+      80                 :            : 
+      81                 :            :     /*//////////////////////////////////////////////////////////////
+      82                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+      83                 :            :     //////////////////////////////////////////////////////////////*/
+      84                 :            :     /* ============ ERC-165 ============ */
+      85                 :            :     /**
+      86                 :            :      * @notice ERC-165 interface detection
+      87                 :            :      * @dev Adds ERC-7741, whose specification requires a contract implementing it to answer `true`
+      88                 :            :      * for `0xa9e50872`. The ERC-7540 operator id is deliberately **not** advertised — this is not an
+      89                 :            :      * asynchronous vault; see {IERC7540Operator}.
+      90                 :            :      * @param interfaceId The interface identifier to check
+      91                 :            :      * @return True if the interface is supported, false otherwise
+      92                 :            :      */
+      93                 :          8 :     function supportsInterface(bytes4 interfaceId)
+      94                 :            :         public
+      95                 :            :         view
+      96                 :            :         virtual
+      97                 :            :         override(AccessControlUpgradeable)
+      98                 :            :         returns (bool)
+      99                 :            :     {
+     100                 :          8 :         return interfaceId == type(IIncomeVault).interfaceId || interfaceId == type(IERC7741).interfaceId
+     101                 :          6 :             || AccessControlUpgradeable.supportsInterface(interfaceId);
+     102                 :            :     }
+     103                 :            : 
+     104                 :            :     /* ============ ERC-2771 / Context disambiguation ============ */
+     105                 :            :     /**
+     106                 :            :      * @inheritdoc IncomeVaultBaseERC2771
+     107                 :            :      */
+     108                 :      24949 :     function _msgSender()
+     109                 :            :         internal
+     110                 :            :         view
+     111                 :            :         virtual
+     112                 :            :         override(IncomeVaultBaseERC2771, ContextUpgradeable)
+     113                 :            :         returns (address sender)
+     114                 :            :     {
+     115                 :      24949 :         return IncomeVaultBaseERC2771._msgSender();
+     116                 :            :     }
+     117                 :            : 
+     118                 :            :     /**
+     119                 :            :      * @inheritdoc IncomeVaultBaseERC2771
+     120                 :            :      */
+     121                 :          0 :     function _msgData()
+     122                 :            :         internal
+     123                 :            :         view
+     124                 :            :         virtual
+     125                 :            :         override(IncomeVaultBaseERC2771, ContextUpgradeable)
+     126                 :            :         returns (bytes calldata)
+     127                 :            :     {
+     128                 :          0 :         return IncomeVaultBaseERC2771._msgData();
+     129                 :            :     }
+     130                 :            : 
+     131                 :            :     /**
+     132                 :            :      * @inheritdoc IncomeVaultBaseERC2771
+     133                 :            :      */
+     134                 :      24949 :     function _contextSuffixLength()
+     135                 :            :         internal
+     136                 :            :         view
+     137                 :            :         virtual
+     138                 :            :         override(IncomeVaultBaseERC2771, ContextUpgradeable)
+     139                 :            :         returns (uint256)
+     140                 :            :     {
+     141                 :      24949 :         return IncomeVaultBaseERC2771._contextSuffixLength();
+     142                 :            :     }
+     143                 :            : 
+     144                 :            :     /* ============ Access Control ============ */
+     145                 :            :     /// @inheritdoc IncomeVaultRestricted
+     146                 :       2257 :     function _authorizeDeposit() internal view virtual override onlyRole(INCOME_VAULT_DEPOSIT_ROLE) {}
+     147                 :            : 
+     148                 :            :     /// @inheritdoc IncomeVaultRestricted
+     149                 :       1224 :     function _authorizeWithdraw() internal view virtual override onlyRole(INCOME_VAULT_WITHDRAW_ROLE) {}
+     150                 :            : 
+     151                 :            :     /// @inheritdoc IncomeVaultRestricted
+     152                 :       4454 :     function _authorizeDistribute() internal view virtual override onlyRole(INCOME_VAULT_DISTRIBUTE_ROLE) {}
+     153                 :            : 
+     154                 :            :     /// @inheritdoc IncomeVaultRestricted
+     155                 :       2096 :     function _authorizeOperator() internal view virtual override onlyRole(INCOME_VAULT_OPERATOR_ROLE) {}
+     156                 :            : 
+     157                 :            :     /// @inheritdoc IncomeVaultSnapshotModule
+     158                 :          6 :     function _authorizeSnapshotSourceManagement() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+     159                 :            : 
+     160                 :            :     /// @inheritdoc IncomeVaultValidationModule
+     161                 :         19 :     function _authorizeRuleEngineManagement() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+     162                 :            : 
+     163                 :            :     /// @inheritdoc PauseModule
+     164                 :       2393 :     function _authorizePause() internal view virtual override(PauseModule) onlyRole(PAUSER_ROLE) {}
+     165                 :            : 
+     166                 :            :     /// @inheritdoc PauseModule
+     167                 :          9 :     function _authorizeDeactivate() internal view virtual override(PauseModule) onlyRole(DEFAULT_ADMIN_ROLE) {}
+     168                 :            : 
+     169                 :            :     /// @inheritdoc EnforcementModule
+     170                 :       2083 :     function _authorizeFreeze() internal view virtual override(EnforcementModule) onlyRole(ENFORCER_ROLE) {}
+     171                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/deployment/IncomeVaultOwnable2Step.sol.func-sort-c.html b/doc/coverage/src/deployment/IncomeVaultOwnable2Step.sol.func-sort-c.html new file mode 100644 index 0000000..5663d1b --- /dev/null +++ b/doc/coverage/src/deployment/IncomeVaultOwnable2Step.sol.func-sort-c.html @@ -0,0 +1,141 @@ + + + + + + + LCOV - lcov.info - src/deployment/IncomeVaultOwnable2Step.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deployment - IncomeVaultOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:272993.1 %
Date:2026-08-31 13:21:44Functions:141593.3 %
Branches:11100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultOwnable2Step._msgData0
IncomeVaultOwnable2Step._authorizeDistribute1
IncomeVaultOwnable2Step._authorizeRuleEngineManagement1
IncomeVaultOwnable2Step._authorizeDeactivate2
IncomeVaultOwnable2Step._authorizeFreeze2
IncomeVaultOwnable2Step._authorizeSnapshotSourceManagement2
IncomeVaultOwnable2Step._authorizeDeposit3
IncomeVaultOwnable2Step._authorizeWithdraw4
IncomeVaultOwnable2Step._authorizePause5
IncomeVaultOwnable2Step.supportsInterface5
IncomeVaultOwnable2Step._authorizeOperator8
IncomeVaultOwnable2Step.constructor50
IncomeVaultOwnable2Step.initialize50
IncomeVaultOwnable2Step._contextSuffixLength52
IncomeVaultOwnable2Step._msgSender52
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/deployment/IncomeVaultOwnable2Step.sol.func.html b/doc/coverage/src/deployment/IncomeVaultOwnable2Step.sol.func.html new file mode 100644 index 0000000..386d74d --- /dev/null +++ b/doc/coverage/src/deployment/IncomeVaultOwnable2Step.sol.func.html @@ -0,0 +1,141 @@ + + + + + + + LCOV - lcov.info - src/deployment/IncomeVaultOwnable2Step.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deployment - IncomeVaultOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:272993.1 %
Date:2026-08-31 13:21:44Functions:141593.3 %
Branches:11100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultOwnable2Step._authorizeDeactivate2
IncomeVaultOwnable2Step._authorizeDeposit3
IncomeVaultOwnable2Step._authorizeDistribute1
IncomeVaultOwnable2Step._authorizeFreeze2
IncomeVaultOwnable2Step._authorizeOperator8
IncomeVaultOwnable2Step._authorizePause5
IncomeVaultOwnable2Step._authorizeRuleEngineManagement1
IncomeVaultOwnable2Step._authorizeSnapshotSourceManagement2
IncomeVaultOwnable2Step._authorizeWithdraw4
IncomeVaultOwnable2Step._contextSuffixLength52
IncomeVaultOwnable2Step._msgData0
IncomeVaultOwnable2Step._msgSender52
IncomeVaultOwnable2Step.constructor50
IncomeVaultOwnable2Step.initialize50
IncomeVaultOwnable2Step.supportsInterface5
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/deployment/IncomeVaultOwnable2Step.sol.gcov.html b/doc/coverage/src/deployment/IncomeVaultOwnable2Step.sol.gcov.html new file mode 100644 index 0000000..9ddca02 --- /dev/null +++ b/doc/coverage/src/deployment/IncomeVaultOwnable2Step.sol.gcov.html @@ -0,0 +1,254 @@ + + + + + + + LCOV - lcov.info - src/deployment/IncomeVaultOwnable2Step.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/deployment - IncomeVaultOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:272993.1 %
Date:2026-08-31 13:21:44Functions:141593.3 %
Branches:11100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+       7                 :            : import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
+       8                 :            : import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
+       9                 :            : /* ==== CMTAT === */
+      10                 :            : import {PauseModule} from "CMTAT/modules/wrapper/core/PauseModule.sol";
+      11                 :            : import {EnforcementModule} from "CMTAT/modules/wrapper/core/EnforcementModule.sol";
+      12                 :            : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
+      13                 :            : /* ==== Snapshot === */
+      14                 :            : import {ISnapshotSource} from "../interfaces/ISnapshotSource.sol";
+      15                 :            : /* ==== IncomeVault === */
+      16                 :            : import {IncomeVaultBaseERC2771} from "../IncomeVaultBaseERC2771.sol";
+      17                 :            : import {IncomeVaultValidationModule} from "../modules/IncomeVaultValidationModule.sol";
+      18                 :            : import {IncomeVaultRestricted} from "../public/IncomeVaultRestricted.sol";
+      19                 :            : import {IncomeVaultSnapshotModule} from "../modules/IncomeVaultSnapshotModule.sol";
+      20                 :            : import {IncomeVaultValidationModule} from "../modules/IncomeVaultValidationModule.sol";
+      21                 :            : import {Ownable2StepERC165Module} from "../modules/Ownable2StepERC165Module.sol";
+      22                 :            : import {IERC7741} from "../interfaces/IERC7741.sol";
+      23                 :            : import {IIncomeVault} from "../interfaces/IIncomeVault.sol";
+      24                 :            : 
+      25                 :            : /**
+      26                 :            :  * @title Income Vault to distribute dividends — single-owner deployment
+      27                 :            :  * @dev
+      28                 :            :  * Answers **who** may do what with a single ERC-173 owner: every authorization hook collapses to
+      29                 :            :  * `onlyOwner`. `Ownable2Step` is used rather than `Ownable` so a mistyped address cannot lose the
+      30                 :            :  * contract — the handover only completes when the new owner calls `acceptOwnership`.
+      31                 :            :  *
+      32                 :            :  * @custom:security This variant **cannot express separated duties**. The owner deposits, withdraws,
+      33                 :            :  * distributes, runs the claim window, pauses, freezes and repoints the RuleEngine. In particular the
+      34                 :            :  * account that funds the vault is the same account that can empty it through `withdrawAll`. Choose
+      35                 :            :  * {IncomeVault}, the role-based deployment, whenever depositing and withdrawing must be held by
+      36                 :            :  * different accounts — which is the usual requirement for an issuer paying dividends.
+      37                 :            :  */
+      38                 :            : contract IncomeVaultOwnable2Step is
+      39                 :            :     IncomeVaultValidationModule,
+      40                 :            :     IncomeVaultBaseERC2771,
+      41                 :            :     Ownable2StepUpgradeable,
+      42                 :            :     Ownable2StepERC165Module
+      43                 :            : {
+      44                 :            :     /**
+      45                 :            :      * @param forwarderIrrevocable Address of the forwarder, required for the gasless support
+      46                 :            :      */
+      47                 :            :     /// @custom:oz-upgrades-unsafe-allow constructor
+      48                 :         50 :     constructor(address forwarderIrrevocable) IncomeVaultBaseERC2771(forwarderIrrevocable) {
+      49                 :            :         // Disable the possibility to initialize the implementation
+      50                 :         50 :         _disableInitializers();
+      51                 :            :     }
+      52                 :            : 
+      53                 :            :     /**
+      54                 :            :      * @notice
+      55                 :            :      * initialize the proxy contract
+      56                 :            :      * The calls to this function will revert if the contract was deployed without a proxy
+      57                 :            :      * @param owner_ Address of the initial contract owner (ERC-173)
+      58                 :            :      * @param ERC20TokenPayment_ ERC20 token used to perform the payment
+      59                 :            :      * @param snapshotSource_ contract implementing {ISnapshotSource}, source of the holder balances
+      60                 :            :      * @param ruleEngine_ optional RuleEngine applied to the payouts, or the zero address
+      61                 :            :      * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted
+      62                 :            :      */
+      63                 :         50 :     function initialize(
+      64                 :            :         address owner_,
+      65                 :            :         IERC20 ERC20TokenPayment_,
+      66                 :            :         ISnapshotSource snapshotSource_,
+      67                 :            :         IRuleEngine ruleEngine_,
+      68                 :            :         uint256 timeLimitToWithdraw_
+      69                 :            :     ) public initializer {
+      70            [ + ]:         50 :         if (owner_ == address(0)) {
+      71                 :          1 :             revert IncomeVault_AdminWithAddressZeroNotAllowed();
+      72                 :            :         }
+      73                 :         49 :         __Ownable_init_unchained(owner_);
+      74                 :         49 :         __Ownable2Step_init_unchained();
+      75                 :         49 :         __ERC165_init_unchained();
+      76                 :            :         // the validation answer this deployment chose
+      77                 :         49 :         __Pausable_init_unchained();
+      78                 :         49 :         __IncomeVaultValidation_init_unchained(ruleEngine_);
+      79                 :         49 :         __IncomeVaultBase_init_unchained(ERC20TokenPayment_, snapshotSource_, timeLimitToWithdraw_);
+      80                 :            :     }
+      81                 :            : 
+      82                 :            :     /* ============ ERC-165 ============ */
+      83                 :            :     /**
+      84                 :            :      * @inheritdoc Ownable2StepERC165Module
+      85                 :            :      * @dev Adds ERC-7741, whose specification requires a contract implementing it to answer `true`
+      86                 :            :      * for `0xa9e50872`.
+      87                 :            :      */
+      88                 :          5 :     function supportsInterface(bytes4 interfaceId)
+      89                 :            :         public
+      90                 :            :         view
+      91                 :            :         virtual
+      92                 :            :         override(Ownable2StepERC165Module)
+      93                 :            :         returns (bool)
+      94                 :            :     {
+      95                 :          5 :         return interfaceId == type(IIncomeVault).interfaceId || interfaceId == type(IERC7741).interfaceId
+      96                 :          3 :             || Ownable2StepERC165Module.supportsInterface(interfaceId);
+      97                 :            :     }
+      98                 :            : 
+      99                 :            :     /*//////////////////////////////////////////////////////////////
+     100                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+     101                 :            :     //////////////////////////////////////////////////////////////*/
+     102                 :            :     /* ============ ERC-2771 / Context disambiguation ============ */
+     103                 :            :     /**
+     104                 :            :      * @inheritdoc IncomeVaultBaseERC2771
+     105                 :            :      */
+     106                 :         52 :     function _msgSender()
+     107                 :            :         internal
+     108                 :            :         view
+     109                 :            :         virtual
+     110                 :            :         override(IncomeVaultBaseERC2771, ContextUpgradeable)
+     111                 :            :         returns (address sender)
+     112                 :            :     {
+     113                 :         52 :         return IncomeVaultBaseERC2771._msgSender();
+     114                 :            :     }
+     115                 :            : 
+     116                 :            :     /**
+     117                 :            :      * @inheritdoc IncomeVaultBaseERC2771
+     118                 :            :      */
+     119                 :          0 :     function _msgData()
+     120                 :            :         internal
+     121                 :            :         view
+     122                 :            :         virtual
+     123                 :            :         override(IncomeVaultBaseERC2771, ContextUpgradeable)
+     124                 :            :         returns (bytes calldata)
+     125                 :            :     {
+     126                 :          0 :         return IncomeVaultBaseERC2771._msgData();
+     127                 :            :     }
+     128                 :            : 
+     129                 :            :     /**
+     130                 :            :      * @inheritdoc IncomeVaultBaseERC2771
+     131                 :            :      */
+     132                 :         52 :     function _contextSuffixLength()
+     133                 :            :         internal
+     134                 :            :         view
+     135                 :            :         virtual
+     136                 :            :         override(IncomeVaultBaseERC2771, ContextUpgradeable)
+     137                 :            :         returns (uint256)
+     138                 :            :     {
+     139                 :         52 :         return IncomeVaultBaseERC2771._contextSuffixLength();
+     140                 :            :     }
+     141                 :            : 
+     142                 :            :     /* ============ Access Control ============ */
+     143                 :            :     /// @inheritdoc IncomeVaultRestricted
+     144                 :          3 :     function _authorizeDeposit() internal view virtual override onlyOwner {}
+     145                 :            : 
+     146                 :            :     /// @inheritdoc IncomeVaultRestricted
+     147                 :          4 :     function _authorizeWithdraw() internal view virtual override onlyOwner {}
+     148                 :            : 
+     149                 :            :     /// @inheritdoc IncomeVaultRestricted
+     150                 :          1 :     function _authorizeDistribute() internal view virtual override onlyOwner {}
+     151                 :            : 
+     152                 :            :     /// @inheritdoc IncomeVaultRestricted
+     153                 :          8 :     function _authorizeOperator() internal view virtual override onlyOwner {}
+     154                 :            : 
+     155                 :            :     /// @inheritdoc IncomeVaultSnapshotModule
+     156                 :          2 :     function _authorizeSnapshotSourceManagement() internal view virtual override onlyOwner {}
+     157                 :            : 
+     158                 :            :     /// @inheritdoc IncomeVaultValidationModule
+     159                 :          1 :     function _authorizeRuleEngineManagement() internal view virtual override onlyOwner {}
+     160                 :            : 
+     161                 :            :     /// @inheritdoc PauseModule
+     162                 :          5 :     function _authorizePause() internal view virtual override(PauseModule) onlyOwner {}
+     163                 :            : 
+     164                 :            :     /// @inheritdoc PauseModule
+     165                 :          2 :     function _authorizeDeactivate() internal view virtual override(PauseModule) onlyOwner {}
+     166                 :            : 
+     167                 :            :     /// @inheritdoc EnforcementModule
+     168                 :          2 :     function _authorizeFreeze() internal view virtual override(EnforcementModule) onlyOwner {}
+     169                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/test/coverage/src/modules/index-sort-b.html b/doc/coverage/src/deployment/index-sort-b.html similarity index 66% rename from doc/test/coverage/src/modules/index-sort-b.html rename to doc/coverage/src/deployment/index-sort-b.html index a088ac9..ca5829f 100644 --- a/doc/test/coverage/src/modules/index-sort-b.html +++ b/doc/coverage/src/deployment/index-sort-b.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/modules + LCOV - lcov.info - src/deployment @@ -19,7 +19,7 @@ - + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - - + + +
Current view:top level - src/modulestop level - src/deployment Hitlcov.info Lines:1250.0 %535793.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:1250.0 %283093.3 %
Branches:00-22100.0 %
@@ -82,16 +82,28 @@ Branches Sort by branch coverage - MetaTxModuleStandalone.sol + IncomeVault.sol + +
92.9%92.9%
+ + 92.9 % + 26 / 28 + 93.3 % + 14 / 15 + 100.0 % + 1 / 1 + + + IncomeVaultOwnable2Step.sol -
50.0%50.0%
+
93.1%93.1%
- 50.0 % - 1 / 2 - 50.0 % - 1 / 2 - - - 0 / 0 + 93.1 % + 27 / 29 + 93.3 % + 14 / 15 + 100.0 % + 1 / 1 diff --git a/doc/test/coverage/src/modules/index-sort-f.html b/doc/coverage/src/deployment/index-sort-f.html similarity index 66% rename from doc/test/coverage/src/modules/index-sort-f.html rename to doc/coverage/src/deployment/index-sort-f.html index c1d3706..598f822 100644 --- a/doc/test/coverage/src/modules/index-sort-f.html +++ b/doc/coverage/src/deployment/index-sort-f.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/modules + LCOV - lcov.info - src/deployment @@ -19,7 +19,7 @@ - + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - - + + +
Current view:top level - src/modulestop level - src/deployment Hitlcov.info Lines:1250.0 %535793.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:1250.0 %283093.3 %
Branches:00-22100.0 %
@@ -82,16 +82,28 @@ Branches Sort by branch coverage - MetaTxModuleStandalone.sol + IncomeVault.sol + +
92.9%92.9%
+ + 92.9 % + 26 / 28 + 93.3 % + 14 / 15 + 100.0 % + 1 / 1 + + + IncomeVaultOwnable2Step.sol -
50.0%50.0%
+
93.1%93.1%
- 50.0 % - 1 / 2 - 50.0 % - 1 / 2 - - - 0 / 0 + 93.1 % + 27 / 29 + 93.3 % + 14 / 15 + 100.0 % + 1 / 1 diff --git a/doc/test/coverage/src/modules/index-sort-l.html b/doc/coverage/src/deployment/index-sort-l.html similarity index 66% rename from doc/test/coverage/src/modules/index-sort-l.html rename to doc/coverage/src/deployment/index-sort-l.html index 375425e..12f1b83 100644 --- a/doc/test/coverage/src/modules/index-sort-l.html +++ b/doc/coverage/src/deployment/index-sort-l.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/modules + LCOV - lcov.info - src/deployment @@ -19,7 +19,7 @@ - + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - - + + +
Current view:top level - src/modulestop level - src/deployment Hitlcov.info Lines:1250.0 %535793.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:1250.0 %283093.3 %
Branches:00-22100.0 %
@@ -82,16 +82,28 @@ Branches Sort by branch coverage - MetaTxModuleStandalone.sol + IncomeVault.sol + +
92.9%92.9%
+ + 92.9 % + 26 / 28 + 93.3 % + 14 / 15 + 100.0 % + 1 / 1 + + + IncomeVaultOwnable2Step.sol -
50.0%50.0%
+
93.1%93.1%
- 50.0 % - 1 / 2 - 50.0 % - 1 / 2 - - - 0 / 0 + 93.1 % + 27 / 29 + 93.3 % + 14 / 15 + 100.0 % + 1 / 1 diff --git a/doc/test/coverage/src/modules/index.html b/doc/coverage/src/deployment/index.html similarity index 67% rename from doc/test/coverage/src/modules/index.html rename to doc/coverage/src/deployment/index.html index bf323f0..e27cb1a 100644 --- a/doc/test/coverage/src/modules/index.html +++ b/doc/coverage/src/deployment/index.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/modules + LCOV - lcov.info - src/deployment @@ -19,7 +19,7 @@ - + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - - + + +
Current view:top level - src/modulestop level - src/deployment Hitlcov.info Lines:1250.0 %535793.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:1250.0 %283093.3 %
Branches:00-22100.0 %
@@ -82,16 +82,28 @@ Branches Sort by branch coverage - MetaTxModuleStandalone.sol + IncomeVault.sol + +
92.9%92.9%
+ + 92.9 % + 26 / 28 + 93.3 % + 14 / 15 + 100.0 % + 1 / 1 + + + IncomeVaultOwnable2Step.sol -
50.0%50.0%
+
93.1%93.1%
- 50.0 % - 1 / 2 - 50.0 % - 1 / 2 - - - 0 / 0 + 93.1 % + 27 / 29 + 93.3 % + 14 / 15 + 100.0 % + 1 / 1 diff --git a/doc/test/coverage/script/index-sort-b.html b/doc/coverage/src/index-sort-b.html similarity index 70% rename from doc/test/coverage/script/index-sort-b.html rename to doc/coverage/src/index-sort-b.html index e8e851a..bb1f015 100644 --- a/doc/test/coverage/script/index-sort-b.html +++ b/doc/coverage/src/index-sort-b.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - script + LCOV - lcov.info - src @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - - + + + - + - - - + + + @@ -50,8 +50,8 @@ - - + +
Current view:top level - scripttop level - src Hitlcov.info Lines:0280.0 %91181.8 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:020.0 %3475.0 %
Branches: 020.0 %0-
@@ -82,26 +82,26 @@ Branches Sort by branch coverage - RuleEngineScript.s.sol + IncomeVaultBase.sol -
0.0%
+
100.0%
- 0.0 % - 0 / 12 - 0.0 % - 0 / 1 - 0.0 % - 0 / 2 + 100.0 % + 5 / 5 + 100.0 % + 1 / 1 + - + 0 / 0 - CMTATWithRuleEngineScript.s.sol + IncomeVaultBaseERC2771.sol -
0.0%
+
66.7%66.7%
- 0.0 % - 0 / 16 - 0.0 % - 0 / 1 + 66.7 % + 4 / 6 + 66.7 % + 2 / 3 - 0 / 0 diff --git a/doc/test/coverage/script/index-sort-f.html b/doc/coverage/src/index-sort-f.html similarity index 70% rename from doc/test/coverage/script/index-sort-f.html rename to doc/coverage/src/index-sort-f.html index 953bf68..84dd6fd 100644 --- a/doc/test/coverage/script/index-sort-f.html +++ b/doc/coverage/src/index-sort-f.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - script + LCOV - lcov.info - src @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - - + + + - + - - - + + + @@ -50,8 +50,8 @@ - - + +
Current view:top level - scripttop level - src Hitlcov.info Lines:0280.0 %91181.8 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:020.0 %3475.0 %
Branches: 020.0 %0-
@@ -82,28 +82,28 @@ Branches Sort by branch coverage - CMTATWithRuleEngineScript.s.sol + IncomeVaultBaseERC2771.sol -
0.0%
+
66.7%66.7%
- 0.0 % - 0 / 16 - 0.0 % - 0 / 1 + 66.7 % + 4 / 6 + 66.7 % + 2 / 3 - 0 / 0 - RuleEngineScript.s.sol + IncomeVaultBase.sol -
0.0%
+
100.0%
- 0.0 % - 0 / 12 - 0.0 % - 0 / 1 - 0.0 % - 0 / 2 + 100.0 % + 5 / 5 + 100.0 % + 1 / 1 + - + 0 / 0 diff --git a/doc/test/coverage/script/index-sort-l.html b/doc/coverage/src/index-sort-l.html similarity index 70% rename from doc/test/coverage/script/index-sort-l.html rename to doc/coverage/src/index-sort-l.html index 0cd3179..8c879a2 100644 --- a/doc/test/coverage/script/index-sort-l.html +++ b/doc/coverage/src/index-sort-l.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - script + LCOV - lcov.info - src @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - - + + + - + - - - + + + @@ -50,8 +50,8 @@ - - + +
Current view:top level - scripttop level - src Hitlcov.info Lines:0280.0 %91181.8 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:020.0 %3475.0 %
Branches: 020.0 %0-
@@ -82,26 +82,26 @@ Branches Sort by branch coverage - RuleEngineScript.s.sol + IncomeVaultBaseERC2771.sol -
0.0%
+
66.7%66.7%
- 0.0 % - 0 / 12 - 0.0 % - 0 / 1 - 0.0 % - 0 / 2 + 66.7 % + 4 / 6 + 66.7 % + 2 / 3 + - + 0 / 0 - CMTATWithRuleEngineScript.s.sol + IncomeVaultBase.sol -
0.0%
+
100.0%
- 0.0 % - 0 / 16 - 0.0 % - 0 / 1 + 100.0 % + 5 / 5 + 100.0 % + 1 / 1 - 0 / 0 diff --git a/doc/test/coverage/script/index.html b/doc/coverage/src/index.html similarity index 70% rename from doc/test/coverage/script/index.html rename to doc/coverage/src/index.html index 483de1c..8accba0 100644 --- a/doc/test/coverage/script/index.html +++ b/doc/coverage/src/index.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - script + LCOV - lcov.info - src @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - - + + + - + - - - + + + @@ -50,8 +50,8 @@ - - + +
Current view:top level - scripttop level - src Hitlcov.info Lines:0280.0 %91181.8 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:020.0 %3475.0 %
Branches: 020.0 %0-
@@ -82,28 +82,28 @@ Branches Sort by branch coverage - CMTATWithRuleEngineScript.s.sol + IncomeVaultBase.sol -
0.0%
+
100.0%
- 0.0 % - 0 / 16 - 0.0 % - 0 / 1 + 100.0 % + 5 / 5 + 100.0 % + 1 / 1 - 0 / 0 - RuleEngineScript.s.sol + IncomeVaultBaseERC2771.sol -
0.0%
+
66.7%66.7%
- 0.0 % - 0 / 12 - 0.0 % - 0 / 1 - 0.0 % - 0 / 2 + 66.7 % + 4 / 6 + 66.7 % + 2 / 3 + - + 0 / 0 diff --git a/doc/test/coverage/src/rules/RuleSanctionList.sol.func-sort-c.html b/doc/coverage/src/modules/ERC7741Module.sol.func-sort-c.html similarity index 56% rename from doc/test/coverage/src/rules/RuleSanctionList.sol.func-sort-c.html rename to doc/coverage/src/modules/ERC7741Module.sol.func-sort-c.html index 8cbcf47..6cfd8bc 100644 --- a/doc/test/coverage/src/rules/RuleSanctionList.sol.func-sort-c.html +++ b/doc/coverage/src/modules/ERC7741Module.sol.func-sort-c.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/rules/RuleSanctionList.sol - functions + LCOV - lcov.info - src/modules/ERC7741Module.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,26 +31,26 @@ - - - + + + - + - - - + + + - - + + @@ -65,36 +65,28 @@
Current view:top level - src/rules - RuleSanctionList.sol (source / functions)top level - src/modules - ERC7741Module.sol (source / functions) Hitlcov.info Lines:151693.8 %2424100.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:6785.7 %55100.0 %
Branches:101044 100.0 %
- + - - - - - + - - - - - + - - + + - - + + - - + +

Function Name Sort by function nameFunction Name Sort by function name Hit count Sort by hit count
RuleSanctionList._msgData0
RuleSanctionList._msgSenderERC7741Module.invalidateNonce 2
RuleSanctionList.setOracle2
RuleSanctionList.canReturnTransferRestrictionCodeERC7741Module.authorizations 3
RuleSanctionList.detectTransferRestriction3ERC7741Module.DOMAIN_SEPARATOR11
RuleSanctionList.messageForTransferRestriction3ERC7741Module.authorizeOperator13
RuleSanctionList.validateTransfer4ERC7741Module._getERC7741ModuleStorage16

diff --git a/doc/test/coverage/src/rules/RuleSanctionList.sol.func.html b/doc/coverage/src/modules/ERC7741Module.sol.func.html similarity index 56% rename from doc/test/coverage/src/rules/RuleSanctionList.sol.func.html rename to doc/coverage/src/modules/ERC7741Module.sol.func.html index f3accdc..3ca422f 100644 --- a/doc/test/coverage/src/rules/RuleSanctionList.sol.func.html +++ b/doc/coverage/src/modules/ERC7741Module.sol.func.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/rules/RuleSanctionList.sol - functions + LCOV - lcov.info - src/modules/ERC7741Module.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,26 +31,26 @@ - - - + + + - + - - - + + + - - + + @@ -66,36 +66,28 @@ - + - - + + - - + + - + - - + + - - - - - + - - - -
Current view:top level - src/rules - RuleSanctionList.sol (source / functions)top level - src/modules - ERC7741Module.sol (source / functions) Hitlcov.info Lines:151693.8 %2424100.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:6785.7 %55100.0 %
Branches:101044 100.0 %

Function Name Sort by function nameHit count Sort by hit countHit count Sort by hit count
RuleSanctionList._msgData0ERC7741Module.DOMAIN_SEPARATOR11
RuleSanctionList._msgSender2ERC7741Module._getERC7741ModuleStorage16
RuleSanctionList.canReturnTransferRestrictionCodeERC7741Module.authorizations 3
RuleSanctionList.detectTransferRestriction3ERC7741Module.authorizeOperator13
RuleSanctionList.messageForTransferRestriction3
RuleSanctionList.setOracleERC7741Module.invalidateNonce 2
RuleSanctionList.validateTransfer4

diff --git a/doc/coverage/src/modules/ERC7741Module.sol.gcov.html b/doc/coverage/src/modules/ERC7741Module.sol.gcov.html new file mode 100644 index 0000000..741e045 --- /dev/null +++ b/doc/coverage/src/modules/ERC7741Module.sol.gcov.html @@ -0,0 +1,230 @@ + + + + + + + LCOV - lcov.info - src/modules/ERC7741Module.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - ERC7741Module.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:2424100.0 %
Date:2026-08-31 13:21:44Functions:55100.0 %
Branches:44100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
+       7                 :            : import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
+       8                 :            : import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
+       9                 :            : /* ==== IncomeVault === */
+      10                 :            : import {IncomeVaultInternal} from "./IncomeVaultInternal.sol";
+      11                 :            : import {IncomeVaultOperatorModule} from "./IncomeVaultOperatorModule.sol";
+      12                 :            : import {IERC7741} from "../interfaces/IERC7741.sol";
+      13                 :            : 
+      14                 :            : /**
+      15                 :            :  * @title ERC-7741 signed operator authorisation
+      16                 :            :  * @dev
+      17                 :            :  * Implements [ERC-7741](https://eips.ethereum.org/EIPS/eip-7741) on top of the operator mapping owned
+      18                 :            :  * by {IncomeVaultInternal}: a holder signs an EIP-712 message and anyone can submit it, so the holder
+      19                 :            :  * never needs gas or even an on-chain transaction to appoint a custodian.
+      20                 :            :  *
+      21                 :            :  * Signatures are checked with OpenZeppelin's `SignatureChecker`, so an **ERC-1271 smart-contract
+      22                 :            :  * wallet** authorises exactly as an EOA does — which matters here, because institutional holders of a
+      23                 :            :  * security token are usually contracts rather than externally owned accounts.
+      24                 :            :  *
+      25                 :            :  * Nonces are `bytes32` and unordered, as the standard specifies, so a holder can prepare several
+      26                 :            :  * independent authorisations without imposing an ordering on them.
+      27                 :            :  */
+      28                 :            : abstract contract ERC7741Module is
+      29                 :            :     EIP712Upgradeable,
+      30                 :            :     ContextUpgradeable,
+      31                 :            :     IncomeVaultOperatorModule,
+      32                 :            :     IncomeVaultInternal,
+      33                 :            :     IERC7741
+      34                 :            : {
+      35                 :            :     /* ============ State Variables ============ */
+      36                 :            :     /**
+      37                 :            :      * @notice EIP-712 type hash of the authorisation message, exactly as ERC-7741 defines it
+      38                 :            :      */
+      39                 :            :     bytes32 public constant AUTHORIZE_OPERATOR_TYPEHASH = keccak256(
+      40                 :            :         "AuthorizeOperator(address controller,address operator,bool approved,bytes32 nonce,uint256 deadline)"
+      41                 :            :     );
+      42                 :            : 
+      43                 :            :     /* ============ ERC-7201 ============ */
+      44                 :            :     /**
+      45                 :            :      * @dev Slot holding the ERC-7201 namespaced storage of this module, derived as
+      46                 :            :      * keccak256(abi.encode(uint256(keccak256("IncomeVault.storage.ERC7741Module")) - 1)) & ~bytes32(uint256(0xff))
+      47                 :            :      * Recompute it with `SlotDerivation.erc7201Slot()` before trusting a change to it.
+      48                 :            :      */
+      49                 :            :     bytes32 private constant ERC7741ModuleStorageLocation =
+      50                 :            :         0xb93ff011b98f03386917a7b9b9106f5d9f85ba058e0b4e9b3aad1f6474a96800;
+      51                 :            : 
+      52                 :            :     /* ==== ERC-7201 State Variables === */
+      53                 :            :     /// @custom:storage-location erc7201:IncomeVault.storage.ERC7741Module
+      54                 :            :     struct ERC7741ModuleStorage {
+      55                 :            :         // Nonces already spent, per holder. Set both by a successful authorisation and by
+      56                 :            :         // {invalidateNonce}, so a holder can burn a signature they no longer want honoured.
+      57                 :            :         mapping(address controller => mapping(bytes32 nonce => bool used)) _authorizations;
+      58                 :            :     }
+      59                 :            : 
+      60                 :            :     /* ============ Errors ============ */
+      61                 :            :     /// @notice Thrown when the signature's deadline has passed.
+      62                 :            :     error IncomeVault_AuthorizationExpired(uint256 deadline);
+      63                 :            :     /// @notice Thrown when the nonce was already spent or invalidated.
+      64                 :            :     error IncomeVault_AuthorizationUsed(address controller, bytes32 nonce);
+      65                 :            :     /// @notice Thrown when the signature does not recover to `controller`.
+      66                 :            :     error IncomeVault_InvalidAuthorization(address controller);
+      67                 :            :     /// @notice Thrown when the controller is the zero address.
+      68                 :            :     error IncomeVault_ControllerWithAddressZeroNotAllowed();
+      69                 :            : 
+      70                 :            :     /*//////////////////////////////////////////////////////////////
+      71                 :            :                             PUBLIC/EXTERNAL FUNCTIONS
+      72                 :            :     //////////////////////////////////////////////////////////////*/
+      73                 :            :     /* ============ State functions ============ */
+      74                 :            :     /// @inheritdoc IERC7741
+      75                 :         13 :     function authorizeOperator(
+      76                 :            :         address controller,
+      77                 :            :         address operator,
+      78                 :            :         bool approved,
+      79                 :            :         bytes32 nonce,
+      80                 :            :         uint256 deadline,
+      81                 :            :         bytes memory signature
+      82                 :            :     ) public virtual override(IERC7741) returns (bool success) {
+      83            [ + ]:         13 :         if (block.timestamp > deadline) {
+      84                 :          1 :             revert IncomeVault_AuthorizationExpired(deadline);
+      85                 :            :         }
+      86            [ + ]:         12 :         if (controller == address(0)) {
+      87                 :          1 :             revert IncomeVault_ControllerWithAddressZeroNotAllowed();
+      88                 :            :         }
+      89                 :         11 :         ERC7741ModuleStorage storage $ = _getERC7741ModuleStorage();
+      90            [ + ]:          2 :         if ($._authorizations[controller][nonce]) {
+      91                 :          2 :             revert IncomeVault_AuthorizationUsed(controller, nonce);
+      92                 :            :         }
+      93                 :            :         // Spend the nonce before validating, so no path can replay it.
+      94                 :          9 :         $._authorizations[controller][nonce] = true;
+      95                 :            : 
+      96                 :          9 :         bytes32 digest = _hashTypedDataV4(
+      97                 :            :             keccak256(abi.encode(AUTHORIZE_OPERATOR_TYPEHASH, controller, operator, approved, nonce, deadline))
+      98                 :            :         );
+      99                 :            :         // SignatureChecker accepts both ECDSA and ERC-1271, so contract wallets work unchanged.
+     100            [ + ]:          9 :         if (!SignatureChecker.isValidSignatureNow(controller, digest, signature)) {
+     101                 :          2 :             revert IncomeVault_InvalidAuthorization(controller);
+     102                 :            :         }
+     103                 :            : 
+     104                 :          7 :         _setOperator(controller, operator, approved);
+     105                 :          7 :         return true;
+     106                 :            :     }
+     107                 :            : 
+     108                 :            :     /// @inheritdoc IERC7741
+     109                 :          2 :     function invalidateNonce(bytes32 nonce) public virtual override(IERC7741) {
+     110                 :          2 :         ERC7741ModuleStorage storage $ = _getERC7741ModuleStorage();
+     111                 :          2 :         $._authorizations[_msgSender()][nonce] = true;
+     112                 :            :     }
+     113                 :            : 
+     114                 :            :     /* ============ View functions ============ */
+     115                 :            :     /// @inheritdoc IERC7741
+     116                 :          3 :     function authorizations(address controller, bytes32 nonce)
+     117                 :            :         public
+     118                 :            :         view
+     119                 :            :         virtual
+     120                 :            :         override(IERC7741)
+     121                 :            :         returns (bool used)
+     122                 :            :     {
+     123                 :          3 :         ERC7741ModuleStorage storage $ = _getERC7741ModuleStorage();
+     124                 :          3 :         return $._authorizations[controller][nonce];
+     125                 :            :     }
+     126                 :            : 
+     127                 :            :     /// @inheritdoc IERC7741
+     128                 :         11 :     function DOMAIN_SEPARATOR() public view virtual override(IERC7741) returns (bytes32) {
+     129                 :         11 :         return _domainSeparatorV4();
+     130                 :            :     }
+     131                 :            : 
+     132                 :            :     /*//////////////////////////////////////////////////////////////
+     133                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+     134                 :            :     //////////////////////////////////////////////////////////////*/
+     135                 :            :     /* ============ ERC-7201 ============ */
+     136                 :            :     /**
+     137                 :            :      * @dev Returns the ERC-7201 namespaced storage of this module
+     138                 :            :      * @return $ the storage struct
+     139                 :            :      */
+     140                 :         16 :     function _getERC7741ModuleStorage() internal pure returns (ERC7741ModuleStorage storage $) {
+     141                 :            :         assembly {
+     142                 :         16 :             $.slot := ERC7741ModuleStorageLocation
+     143                 :            :         }
+     144                 :            :     }
+     145                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultInternal.sol.func-sort-c.html b/doc/coverage/src/modules/IncomeVaultInternal.sol.func-sort-c.html new file mode 100644 index 0000000..1832ea7 --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultInternal.sol.func-sort-c.html @@ -0,0 +1,157 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultInternal.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultInternal.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:909198.9 %
Date:2026-08-31 13:21:44Functions:1919100.0 %
Branches:181994.7 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultInternal.ERC20TokenPayment4
IncomeVaultInternal.timeLimitToWithdraw8
IncomeVaultInternal.openClaimCount10
IncomeVaultInternal.paidDividend10
IncomeVaultInternal.segregatedClaim10
IncomeVaultInternal.unclaimedDividend15
IncomeVaultInternal._setERC20TokenPayment288
IncomeVaultInternal._setTimeLimitToWithdraw292
IncomeVaultInternal._transferDividend485
IncomeVaultInternal._computeDividend673
IncomeVaultInternal._computeDividendBatch1147
IncomeVaultInternal._unclaimed1707
IncomeVaultInternal._setStatusClaim2092
IncomeVaultInternal.segregatedDividend2251
IncomeVaultInternal._deposit2267
IncomeVaultInternal._revertOnInvalidTime9737
IncomeVaultInternal._timeCode9745
IncomeVaultInternal.claimedDividend26251
IncomeVaultInternal._getIncomeVaultInternalStorage48049
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultInternal.sol.func.html b/doc/coverage/src/modules/IncomeVaultInternal.sol.func.html new file mode 100644 index 0000000..0149610 --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultInternal.sol.func.html @@ -0,0 +1,157 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultInternal.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultInternal.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:909198.9 %
Date:2026-08-31 13:21:44Functions:1919100.0 %
Branches:181994.7 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultInternal.ERC20TokenPayment4
IncomeVaultInternal._computeDividend673
IncomeVaultInternal._computeDividendBatch1147
IncomeVaultInternal._deposit2267
IncomeVaultInternal._getIncomeVaultInternalStorage48049
IncomeVaultInternal._revertOnInvalidTime9737
IncomeVaultInternal._setERC20TokenPayment288
IncomeVaultInternal._setStatusClaim2092
IncomeVaultInternal._setTimeLimitToWithdraw292
IncomeVaultInternal._timeCode9745
IncomeVaultInternal._transferDividend485
IncomeVaultInternal._unclaimed1707
IncomeVaultInternal.claimedDividend26251
IncomeVaultInternal.openClaimCount10
IncomeVaultInternal.paidDividend10
IncomeVaultInternal.segregatedClaim10
IncomeVaultInternal.segregatedDividend2251
IncomeVaultInternal.timeLimitToWithdraw8
IncomeVaultInternal.unclaimedDividend15
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultInternal.sol.gcov.html b/doc/coverage/src/modules/IncomeVaultInternal.sol.gcov.html new file mode 100644 index 0000000..533a305 --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultInternal.sol.gcov.html @@ -0,0 +1,481 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultInternal.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultInternal.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:909198.9 %
Date:2026-08-31 13:21:44Functions:1919100.0 %
Branches:181994.7 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+       7                 :            : import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+       8                 :            : /* ==== Snapshot === */
+       9                 :            : /* ==== IncomeVault === */
+      10                 :            : import {IncomeVaultInvariantStorage} from "../storage/IncomeVaultInvariantStorage.sol";
+      11                 :            : import {IIncomeVault} from "../interfaces/IIncomeVault.sol";
+      12                 :            : 
+      13                 :            : /**
+      14                 :            :  * @title Internal functions and ERC-7201 storage of the IncomeVault
+      15                 :            :  * @dev
+      16                 :            :  * Holds the dividend bookkeeping. The snapshot source is deliberately **not** here — see
+      17                 :            :  * {IncomeVaultSnapshotCore} — so a host that is its own source does not inherit an unused reference.
+      18                 :            :  *
+      19                 :            :  * The state is held in an ERC-7201 namespaced storage struct, as OpenZeppelin Upgradeable and the
+      20                 :            :  * CMTAT do. The namespace is derived from a hash, so it cannot collide with the storage of the
+      21                 :            :  * inherited modules; no `__gap` is needed and new fields can be appended to the struct freely.
+      22                 :            :  */
+      23                 :            : abstract contract IncomeVaultInternal is IncomeVaultInvariantStorage, IIncomeVault {
+      24                 :            :     // Manage transfer failure
+      25                 :            :     using SafeERC20 for IERC20;
+      26                 :            : 
+      27                 :            :     /* ============ Type declarations ============ */
+      28                 :            :     /* ============ ERC-7201 ============ */
+      29                 :            :     /**
+      30                 :            :      * @dev Slot holding the ERC-7201 namespaced storage of this module, derived as
+      31                 :            :      * keccak256(abi.encode(uint256(keccak256("IncomeVault.storage.IncomeVaultInternal")) - 1)) & ~bytes32(uint256(0xff))
+      32                 :            :      * Recompute it with `SlotDerivation.erc7201Slot()` before trusting a change to it.
+      33                 :            :      */
+      34                 :            :     bytes32 private constant IncomeVaultInternalStorageLocation =
+      35                 :            :         0xe4f8b033bcfc537db031b0e68e3c1ab0f1de86cf03893d031b6590510b0c0c00;
+      36                 :            : 
+      37                 :            :     /* ==== ERC-7201 State Variables === */
+      38                 :            :     /// @custom:storage-location erc7201:IncomeVault.storage.IncomeVaultInternal
+      39                 :            :     struct IncomeVaultInternalStorage {
+      40                 :            :         // ERC-20 token used to pay the dividends
+      41                 :            :         IERC20 _ERC20TokenPayment;
+      42                 :            :         // Records, per token holder and per dividend time, whether the dividends were claimed
+      43                 :            :         mapping(address tokenHolder => mapping(uint256 time => bool claimed)) _claimedDividend;
+      44                 :            :         // Total amount of payment token deposited for a given dividend time
+      45                 :            :         mapping(uint256 time => uint256 dividend) _segregatedDividend;
+      46                 :            :         // Claim status, per dividend time: true when the holders can claim
+      47                 :            :         mapping(uint256 time => bool status) _segregatedClaim;
+      48                 :            :         // Delay, after the dividend time, during which a claim is still accepted
+      49                 :            :         uint256 _timeLimitToWithdraw;
+      50                 :            :         // How many dividend times currently have their claims open. Appended after the fields above:
+      51                 :            :         // ERC-7201 struct members are append-only, never reordered.
+      52                 :            :         uint256 _openClaimCount;
+      53                 :            :         // Total already paid out for a dividend time. `_segregatedDividend` is deliberately NOT
+      54                 :            :         // reduced on a payout — it is the pro-rata denominator and must stay fixed for the period —
+      55                 :            :         // so this is what makes "how much of that deposit is still here" answerable.
+      56                 :            :         mapping(uint256 time => uint256 paid) _paidDividend;
+      57                 :            :     }
+      58                 :            : 
+      59                 :            :     /*//////////////////////////////////////////////////////////////
+      60                 :            :                             PUBLIC/EXTERNAL FUNCTIONS
+      61                 :            :     //////////////////////////////////////////////////////////////*/
+      62                 :            :     /* ============ View functions ============ */
+      63                 :            :     /**
+      64                 :            :      * @notice ERC-20 token used to pay the dividends
+      65                 :            :      * @return The payment token
+      66                 :            :      */
+      67                 :          4 :     function ERC20TokenPayment() public view virtual returns (IERC20) {
+      68                 :          4 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+      69                 :          4 :         return $._ERC20TokenPayment;
+      70                 :            :     }
+      71                 :            : 
+      72                 :            :     /**
+      73                 :            :      * @notice Tells whether a token holder already claimed the dividends of a given time
+      74                 :            :      * @param tokenHolder the address to check
+      75                 :            :      * @param time the dividend time
+      76                 :            :      * @return True if the dividends were already claimed or distributed
+      77                 :            :      */
+      78                 :      26251 :     function claimedDividend(address tokenHolder, uint256 time) public view virtual returns (bool) {
+      79                 :      26251 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+      80                 :      26251 :         return $._claimedDividend[tokenHolder][time];
+      81                 :            :     }
+      82                 :            : 
+      83                 :            :     /**
+      84                 :            :      * @notice Total amount of payment token deposited for a given dividend time
+      85                 :            :      * @param time the dividend time
+      86                 :            :      * @return The amount deposited, minus what was already withdrawn
+      87                 :            :      */
+      88                 :       2251 :     function segregatedDividend(uint256 time) public view virtual returns (uint256) {
+      89                 :       4071 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+      90                 :       4071 :         return $._segregatedDividend[time];
+      91                 :            :     }
+      92                 :            : 
+      93                 :            :     /**
+      94                 :            :      * @notice Claim status of a given dividend time
+      95                 :            :      * @param time the dividend time
+      96                 :            :      * @return True when the token holders can claim their dividends
+      97                 :            :      */
+      98                 :         10 :     function segregatedClaim(uint256 time) public view virtual returns (bool) {
+      99                 :         10 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     100                 :         10 :         return $._segregatedClaim[time];
+     101                 :            :     }
+     102                 :            : 
+     103                 :            :     /**
+     104                 :            :      * @notice Total already paid out for a dividend time
+     105                 :            :      * @param time the dividend time
+     106                 :            :      * @return The amount of payment token already transferred to holders for `time`
+     107                 :            :      */
+     108                 :         10 :     function paidDividend(uint256 time) public view virtual returns (uint256) {
+     109                 :         10 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     110                 :         10 :         return $._paidDividend[time];
+     111                 :            :     }
+     112                 :            : 
+     113                 :            :     /**
+     114                 :            :      * @notice What is still held for a dividend time — the deposit minus what has been paid out
+     115                 :            :      * @dev
+     116                 :            :      * This is the amount an issuer can sweep with {IncomeVaultRestricted-withdraw}, and it is the bound
+     117                 :            :      * that function enforces. `segregatedDividend` alone is **not** that amount: it is the pro-rata
+     118                 :            :      * denominator and stays fixed at the deposit even after holders are paid.
+     119                 :            :      *
+     120                 :            :      * After the claim window closes it is exactly the rounding dust plus anything unclaimed. Before it
+     121                 :            :      * closes it still includes what the remaining holders are entitled to, so sweeping early takes
+     122                 :            :      * money they can no longer be paid — see the note on {IncomeVaultRestricted-withdraw}.
+     123                 :            :      * @param time the dividend time
+     124                 :            :      * @return The amount of payment token still attributable to `time`
+     125                 :            :      */
+     126                 :         15 :     function unclaimedDividend(uint256 time) public view virtual returns (uint256) {
+     127                 :       1235 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     128                 :       1235 :         return _unclaimed($._segregatedDividend[time], $._paidDividend[time]);
+     129                 :            :     }
+     130                 :            : 
+     131                 :            :     /**
+     132                 :            :      * @notice How many dividend times currently have their claims open
+     133                 :            :      * @dev Maintained exactly by {_setStatusClaim}, the only writer of the claim status. Used by
+     134                 :            :      * {IncomeVaultSnapshotModule-setDividendSnapshotSource}, which refuses to change the snapshot source while any
+     135                 :            :      * period is open.
+     136                 :            :      * @return The number of open claim periods
+     137                 :            :      */
+     138                 :         10 :     function openClaimCount() public view virtual returns (uint256) {
+     139                 :         16 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     140                 :         16 :         return $._openClaimCount;
+     141                 :            :     }
+     142                 :            : 
+     143                 :            :     /**
+     144                 :            :      * @notice Delay, after the dividend time, during which a claim is still accepted
+     145                 :            :      * @return The delay in seconds
+     146                 :            :      */
+     147                 :          8 :     function timeLimitToWithdraw() public view virtual returns (uint256) {
+     148                 :          8 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     149                 :          8 :         return $._timeLimitToWithdraw;
+     150                 :            :     }
+     151                 :            : 
+     152                 :            :     /*//////////////////////////////////////////////////////////////
+     153                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+     154                 :            :     //////////////////////////////////////////////////////////////*/
+     155                 :            :     /* ============ State functions ============ */
+     156                 :            :     /**
+     157                 :            :      * @notice Records the claim then sends the dividends to the token holder
+     158                 :            :      * @param time dividend time
+     159                 :            :      * @param tokenHolder addresses to send the dividends
+     160                 :            :      * @param tokenHolderDividend the computed dividends
+     161                 :            :      */
+     162                 :        485 :     function _transferDividend(uint256 time, address tokenHolder, uint256 tokenHolderDividend) internal virtual {
+     163                 :        485 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     164                 :            :         // Before ERC-20 transfer to avoid re-entrancy attack
+     165                 :        485 :         $._claimedDividend[tokenHolder][time] = true;
+     166                 :        485 :         emit DividendClaimed(time, tokenHolder, tokenHolderDividend);
+     167                 :            :         // transfer
+     168                 :            :         // We don't revert if SenderBalance == 0 to record the claim
+     169            [ + ]:        485 :         if (tokenHolderDividend != 0) {
+     170                 :            :             // A payout must come out of its own period. Without this a claim made after the period
+     171                 :            :             // was swept mid-window would silently be funded from another period's deposit, leaving
+     172                 :            :             // that one unable to pay its holders. Unreachable in normal operation: the entitlements
+     173                 :            :             // of a period always sum to at most its deposit.
+     174                 :        472 :             uint256 paid = $._paidDividend[time];
+     175            [ + ]:        472 :             if (tokenHolderDividend > _unclaimed($._segregatedDividend[time], paid)) {
+     176                 :          2 :                 revert IncomeVault_NotEnoughAmount();
+     177                 :            :             }
+     178                 :        470 :             $._paidDividend[time] = paid + tokenHolderDividend;
+     179                 :            :             // Will revert in case of failure
+     180                 :        470 :             $._ERC20TokenPayment.safeTransfer(tokenHolder, tokenHolderDividend);
+     181                 :            :         }
+     182                 :            :     }
+     183                 :            : 
+     184                 :            :     /**
+     185                 :            :      * @notice Sets the ERC-20 token used to pay the dividends
+     186                 :            :      * @dev reverts if `ERC20TokenPayment_` is the zero address
+     187                 :            :      * @param ERC20TokenPayment_ the payment token
+     188                 :            :      */
+     189                 :        288 :     function _setERC20TokenPayment(IERC20 ERC20TokenPayment_) internal virtual {
+     190            [ + ]:        288 :         if (address(ERC20TokenPayment_) == address(0)) {
+     191                 :          1 :             revert IncomeVault_TokenPaymentWithAddressZeroNotAllowed();
+     192                 :            :         }
+     193                 :        287 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     194                 :        287 :         $._ERC20TokenPayment = ERC20TokenPayment_;
+     195                 :        287 :         emit ERC20TokenPaymentSet(ERC20TokenPayment_);
+     196                 :            :     }
+     197                 :            : 
+     198                 :            :     /**
+     199                 :            :      * @notice Sets the delay, after the dividend time, during which a claim is still accepted
+     200                 :            :      * @dev reverts if `timeLimitToWithdraw_` is zero — see {IncomeVault_TimeLimitToWithdrawZeroNotAllowed}
+     201                 :            :      * @param timeLimitToWithdraw_ the delay in seconds, must be greater than zero
+     202                 :            :      */
+     203                 :        292 :     function _setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) internal virtual {
+     204                 :            :         // Zero collapses the claim window to the single instant `block.timestamp == time`: one second
+     205                 :            :         // later {_timeCode} already returns TOO_LATE_TO_WITHDRAW and the period is unclaimable. Any
+     206                 :            :         // positive value is allowed — a short settlement window may be deliberate; zero never is.
+     207            [ + ]:        292 :         if (timeLimitToWithdraw_ == 0) {
+     208                 :          2 :             revert IncomeVault_TimeLimitToWithdrawZeroNotAllowed();
+     209                 :            :         }
+     210                 :        290 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     211                 :        290 :         $._timeLimitToWithdraw = timeLimitToWithdraw_;
+     212                 :        290 :         emit TimeLimitToWithdrawSet(timeLimitToWithdraw_);
+     213                 :            :     }
+     214                 :            : 
+     215                 :            :     /**
+     216                 :            :      * @notice Records a deposit against a dividend time
+     217                 :            :      * @dev
+     218                 :            :      * The single writer of `_segregatedDividend`, and the only place `newDeposit` is emitted. Both
+     219                 :            :      * funding paths go through it — {IncomeVaultRestricted-deposit} once,
+     220                 :            :      * {IncomeVaultRestricted-depositBatch} once per element — so validating, writing and announcing a
+     221                 :            :      * deposit cannot come apart. Each path carrying its own copy is what lets them diverge, so a new
+     222                 :            :      * funding path must call this rather than repeat it.
+     223                 :            :      *
+     224                 :            :      * The ERC-20 transfer is deliberately **not** here. `depositBatch` makes a single
+     225                 :            :      * `safeTransferFrom` for the whole batch, which is the reason it exists; folding the transfer in
+     226                 :            :      * would turn that back into one transfer per element.
+     227                 :            :      *
+     228                 :            :      * Takes the storage pointer rather than fetching it, as {_timeCode} does, so a batch acquires it
+     229                 :            :      * once instead of once per element.
+     230                 :            :      * @param $ the ERC-7201 storage of the vault
+     231                 :            :      * @param sender the account funding the deposit, reported by the event
+     232                 :            :      * @param time the dividend time the deposit is segregated under
+     233                 :            :      * @param amount the amount of payment token, which may not be zero
+     234                 :            :      */
+     235                 :       2267 :     function _deposit(IncomeVaultInternalStorage storage $, address sender, uint256 time, uint256 amount)
+     236                 :            :         internal
+     237                 :            :         virtual
+     238                 :            :     {
+     239            [ + ]:       2267 :         if (amount == 0) {
+     240                 :          2 :             revert IncomeVault_NoAmountSend();
+     241                 :            :         }
+     242                 :       2265 :         $._segregatedDividend[time] += amount;
+     243                 :       2265 :         emit newDeposit(time, sender, amount);
+     244                 :            :     }
+     245                 :            : 
+     246                 :            :     /**
+     247                 :            :      * @notice Opens or closes the claims for a dividend time
+     248                 :            :      * @param time the dividend time
+     249                 :            :      * @param status true when the token holders can claim
+     250                 :            :      */
+     251                 :       2092 :     function _setStatusClaim(uint256 time, bool status) internal virtual {
+     252                 :       2092 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     253                 :            :         // Idempotent: a call that does not change the status writes nothing, emits nothing and — the
+     254                 :            :         // reason this branch exists — leaves `_openClaimCount` exact. Without it, opening an already
+     255                 :            :         // open period would double-count and the counter could never return to zero.
+     256            [ + ]:       2092 :         if ($._segregatedClaim[time] == status) {
+     257                 :        939 :             return;
+     258                 :            :         }
+     259                 :       1153 :         $._segregatedClaim[time] = status;
+     260         [ +  + ]:        850 :         if (status) {
+     261                 :        850 :             ++$._openClaimCount;
+     262                 :            :         } else {
+     263                 :        303 :             --$._openClaimCount;
+     264                 :            :         }
+     265                 :       1153 :         emit ClaimStatusSet(time, status);
+     266                 :            :     }
+     267                 :            : 
+     268                 :            :     /* ============ View functions ============ */
+     269                 :            :     /**
+     270                 :            :      * @dev How much of a period's deposit is still held, given the two figures that decide it.
+     271                 :            :      *
+     272                 :            :      * Saturating, not a plain subtraction. Withdrawing mid-period lowers the denominator, so a claim
+     273                 :            :      * made afterwards is priced against the reduced figure and can push `paid` above `segregated`. That
+     274                 :            :      * state means the period is over-drawn and nothing is left to sweep — this must report zero, never
+     275                 :            :      * revert.
+     276                 :            :      *
+     277                 :            :      * One `pure` rule because both callers must agree on it: {unclaimedDividend} reports it, and
+     278                 :            :      * {_transferDividend} enforces it as the bound on a payout. Were they to diverge, a payout could be
+     279                 :            :      * funded from another period's deposit.
+     280                 :            :      * @param segregated the amount deposited for the period, the pro-rata denominator
+     281                 :            :      * @param paid the amount already paid out of that period
+     282                 :            :      * @return The amount still attributable to the period, or zero when it is over-drawn
+     283                 :            :      */
+     284                 :       1707 :     function _unclaimed(uint256 segregated, uint256 paid) internal pure virtual returns (uint256) {
+     285                 :       1707 :         return segregated > paid ? segregated - paid : 0;
+     286                 :            :     }
+     287                 :            : 
+     288                 :            :     /**
+     289                 :            :      * @notice Computes the dividends owed to several token holders for a given time
+     290                 :            :      * @param time dividend time
+     291                 :            :      * @param tokenHolders addresses to compute dividend
+     292                 :            :      * @param tokenHoldersBalance the sender balance
+     293                 :            :      * @param tokenTotalSupply the total supply
+     294                 :            :      * @return tokenHolderDividend the dividends owed to each address of `tokenHolders`
+     295                 :            :      */
+     296                 :       1147 :     function _computeDividendBatch(
+     297                 :            :         uint256 time,
+     298                 :            :         address[] calldata tokenHolders,
+     299                 :            :         uint256[] memory tokenHoldersBalance,
+     300                 :            :         uint256 tokenTotalSupply
+     301                 :            :     ) internal view virtual returns (uint256[] memory tokenHolderDividend) {
+     302                 :       1147 :         tokenHolderDividend = new uint256[](tokenHolders.length);
+     303                 :       1147 :         uint256 dividendTotalSupply = segregatedDividend(time);
+     304                 :       1147 :         for (uint256 i = 0; i < tokenHolders.length; ++i) {
+     305            [ + ]:       2235 :             if (tokenHoldersBalance[i] > 0) {
+     306                 :       2235 :                 tokenHolderDividend[i] = (tokenHoldersBalance[i] * dividendTotalSupply) / tokenTotalSupply;
+     307                 :            :             }
+     308                 :            :         }
+     309                 :            :     }
+     310                 :            : 
+     311                 :            :     /**
+     312                 :            :      * @notice Computes the dividends owed to a single token holder for a given time
+     313                 :            :      * @param time dividend time
+     314                 :            :      * @param senderBalance token holder balance
+     315                 :            :      * @param tokenTotalSupply the total supply
+     316                 :            :      * @return tokenHolderDividend the dividends owed to the token holder, rounded down
+     317                 :            :      */
+     318                 :        673 :     function _computeDividend(uint256 time, uint256 senderBalance, uint256 tokenTotalSupply)
+     319                 :            :         internal
+     320                 :            :         view
+     321                 :            :         virtual
+     322                 :            :         returns (uint256 tokenHolderDividend)
+     323                 :            :     {
+     324            [ # ]:        673 :         if (senderBalance == 0) {
+     325                 :          0 :             revert IncomeVault_NoDividendToClaim();
+     326                 :            :         }
+     327                 :            :         /**
+     328                 :            :          * Example
+     329                 :            :          * SenderBalance = 300
+     330                 :            :          * totalSupply = 900
+     331                 :            :          * Dividend total supply = 200
+     332                 :            :          * dividend = (300 * 200) / 900 = 60000 / 900 = 600/9 = 66.6 = 66
+     333                 :            :          */
+     334                 :        673 :         uint256 dividendTotalSupply = segregatedDividend(time);
+     335                 :            : 
+     336                 :        673 :         tokenHolderDividend = (senderBalance * dividendTotalSupply) / tokenTotalSupply;
+     337                 :            :     }
+     338                 :            : 
+     339                 :            :     /**
+     340                 :            :      * @dev reverts with the error matching a non-OK {TIME_ERROR_CODE}. Exhaustive over the enum, and
+     341                 :            :      * fails closed on an unhandled value — see the comment on the final branch.
+     342                 :            :      * @param code the code returned by {_timeCode}
+     343                 :            :      */
+     344                 :       9737 :     function _revertOnInvalidTime(TIME_ERROR_CODE code) internal view virtual {
+     345         [ +  + ]:       9737 :         if (code == TIME_ERROR_CODE.OK) {
+     346                 :       9737 :             return;
+     347         [ +  + ]:       7190 :         } else if (code == TIME_ERROR_CODE.CLAIM_NOT_ACTIVATED) {
+     348                 :       7024 :             revert IncomeVault_ClaimNotActivated();
+     349         [ +  + ]:        166 :         } else if (code == TIME_ERROR_CODE.TOO_LATE_TO_WITHDRAW) {
+     350                 :          8 :             revert IncomeVault_TooLateToWithdraw(block.timestamp);
+     351                 :            :         } else {
+     352                 :            :             // TOO_EARLY_TO_WITHDRAW — the only remaining value of an exhaustive enum, so an
+     353                 :            :             // unconditional `else` rather than a fourth comparison. This also fails **closed**: a
+     354                 :            :             // value added to TIME_ERROR_CODE without a matching arm reverts here instead of falling
+     355                 :            :             // through and silently allowing the claim, which is what a trailing `else if` would do.
+     356                 :        158 :             revert IncomeVault_TooEarlyToWithdraw(block.timestamp);
+     357                 :            :         }
+     358                 :            :     }
+     359                 :            : 
+     360                 :            :     /**
+     361                 :            :      * @dev {validateTimeCode} with the caller supplying the storage pointer and the withdraw limit,
+     362                 :            :      * so a batch can read the limit once instead of once per element.
+     363                 :            :      * @param $ the ERC-7201 storage of the vault
+     364                 :            :      * @param time the dividend time to check
+     365                 :            :      * @param timeLimit the value of `timeLimitToWithdraw`
+     366                 :            :      * @return code the reason the time is invalid, or `TIME_ERROR_CODE.OK`
+     367                 :            :      */
+     368                 :       9745 :     function _timeCode(IncomeVaultInternalStorage storage $, uint256 time, uint256 timeLimit)
+     369                 :            :         internal
+     370                 :            :         view
+     371                 :            :         virtual
+     372                 :            :         returns (TIME_ERROR_CODE code)
+     373                 :            :     {
+     374            [ + ]:       9745 :         if (!$._segregatedClaim[time]) {
+     375                 :       7026 :             return TIME_ERROR_CODE.CLAIM_NOT_ACTIVATED;
+     376                 :            :         }
+     377            [ + ]:       2719 :         if (block.timestamp > timeLimit + time) {
+     378                 :          9 :             return TIME_ERROR_CODE.TOO_LATE_TO_WITHDRAW;
+     379                 :            :         }
+     380            [ + ]:       2710 :         if (block.timestamp < time) {
+     381                 :        160 :             return TIME_ERROR_CODE.TOO_EARLY_TO_WITHDRAW;
+     382                 :            :         }
+     383                 :       2550 :         return TIME_ERROR_CODE.OK;
+     384                 :            :     }
+     385                 :            : 
+     386                 :            :     /* ============ ERC-7201 ============ */
+     387                 :            :     /**
+     388                 :            :      * @dev Returns the ERC-7201 namespaced storage of the IncomeVault
+     389                 :            :      * @return $ the storage struct
+     390                 :            :      */
+     391                 :      48049 :     function _getIncomeVaultInternalStorage() internal pure returns (IncomeVaultInternalStorage storage $) {
+     392                 :            :         assembly {
+     393                 :      48049 :             $.slot := IncomeVaultInternalStorageLocation
+     394                 :            :         }
+     395                 :            :     }
+     396                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultOperatorModule.sol.func-sort-c.html b/doc/coverage/src/modules/IncomeVaultOperatorModule.sol.func-sort-c.html new file mode 100644 index 0000000..20ee0e0 --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultOperatorModule.sol.func-sort-c.html @@ -0,0 +1,101 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultOperatorModule.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultOperatorModule.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1616100.0 %
Date:2026-08-31 13:21:44Functions:55100.0 %
Branches:11100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultOperatorModule._requireHolderOrOperator12
IncomeVaultOperatorModule.isOperator12
IncomeVaultOperatorModule.setOperator14
IncomeVaultOperatorModule._setOperator21
IncomeVaultOperatorModule._getOperatorStorage44
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultOperatorModule.sol.func.html b/doc/coverage/src/modules/IncomeVaultOperatorModule.sol.func.html new file mode 100644 index 0000000..a35c241 --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultOperatorModule.sol.func.html @@ -0,0 +1,101 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultOperatorModule.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultOperatorModule.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1616100.0 %
Date:2026-08-31 13:21:44Functions:55100.0 %
Branches:11100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultOperatorModule._getOperatorStorage44
IncomeVaultOperatorModule._requireHolderOrOperator12
IncomeVaultOperatorModule._setOperator21
IncomeVaultOperatorModule.isOperator12
IncomeVaultOperatorModule.setOperator14
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultOperatorModule.sol.gcov.html b/doc/coverage/src/modules/IncomeVaultOperatorModule.sol.gcov.html new file mode 100644 index 0000000..d1559d1 --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultOperatorModule.sol.gcov.html @@ -0,0 +1,194 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultOperatorModule.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultOperatorModule.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:1616100.0 %
Date:2026-08-31 13:21:44Functions:55100.0 %
Branches:11100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
+       6                 :            : import {IERC7540Operator} from "../interfaces/IERC7540Operator.sol";
+       7                 :            : import {IncomeVaultInvariantStorage} from "../storage/IncomeVaultInvariantStorage.sol";
+       8                 :            : 
+       9                 :            : /**
+      10                 :            :  * @title Claim delegation — one capability, one namespace
+      11                 :            :  * @dev
+      12                 :            :  * A holder may authorise another address to claim on their behalf. Payouts always go to the **holder**;
+      13                 :            :  * the operator only pays the gas and chooses the moment.
+      14                 :            :  *
+      15                 :            :  * The signatures and the `OperatorSet` event are ERC-7540's, verbatim, so tooling written for that
+      16                 :            :  * standard works unchanged. The vault is **not** an asynchronous vault and does not advertise
+      17                 :            :  * {IERC7540Operator} through `supportsInterface`.
+      18                 :            :  *
+      19                 :            :  * This module owns the authorisation mapping in its own ERC-7201 namespace rather than in the
+      20                 :            :  * distribution namespace, so the two capabilities can be reasoned about — and one day inherited —
+      21                 :            :  * separately. {ERC7741Module} adds the signed variant on top and keeps a third namespace of its own for
+      22                 :            :  * the consumed nonces.
+      23                 :            :  */
+      24                 :            : abstract contract IncomeVaultOperatorModule is ContextUpgradeable, IncomeVaultInvariantStorage, IERC7540Operator {
+      25                 :            :     /* ============ ERC-7201 ============ */
+      26                 :            :     /**
+      27                 :            :      * @dev Slot holding the ERC-7201 namespaced storage of this module, derived as
+      28                 :            :      * keccak256(abi.encode(uint256(keccak256("IncomeVault.storage.Operator")) - 1)) & ~bytes32(uint256(0xff))
+      29                 :            :      * Recompute it with `SlotDerivation.erc7201Slot()` before trusting a change to it.
+      30                 :            :      */
+      31                 :            :     bytes32 private constant OperatorStorageLocation =
+      32                 :            :         0x70af7571496f61583375b861df45fee91dcc3edadeaff09b686f7920599a5500;
+      33                 :            : 
+      34                 :            :     /// @custom:storage-location erc7201:IncomeVault.storage.Operator
+      35                 :            :     struct OperatorStorage {
+      36                 :            :         // Holders that authorised another address to claim on their behalf
+      37                 :            :         mapping(address controller => mapping(address operator => bool)) _isOperator;
+      38                 :            :     }
+      39                 :            : 
+      40                 :            :     /*//////////////////////////////////////////////////////////////
+      41                 :            :                             PUBLIC/EXTERNAL FUNCTIONS
+      42                 :            :     //////////////////////////////////////////////////////////////*/
+      43                 :            :     /* ============ State functions ============ */
+      44                 :            :     /**
+      45                 :            :      * @inheritdoc IERC7540Operator
+      46                 :            :      * @dev Permissionless on purpose: a holder authorises their own operator, so there is no role to
+      47                 :            :      * check. The authorisation only lets the operator trigger a claim; the payout still goes to the
+      48                 :            :      * holder. {ERC7741Module-authorizeOperator} is the signed equivalent for a holder who cannot send
+      49                 :            :      * the transaction themselves.
+      50                 :            :      */
+      51                 :         14 :     function setOperator(address operator, bool approved) public virtual override(IERC7540Operator) returns (bool) {
+      52                 :         14 :         _setOperator(_msgSender(), operator, approved);
+      53                 :         14 :         return true;
+      54                 :            :     }
+      55                 :            : 
+      56                 :            :     /* ============ View functions ============ */
+      57                 :            :     /**
+      58                 :            :      * @inheritdoc IERC7540Operator
+      59                 :            :      */
+      60                 :         12 :     function isOperator(address controller, address operator)
+      61                 :            :         public
+      62                 :            :         view
+      63                 :            :         virtual
+      64                 :            :         override(IERC7540Operator)
+      65                 :            :         returns (bool)
+      66                 :            :     {
+      67                 :         23 :         OperatorStorage storage $ = _getOperatorStorage();
+      68                 :         23 :         return $._isOperator[controller][operator];
+      69                 :            :     }
+      70                 :            : 
+      71                 :            :     /*//////////////////////////////////////////////////////////////
+      72                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+      73                 :            :     //////////////////////////////////////////////////////////////*/
+      74                 :            :     /* ============ State functions ============ */
+      75                 :            :     /**
+      76                 :            :      * @dev Records an authorisation and emits the ERC-7540 event. The only writer of the mapping.
+      77                 :            :      * @param controller the holder granting or revoking the authorisation
+      78                 :            :      * @param operator the address being authorised
+      79                 :            :      * @param approved true to authorise, false to revoke
+      80                 :            :      */
+      81                 :         21 :     function _setOperator(address controller, address operator, bool approved) internal virtual {
+      82                 :         21 :         OperatorStorage storage $ = _getOperatorStorage();
+      83                 :         21 :         $._isOperator[controller][operator] = approved;
+      84                 :         21 :         emit OperatorSet(controller, operator, approved);
+      85                 :            :     }
+      86                 :            : 
+      87                 :            :     /* ============ View functions ============ */
+      88                 :            :     /**
+      89                 :            :      * @dev Reverts unless the caller is `holder` or an operator `holder` authorised
+      90                 :            :      * @param holder the token holder being claimed for
+      91                 :            :      */
+      92                 :         12 :     function _requireHolderOrOperator(address holder) internal view virtual {
+      93                 :         12 :         address caller = _msgSender();
+      94            [ + ]:         12 :         if (caller != holder && !isOperator(holder, caller)) {
+      95                 :          3 :             revert IncomeVault_UnauthorizedOperator(holder, caller);
+      96                 :            :         }
+      97                 :            :     }
+      98                 :            : 
+      99                 :            :     /* ============ ERC-7201 ============ */
+     100                 :            :     /**
+     101                 :            :      * @dev Returns the ERC-7201 namespaced storage of this module
+     102                 :            :      * @return $ the storage struct
+     103                 :            :      */
+     104                 :         44 :     function _getOperatorStorage() internal pure returns (OperatorStorage storage $) {
+     105                 :            :         assembly {
+     106                 :         44 :             $.slot := OperatorStorageLocation
+     107                 :            :         }
+     108                 :            :     }
+     109                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/test/coverage/test/utils/SanctionListOracle.sol.func-sort-c.html b/doc/coverage/src/modules/IncomeVaultSnapshotCore.sol.func-sort-c.html similarity index 69% rename from doc/test/coverage/test/utils/SanctionListOracle.sol.func-sort-c.html rename to doc/coverage/src/modules/IncomeVaultSnapshotCore.sol.func-sort-c.html index fd37951..cb9a78e 100644 --- a/doc/test/coverage/test/utils/SanctionListOracle.sol.func-sort-c.html +++ b/doc/coverage/src/modules/IncomeVaultSnapshotCore.sol.func-sort-c.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - test/utils/SanctionListOracle.sol - functions + LCOV - lcov.info - src/modules/IncomeVaultSnapshotCore.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - + - + - + - + - + @@ -65,20 +65,20 @@
Current view:top level - test/utils - SanctionListOracle.sol (source / functions)top level - src/modules - IncomeVaultSnapshotCore.sol (source / functions) Hitlcov.info Lines:10 333.3 %0.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:10 333.3 %0.0 %
- + - + - + - - + +

Function Name Sort by function nameFunction Name Sort by function name Hit count Sort by hit count
SanctionListOracle.addToSanctionsListIncomeVaultSnapshotCore._snapshotInfo 0
SanctionListOracle.removeFromSanctionsListIncomeVaultSnapshotCore._snapshotInfoBatch.0 0
SanctionListOracle.isSanctioned12IncomeVaultSnapshotCore._snapshotInfoBatch.10

diff --git a/doc/test/coverage/test/utils/SanctionListOracle.sol.func.html b/doc/coverage/src/modules/IncomeVaultSnapshotCore.sol.func.html similarity index 70% rename from doc/test/coverage/test/utils/SanctionListOracle.sol.func.html rename to doc/coverage/src/modules/IncomeVaultSnapshotCore.sol.func.html index 1fd3f30..1ac0d7f 100644 --- a/doc/test/coverage/test/utils/SanctionListOracle.sol.func.html +++ b/doc/coverage/src/modules/IncomeVaultSnapshotCore.sol.func.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - test/utils/SanctionListOracle.sol - functions + LCOV - lcov.info - src/modules/IncomeVaultSnapshotCore.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - + - + - + - + - + @@ -66,18 +66,18 @@ - + - + - - + + - +
Current view:top level - test/utils - SanctionListOracle.sol (source / functions)top level - src/modules - IncomeVaultSnapshotCore.sol (source / functions) Hitlcov.info Lines:10 333.3 %0.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:10 333.3 %0.0 %

Function Name Sort by function nameHit count Sort by hit countHit count Sort by hit count
SanctionListOracle.addToSanctionsListIncomeVaultSnapshotCore._snapshotInfo 0
SanctionListOracle.isSanctioned12IncomeVaultSnapshotCore._snapshotInfoBatch.00
SanctionListOracle.removeFromSanctionsListIncomeVaultSnapshotCore._snapshotInfoBatch.1 0
diff --git a/doc/test/coverage/script/CMTATWithRuleEngineScript.s.sol.gcov.html b/doc/coverage/src/modules/IncomeVaultSnapshotCore.sol.gcov.html similarity index 52% rename from doc/test/coverage/script/CMTATWithRuleEngineScript.s.sol.gcov.html rename to doc/coverage/src/modules/IncomeVaultSnapshotCore.sol.gcov.html index 30eef0f..244981e 100644 --- a/doc/test/coverage/script/CMTATWithRuleEngineScript.s.sol.gcov.html +++ b/doc/coverage/src/modules/IncomeVaultSnapshotCore.sol.gcov.html @@ -4,22 +4,22 @@ - LCOV - lcov.info - script/CMTATWithRuleEngineScript.s.sol - + LCOV - lcov.info - src/modules/IncomeVaultSnapshotCore.sol + - + - +
LCOV - code coverage report
- + @@ -32,16 +32,16 @@ - + - + - + @@ -53,12 +53,12 @@ - +
Current view:top level - script - CMTATWithRuleEngineScript.s.sol (source / functions)top level - src/modules - IncomeVaultSnapshotCore.sol (source / functions) Hit Lines: 0163 0.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions: 013 0.0 %
0 -
@@ -69,58 +69,65 @@ @@ -128,7 +135,7 @@
           Branch data     Line data    Source code
-       1                 :            : // SPDX-License-Identifier: UNLICENSED
-       2                 :            : // Documentation :
-       3                 :            : // https://book.getfoundry.sh/tutorials/solidity-scripting
-       4                 :            : pragma solidity ^0.8.17;
-       5                 :            : 
-       6                 :            : import "forge-std/Script.sol";
-       7                 :            : import "CMTAT/CMTAT_STANDALONE.sol";
-       8                 :            : import "src/RuleEngine.sol";
-       9                 :            : import "src/rules/RuleWhitelist.sol";
-      10                 :            : /**
-      11                 :            : @title Deploy a CMTAT, a RuleWhitelist and a RuleEngine
-      12                 :            : */
-      13                 :            : contract CMTATWithRuleEngineScript is Script {
-      14                 :            :     function run() external {
-      15                 :            :         // Get env variable
-      16                 :          0 :         uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
-      17                 :          0 :         address ADMIN = vm.addr(deployerPrivateKey);
-      18                 :          0 :         address trustedForwarder = address(0x0);
-      19                 :          0 :         vm.startBroadcast(deployerPrivateKey);
-      20                 :          0 :         uint256 flag = 5;
-      21                 :          0 :         uint48 initialDelay = 0;
-      22                 :          0 :         uint8 decimals = 0;
-      23                 :            :         // CMTAT
-      24                 :          0 :         CMTAT_STANDALONE CMTAT_CONTRACT = new CMTAT_STANDALONE(
-      25                 :            :             trustedForwarder,
-      26                 :            :             ADMIN,
-      27                 :            :             initialDelay,
-      28                 :            :             "CMTA Token",
-      29                 :            :             "CMTAT",
-      30                 :            :             decimals,
-      31                 :            :             "CMTAT_ISIN",
-      32                 :            :             "https://cmta.ch",
-      33                 :            :             IRuleEngine(address(0)),
-      34                 :            :             "CMTAT_info",
-      35                 :            :             flag
-      36                 :            :         );
-      37                 :          0 :         console.log("CMTAT CMTAT_CONTRACT : ", address(CMTAT_CONTRACT));
-      38                 :            :         // whitelist
-      39                 :          0 :         RuleWhitelist ruleWhitelist = new RuleWhitelist(
-      40                 :            :             ADMIN,
-      41                 :            :             trustedForwarder
-      42                 :            :         );
-      43                 :          0 :         console.log("whitelist: ", address(ruleWhitelist));
-      44                 :            :         // ruleEngine
-      45                 :          0 :         RuleEngine RULE_ENGINE = new RuleEngine(ADMIN, trustedForwarder);
-      46                 :          0 :         console.log("RuleEngine : ", address(RULE_ENGINE));
-      47                 :          0 :         RULE_ENGINE.addRule(ruleWhitelist);
-      48                 :          0 :         CMTAT_CONTRACT.setRuleEngine(RULE_ENGINE);
-      49                 :            : 
-      50                 :          0 :         vm.stopBroadcast();
-      51                 :            :     }
-      52                 :            : }
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /**
+       6                 :            :  * @title What the dividend logic needs from a snapshot provider — and nothing more
+       7                 :            :  * @dev
+       8                 :            :  * The payout paths need three answers: one holder's balance at a `time`, many holders' balances at a
+       9                 :            :  * `time`, and one holder's balances across many `time`s. This contract is those three questions, and
+      10                 :            :  * **inherits nothing**.
+      11                 :            :  *
+      12                 :            :  * Declaring them as hooks rather than as calls on a stored address is what lets a host *be* its own
+      13                 :            :  * snapshot source. A `CMTATStandaloneInternalSnapshot` already exposes `snapshotInfo` and both
+      14                 :            :  * `snapshotInfoBatch` overloads, so it answers these from itself — no external call, no stored
+      15                 :            :  * address, and no `snapshotEngine()` getter to collide with the one it already has.
+      16                 :            :  *
+      17                 :            :  * {IncomeVaultSnapshotModule} is the answer used by the standalone vault: an external
+      18                 :            :  * {ISnapshotSource} held in storage. It is one implementation, not the only one.
+      19                 :            :  */
+      20                 :            : abstract contract IncomeVaultSnapshotCore {
+      21                 :            :     /**
+      22                 :            :      * @dev Balance of one holder and the total supply, at `time`
+      23                 :            :      * @param time the dividend time
+      24                 :            :      * @param tokenHolder the holder to look up
+      25                 :            :      * @return tokenHolderBalance the holder's recorded balance
+      26                 :            :      * @return totalSupply the recorded total supply
+      27                 :            :      */
+      28                 :          0 :     function _snapshotInfo(uint256 time, address tokenHolder)
+      29                 :            :         internal
+      30                 :            :         view
+      31                 :            :         virtual
+      32                 :            :         returns (uint256 tokenHolderBalance, uint256 totalSupply);
+      33                 :            : 
+      34                 :            :     /**
+      35                 :            :      * @dev Balances of many holders and the total supply, at one `time`
+      36                 :            :      * @param time the dividend time
+      37                 :            :      * @param addresses the holders to look up
+      38                 :            :      * @return tokenHolderBalances one balance per address
+      39                 :            :      * @return totalSupply the recorded total supply
+      40                 :            :      */
+      41                 :          0 :     function _snapshotInfoBatch(uint256 time, address[] calldata addresses)
+      42                 :            :         internal
+      43                 :            :         view
+      44                 :            :         virtual
+      45                 :            :         returns (uint256[] memory tokenHolderBalances, uint256 totalSupply);
+      46                 :            : 
+      47                 :            :     /**
+      48                 :            :      * @dev Balances of holders across many `time`s
+      49                 :            :      * @param times the dividend times
+      50                 :            :      * @param addresses the holders to look up
+      51                 :            :      * @return tokenHolderBalances one row per time
+      52                 :            :      * @return totalSupplies one total supply per time
+      53                 :            :      */
+      54                 :          0 :     function _snapshotInfoBatch(uint256[] calldata times, address[] memory addresses)
+      55                 :            :         internal
+      56                 :            :         view
+      57                 :            :         virtual
+      58                 :            :         returns (uint256[][] memory tokenHolderBalances, uint256[] memory totalSupplies);
+      59                 :            : }
 
- +
Generated by: LCOV version 1.16

diff --git a/doc/coverage/src/modules/IncomeVaultSnapshotModule.sol.func-sort-c.html b/doc/coverage/src/modules/IncomeVaultSnapshotModule.sol.func-sort-c.html new file mode 100644 index 0000000..c33a9ca --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultSnapshotModule.sol.func-sort-c.html @@ -0,0 +1,117 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultSnapshotModule.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultSnapshotModule.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:262796.3 %
Date:2026-08-31 13:21:44Functions:8988.9 %
Branches:33100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultSnapshotModule._authorizeSnapshotSourceManagement0
IncomeVaultSnapshotModule.onlySnapshotSourceManager8
IncomeVaultSnapshotModule.setDividendSnapshotSource8
IncomeVaultSnapshotModule.dividendSnapshotSource9
IncomeVaultSnapshotModule._snapshotInfoBatch.177
IncomeVaultSnapshotModule._setDividendSnapshotSource291
IncomeVaultSnapshotModule._snapshotInfo586
IncomeVaultSnapshotModule._snapshotInfoBatch.01147
IncomeVaultSnapshotModule._getSnapshotSourceStorage2113
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultSnapshotModule.sol.func.html b/doc/coverage/src/modules/IncomeVaultSnapshotModule.sol.func.html new file mode 100644 index 0000000..7e33cd9 --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultSnapshotModule.sol.func.html @@ -0,0 +1,117 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultSnapshotModule.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultSnapshotModule.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:262796.3 %
Date:2026-08-31 13:21:44Functions:8988.9 %
Branches:33100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultSnapshotModule._authorizeSnapshotSourceManagement0
IncomeVaultSnapshotModule._getSnapshotSourceStorage2113
IncomeVaultSnapshotModule._setDividendSnapshotSource291
IncomeVaultSnapshotModule._snapshotInfo586
IncomeVaultSnapshotModule._snapshotInfoBatch.01147
IncomeVaultSnapshotModule._snapshotInfoBatch.177
IncomeVaultSnapshotModule.dividendSnapshotSource9
IncomeVaultSnapshotModule.onlySnapshotSourceManager8
IncomeVaultSnapshotModule.setDividendSnapshotSource8
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultSnapshotModule.sol.gcov.html b/doc/coverage/src/modules/IncomeVaultSnapshotModule.sol.gcov.html new file mode 100644 index 0000000..5db9814 --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultSnapshotModule.sol.gcov.html @@ -0,0 +1,240 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultSnapshotModule.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultSnapshotModule.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:262796.3 %
Date:2026-08-31 13:21:44Functions:8988.9 %
Branches:33100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== IncomeVault === */
+       6                 :            : import {ISnapshotSource} from "../interfaces/ISnapshotSource.sol";
+       7                 :            : import {IncomeVaultInternal} from "./IncomeVaultInternal.sol";
+       8                 :            : import {IncomeVaultSnapshotCore} from "./IncomeVaultSnapshotCore.sol";
+       9                 :            : 
+      10                 :            : /**
+      11                 :            :  * @title The standalone vault's answer to {IncomeVaultSnapshotCore} — an external source
+      12                 :            :  * @dev
+      13                 :            :  * Holds an {ISnapshotSource} and forwards the three queries to it. This is what the deployable vault
+      14                 :            :  * uses; a host that is itself the snapshot source overrides the hooks instead and never inherits this
+      15                 :            :  * module.
+      16                 :            :  *
+      17                 :            :  * The getter is deliberately **not** called `snapshotEngine()`. CMTAT's `ISnapshotEngineModule`
+      18                 :            :  * declares `snapshotEngine() returns (ISnapshotEngine)`, and a same-name, same-parameter function with
+      19                 :            :  * a *different return type* cannot be reconciled by any override — a contract inheriting both simply
+      20                 :            :  * does not compile. Naming this after the capability rather than the generic concept removes the
+      21                 :            :  * collision entirely.
+      22                 :            :  */
+      23                 :            : abstract contract IncomeVaultSnapshotModule is IncomeVaultSnapshotCore, IncomeVaultInternal {
+      24                 :            :     /* ============ Modifier ============ */
+      25                 :            :     /// @dev Restricts the replacement of the snapshot source
+      26                 :          8 :     modifier onlySnapshotSourceManager() {
+      27                 :          8 :         _authorizeSnapshotSourceManagement();
+      28                 :            :         _;
+      29                 :            :     }
+      30                 :            : 
+      31                 :            :     /* ============ ERC-7201 ============ */
+      32                 :            :     /**
+      33                 :            :      * @dev Slot holding the ERC-7201 namespaced storage of this module, derived as
+      34                 :            :      * keccak256(abi.encode(uint256(keccak256("IncomeVault.storage.SnapshotSource")) - 1)) & ~bytes32(uint256(0xff))
+      35                 :            :      * Recompute it with `SlotDerivation.erc7201Slot()` before trusting a change to it.
+      36                 :            :      */
+      37                 :            :     bytes32 private constant SnapshotSourceStorageLocation =
+      38                 :            :         0x45a69a32b5b7efb4ae8ac48e2427653ef15920a29875121a072e6b49aaccac00;
+      39                 :            : 
+      40                 :            :     /* ==== ERC-7201 State Variables === */
+      41                 :            :     /// @custom:storage-location erc7201:IncomeVault.storage.SnapshotSource
+      42                 :            :     struct SnapshotSourceStorage {
+      43                 :            :         // Where the holder balances are read from
+      44                 :            :         ISnapshotSource _source;
+      45                 :            :     }
+      46                 :            : 
+      47                 :            :     /*//////////////////////////////////////////////////////////////
+      48                 :            :                             PUBLIC/EXTERNAL FUNCTIONS
+      49                 :            :     //////////////////////////////////////////////////////////////*/
+      50                 :            :     /* ============ State functions ============ */
+      51                 :            :     /**
+      52                 :            :      * @notice Replace the contract the vault reads the holder balances from
+      53                 :            :      * @dev
+      54                 :            :      * Only accepted while **no claim period is open** — `openClaimCount()` must be zero. Changing the
+      55                 :            :      * source under an open period would silently re-price every unclaimed dividend of that period,
+      56                 :            :      * because the amounts are computed from the source at claim time, not fixed at deposit.
+      57                 :            :      *
+      58                 :            :      * @custom:security The restriction narrows the hazard, it does not remove it: entitlements resolve
+      59                 :            :      * against whichever source is configured *when the claim happens*, so re-opening a past `time`
+      60                 :            :      * after a swap resolves it against the new source. Holders who already claimed are protected;
+      61                 :            :      * holders who had not are not.
+      62                 :            :      *
+      63                 :            :      * @param source the new snapshot source, must implement {ISnapshotSource} and be non-zero
+      64                 :            :      */
+      65                 :          8 :     function setDividendSnapshotSource(ISnapshotSource source) public virtual onlySnapshotSourceManager {
+      66                 :          6 :         uint256 open = openClaimCount();
+      67            [ + ]:          6 :         if (open != 0) {
+      68                 :          1 :             revert IncomeVault_ClaimPeriodOpen(open);
+      69                 :            :         }
+      70            [ + ]:          5 :         if (address(source) == address(dividendSnapshotSource())) {
+      71                 :          1 :             revert IncomeVault_SameValue();
+      72                 :            :         }
+      73                 :          4 :         _setDividendSnapshotSource(source);
+      74                 :            :     }
+      75                 :            : 
+      76                 :            :     /* ============ View functions ============ */
+      77                 :            :     /**
+      78                 :            :      * @notice The contract the vault reads the holder balances from
+      79                 :            :      * @return The configured {ISnapshotSource}
+      80                 :            :      */
+      81                 :          9 :     function dividendSnapshotSource() public view virtual returns (ISnapshotSource) {
+      82                 :       1824 :         SnapshotSourceStorage storage $ = _getSnapshotSourceStorage();
+      83                 :       1824 :         return $._source;
+      84                 :            :     }
+      85                 :            : 
+      86                 :            :     /*//////////////////////////////////////////////////////////////
+      87                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+      88                 :            :     //////////////////////////////////////////////////////////////*/
+      89                 :            :     /* ============ State functions ============ */
+      90                 :            :     /**
+      91                 :            :      * @notice Sets the snapshot source used to compute the dividends
+      92                 :            :      * @dev reverts if `source` is the zero address
+      93                 :            :      * @param source any contract implementing {ISnapshotSource}
+      94                 :            :      */
+      95                 :        291 :     function _setDividendSnapshotSource(ISnapshotSource source) internal virtual {
+      96            [ + ]:        291 :         if (address(source) == address(0)) {
+      97                 :          2 :             revert IncomeVault_SnapshotSourceWithAddressZeroNotAllowed();
+      98                 :            :         }
+      99                 :        289 :         SnapshotSourceStorage storage $ = _getSnapshotSourceStorage();
+     100                 :        289 :         $._source = source;
+     101                 :        289 :         emit DividendSnapshotSourceSet(source);
+     102                 :            :     }
+     103                 :            : 
+     104                 :            :     /* ============ View functions ============ */
+     105                 :            :     /// @inheritdoc IncomeVaultSnapshotCore
+     106                 :        586 :     function _snapshotInfo(uint256 time, address tokenHolder)
+     107                 :            :         internal
+     108                 :            :         view
+     109                 :            :         virtual
+     110                 :            :         override
+     111                 :            :         returns (uint256, uint256)
+     112                 :            :     {
+     113                 :        586 :         return dividendSnapshotSource().snapshotInfo(time, tokenHolder);
+     114                 :            :     }
+     115                 :            : 
+     116                 :            :     /// @inheritdoc IncomeVaultSnapshotCore
+     117                 :       1147 :     function _snapshotInfoBatch(uint256 time, address[] calldata addresses)
+     118                 :            :         internal
+     119                 :            :         view
+     120                 :            :         virtual
+     121                 :            :         override
+     122                 :            :         returns (uint256[] memory, uint256)
+     123                 :            :     {
+     124                 :       1147 :         return dividendSnapshotSource().snapshotInfoBatch(time, addresses);
+     125                 :            :     }
+     126                 :            : 
+     127                 :            :     /// @inheritdoc IncomeVaultSnapshotCore
+     128                 :         77 :     function _snapshotInfoBatch(uint256[] calldata times, address[] memory addresses)
+     129                 :            :         internal
+     130                 :            :         view
+     131                 :            :         virtual
+     132                 :            :         override
+     133                 :            :         returns (uint256[][] memory, uint256[] memory)
+     134                 :            :     {
+     135                 :         77 :         return dividendSnapshotSource().snapshotInfoBatch(times, addresses);
+     136                 :            :     }
+     137                 :            : 
+     138                 :            :     /* ============ Access Control ============ */
+     139                 :            :     /**
+     140                 :            :      * @dev Authorization hook invoked before {setDividendSnapshotSource}.
+     141                 :            :      * Implemented by the deployment contract with the desired access-control policy.
+     142                 :            :      */
+     143                 :          0 :     function _authorizeSnapshotSourceManagement() internal view virtual;
+     144                 :            : 
+     145                 :            :     /* ============ ERC-7201 ============ */
+     146                 :            :     /**
+     147                 :            :      * @dev Returns the ERC-7201 namespaced storage of this module
+     148                 :            :      * @return $ the storage struct
+     149                 :            :      */
+     150                 :       2113 :     function _getSnapshotSourceStorage() internal pure returns (SnapshotSourceStorage storage $) {
+     151                 :            :         assembly {
+     152                 :       2113 :             $.slot := SnapshotSourceStorageLocation
+     153                 :            :         }
+     154                 :            :     }
+     155                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultValidationCore.sol.func-sort-c.html b/doc/coverage/src/modules/IncomeVaultValidationCore.sol.func-sort-c.html new file mode 100644 index 0000000..3b3305d --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultValidationCore.sol.func-sort-c.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultValidationCore.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultValidationCore.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:010.0 %
Date:2026-08-31 13:21:44Functions:010.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultValidationCore._validateTransfer0
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/IncomeVaultValidationCore.sol.func.html b/doc/coverage/src/modules/IncomeVaultValidationCore.sol.func.html new file mode 100644 index 0000000..b14511b --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultValidationCore.sol.func.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultValidationCore.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultValidationCore.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:010.0 %
Date:2026-08-31 13:21:44Functions:010.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultValidationCore._validateTransfer0
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/test/coverage/test/utils/SanctionListOracle.sol.gcov.html b/doc/coverage/src/modules/IncomeVaultValidationCore.sol.gcov.html similarity index 57% rename from doc/test/coverage/test/utils/SanctionListOracle.sol.gcov.html rename to doc/coverage/src/modules/IncomeVaultValidationCore.sol.gcov.html index 7068b2c..ed85934 100644 --- a/doc/test/coverage/test/utils/SanctionListOracle.sol.gcov.html +++ b/doc/coverage/src/modules/IncomeVaultValidationCore.sol.gcov.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - test/utils/SanctionListOracle.sol + LCOV - lcov.info - src/modules/IncomeVaultValidationCore.sol @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ + - - + - + + - - + @@ -70,31 +70,36 @@
           Branch data     Line data    Source code
        1                 :            : // SPDX-License-Identifier: MPL-2.0
-       2                 :            : pragma solidity ^0.8.20;
-       3                 :            : 
-       4                 :            : /**
-       5                 :            : * @notice Test contract from
-       6                 :            : https://etherscan.io/address/0x40c57923924b5c5c5455c48d93317139addac8fb#code
-       7                 :            : */
-       8                 :            : contract SanctionListOracle {
-       9                 :            : 
-      10                 :            :   constructor() {}
-      11                 :            : 
-      12                 :            :   mapping(address => bool) private sanctionedAddresses;
-      13                 :            : 
-      14                 :            : 
-      15                 :            :   function addToSanctionsList(address newSanction) public{
-      16                 :          0 :       sanctionedAddresses[newSanction] = true;  
-      17                 :            :   }
-      18                 :            : 
-      19                 :            :   function removeFromSanctionsList(address removeSanction) public{
-      20                 :          0 :       sanctionedAddresses[removeSanction] = true;
-      21                 :            :   }
-      22                 :            : 
-      23                 :            :   function isSanctioned(address addr) public view returns (bool) {
-      24                 :         12 :     return sanctionedAddresses[addr] == true ;
-      25                 :            :   }
-      26                 :            : }
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /**
+       6                 :            :  * @title What the dividend logic needs from a transfer-restriction policy — and nothing more
+       7                 :            :  * @dev
+       8                 :            :  * The payout paths ask one question before moving tokens: *may this payout proceed?* This contract is
+       9                 :            :  * that question, and only that question. It **inherits nothing**, which is the point: a host embedding
+      10                 :            :  * the dividend logic — a CMTAT that already has pause, freeze and a RuleEngine — answers from the
+      11                 :            :  * modules it already owns instead of inheriting a second copy.
+      12                 :            :  *
+      13                 :            :  * {IncomeVaultValidationModule} is the answer used by the standalone vault, built on the CMTAT
+      14                 :            :  * modules. It is one implementation, not the only one.
+      15                 :            :  *
+      16                 :            :  * This is the same authorization-hook pattern the project uses for access control, applied to the
+      17                 :            :  * other dependency that was previously hard-wired. Before the split, {IncomeVaultOpen} and
+      18                 :            :  * {IncomeVaultRestricted} each inherited the CMTAT `PauseModule` and `EnforcementModule` transitively,
+      19                 :            :  * so **no CMTAT could ever embed them** — C3 linearization had no solution and the compiler rejected
+      20                 :            :  * the combination with `Error (5005)`, which no override or ordering can repair.
+      21                 :            :  */
+      22                 :            : abstract contract IncomeVaultValidationCore {
+      23                 :            :     /**
+      24                 :            :      * @dev Reverts if the vault may not pay `value` to `to`. Implemented by the deployment — or by the
+      25                 :            :      * host contract, when the dividend logic is embedded in one.
+      26                 :            :      * @param from the address sending the payment, always the vault itself
+      27                 :            :      * @param to the token holder receiving the dividends
+      28                 :            :      * @param value the amount of payment token
+      29                 :            :      */
+      30                 :          0 :     function _validateTransfer(address from, address to, uint256 value) internal view virtual;
+      31                 :            : }
 
diff --git a/doc/test/coverage/src/rules/RuleWhitelist.sol.func-sort-c.html b/doc/coverage/src/modules/IncomeVaultValidationModule.sol.func-sort-c.html similarity index 50% rename from doc/test/coverage/src/rules/RuleWhitelist.sol.func-sort-c.html rename to doc/coverage/src/modules/IncomeVaultValidationModule.sol.func-sort-c.html index 6d88893..a1aebf1 100644 --- a/doc/test/coverage/src/rules/RuleWhitelist.sol.func-sort-c.html +++ b/doc/coverage/src/modules/IncomeVaultValidationModule.sol.func-sort-c.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/rules/RuleWhitelist.sol - functions + LCOV - lcov.info - src/modules/IncomeVaultValidationModule.sol - functions @@ -19,7 +19,7 @@
Current view:top level - test/utils - SanctionListOracle.sol (source / functions)top level - src/modules - IncomeVaultValidationCore.sol (source / functions) Hitlcov.info Lines:0 1333.3 %0.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:0 1333.3 %0.0 %
- + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - + +
Current view:top level - src/rules - RuleWhitelist.sol (source / functions)top level - src/modules - IncomeVaultValidationModule.sol (source / functions) Hitlcov.info Lines:373897.4 %484998.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:111291.7 %7887.5 %
Branches:14 1687.5 %16100.0 %
@@ -65,56 +65,40 @@ - + - + - - + + - - + + - - + + - - - - - - - - - - - - - + - + - - - - - - + + - - + +

Function Name Sort by function nameFunction Name Sort by function name Hit count Sort by hit count
RuleWhitelist._msgDataIncomeVaultValidationModule._authorizeRuleEngineManagement 0
RuleWhitelist.removeAddressFromTheWhitelist3IncomeVaultValidationModule.canTransfer11
RuleWhitelist.validateTransfer3IncomeVaultValidationModule.messageForTransferRestriction14
RuleWhitelist.removeAddressesFromTheWhitelist4IncomeVaultValidationModule.detectTransferRestriction15
RuleWhitelist.messageForTransferRestriction7
RuleWhitelist.canReturnTransferRestrictionCode8
RuleWhitelist.addAddressesToTheWhitelist13
RuleWhitelist.detectTransferRestrictionIncomeVaultValidationModule.onlyRuleEngineManager 20
RuleWhitelist.numberWhitelistedAddressIncomeVaultValidationModule.setRuleEngine 20
RuleWhitelist.addAddressToTheWhitelist22
RuleWhitelist.addressIsWhitelisted50IncomeVaultValidationModule.__IncomeVaultValidation_init_unchained285
RuleWhitelist._msgSender52IncomeVaultValidationModule._validateTransfer1588

diff --git a/doc/test/coverage/src/rules/RuleWhitelist.sol.func.html b/doc/coverage/src/modules/IncomeVaultValidationModule.sol.func.html similarity index 50% rename from doc/test/coverage/src/rules/RuleWhitelist.sol.func.html rename to doc/coverage/src/modules/IncomeVaultValidationModule.sol.func.html index 0b40c22..12e1521 100644 --- a/doc/test/coverage/src/rules/RuleWhitelist.sol.func.html +++ b/doc/coverage/src/modules/IncomeVaultValidationModule.sol.func.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/rules/RuleWhitelist.sol - functions + LCOV - lcov.info - src/modules/IncomeVaultValidationModule.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - + +
Current view:top level - src/rules - RuleWhitelist.sol (source / functions)top level - src/modules - IncomeVaultValidationModule.sol (source / functions) Hitlcov.info Lines:373897.4 %484998.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:111291.7 %7887.5 %
Branches:14 1687.5 %16100.0 %
@@ -66,56 +66,40 @@
Function Name Sort by function name - Hit count Sort by hit count + Hit count Sort by hit count - RuleWhitelist._msgData - 0 + IncomeVaultValidationModule.__IncomeVaultValidation_init_unchained + 285 - RuleWhitelist._msgSender - 52 + IncomeVaultValidationModule._authorizeRuleEngineManagement + 0 - RuleWhitelist.addAddressToTheWhitelist - 22 + IncomeVaultValidationModule._validateTransfer + 1588 - RuleWhitelist.addAddressesToTheWhitelist - 13 + IncomeVaultValidationModule.canTransfer + 11 - RuleWhitelist.addressIsWhitelisted - 50 + IncomeVaultValidationModule.detectTransferRestriction + 15 - RuleWhitelist.canReturnTransferRestrictionCode - 8 + IncomeVaultValidationModule.messageForTransferRestriction + 14 - RuleWhitelist.detectTransferRestriction + IncomeVaultValidationModule.onlyRuleEngineManager 20 - RuleWhitelist.messageForTransferRestriction - 7 - - - RuleWhitelist.numberWhitelistedAddress + IncomeVaultValidationModule.setRuleEngine 20 - - RuleWhitelist.removeAddressFromTheWhitelist - 3 - - - RuleWhitelist.removeAddressesFromTheWhitelist - 4 - - - RuleWhitelist.validateTransfer - 3 -
diff --git a/doc/coverage/src/modules/IncomeVaultValidationModule.sol.gcov.html b/doc/coverage/src/modules/IncomeVaultValidationModule.sol.gcov.html new file mode 100644 index 0000000..adb2e82 --- /dev/null +++ b/doc/coverage/src/modules/IncomeVaultValidationModule.sol.gcov.html @@ -0,0 +1,302 @@ + + + + + + + LCOV - lcov.info - src/modules/IncomeVaultValidationModule.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - IncomeVaultValidationModule.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:484998.0 %
Date:2026-08-31 13:21:44Functions:7887.5 %
Branches:1616100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== CMTAT modules === */
+       6                 :            : import {PauseModule} from "CMTAT/modules/wrapper/core/PauseModule.sol";
+       7                 :            : import {EnforcementModule} from "CMTAT/modules/wrapper/core/EnforcementModule.sol";
+       8                 :            : import {ValidationModuleRuleEngineInternal} from "CMTAT/modules/internal/ValidationModuleRuleEngineInternal.sol";
+       9                 :            : /* ==== CMTAT engine === */
+      10                 :            : import {IRuleEngine, IRuleEngineERC1404} from "CMTAT/interfaces/engine/IRuleEngine.sol";
+      11                 :            : import {IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol";
+      12                 :            : /* ==== IncomeVault === */
+      13                 :            : import {IncomeVaultInvariantStorage} from "../storage/IncomeVaultInvariantStorage.sol";
+      14                 :            : import {IncomeVaultValidationCore} from "./IncomeVaultValidationCore.sol";
+      15                 :            : 
+      16                 :            : /**
+      17                 :            :  * @title The standalone vault's answer to {IncomeVaultValidationCore}
+      18                 :            :  * @dev
+      19                 :            :  * A dividend payout is treated as a transfer from the vault to the token holder and can be
+      20                 :            :  * restricted the same way a CMTAT transfer is:
+      21                 :            :  *
+      22                 :            :  * - the vault can be put in the pause state ({PauseModule}),
+      23                 :            :  * - an address can be frozen ({EnforcementModule}),
+      24                 :            :  * - an optional {IRuleEngine} can apply arbitrary rules (allowlist, blocklist, sanction list, ...).
+      25                 :            :  *
+      26                 :            :  * Unlike the CMTAT, the vault is not a token bound to the RuleEngine: it only uses the *view*
+      27                 :            :  * entry point {IRuleEngine-canTransfer}. `transferred()` is restricted to bound tokens by the
+      28                 :            :  * RuleEngine and would revert here, and a payout is not a movement of the security token, so it
+      29                 :            :  * must not update the stateful rules of the engine.
+      30                 :            :  */
+      31                 :            : abstract contract IncomeVaultValidationModule is
+      32                 :            :     IncomeVaultValidationCore,
+      33                 :            :     PauseModule,
+      34                 :            :     EnforcementModule,
+      35                 :            :     ValidationModuleRuleEngineInternal,
+      36                 :            :     IncomeVaultInvariantStorage
+      37                 :            : {
+      38                 :            :     /* ============ State variables ============ */
+      39                 :            :     /**
+      40                 :            :      * @dev Human-readable answers for {messageForTransferRestriction}. The strings are CMTAT's
+      41                 :            :      * (`ValidationModuleERC1404`) verbatim, so an operator console written against a CMTAT reads a
+      42                 :            :      * payout refusal exactly as it reads a transfer refusal. The codes are CMTAT's
+      43                 :            :      * `REJECTED_CODE_BASE`, for the same reason.
+      44                 :            :      */
+      45                 :            :     string internal constant TEXT_TRANSFER_OK = "NoRestriction";
+      46                 :            :     /// @dev Returned when no configured source claims the code
+      47                 :            :     string internal constant TEXT_UNKNOWN_CODE = "UnknownCode";
+      48                 :            :     /// @dev The vault is paused
+      49                 :            :     string internal constant TEXT_TRANSFER_REJECTED_PAUSED = "EnforcedPause";
+      50                 :            :     /// @dev The vault has been permanently deactivated
+      51                 :            :     string internal constant TEXT_TRANSFER_REJECTED_DEACTIVATED = "ContractDeactivated";
+      52                 :            :     /// @dev The paying address is frozen
+      53                 :            :     string internal constant TEXT_TRANSFER_REJECTED_FROM_FROZEN = "AddrFromIsFrozen";
+      54                 :            :     /// @dev The receiving token holder is frozen
+      55                 :            :     string internal constant TEXT_TRANSFER_REJECTED_TO_FROZEN = "AddrToIsFrozen";
+      56                 :            : 
+      57                 :            :     /* ============ Modifier ============ */
+      58                 :            :     /// @dev Restricts the management of the RuleEngine
+      59                 :         20 :     modifier onlyRuleEngineManager() {
+      60                 :         20 :         _authorizeRuleEngineManagement();
+      61                 :            :         _;
+      62                 :            :     }
+      63                 :            : 
+      64                 :            :     /* ============  Initializer Function ============ */
+      65                 :            :     /**
+      66                 :            :      * @notice Initializes the validation module
+      67                 :            :      * @dev Writes the RuleEngine slot that CMTAT's {ValidationModuleRuleEngineInternal} owns, at its
+      68                 :            :      * hardcoded ERC-7201 location. In the standalone vault that slot belongs to this contract alone. In
+      69                 :            :      * a host that also inherits a CMTAT validation stack it is **shared**, so a non-zero `ruleEngine_`
+      70                 :            :      * here would replace the *token's* compliance engine from the dividend initializer. Such a host must
+      71                 :            :      * pass the zero address, which CMTAT's initializer treats as a no-op, and keep the engine the token
+      72                 :            :      * already configured. Embedding the payout logic via {IncomeVaultValidationCore} instead avoids the
+      73                 :            :      * question entirely, and is the supported route. Finding M-4.
+      74                 :            :      * @param ruleEngine_ the RuleEngine applied to the payouts, or the zero address for none
+      75                 :            :      */
+      76                 :        285 :     function __IncomeVaultValidation_init_unchained(IRuleEngine ruleEngine_) internal onlyInitializing {
+      77                 :        285 :         ValidationModuleRuleEngineInternal.__ValidationRuleEngine_init_unchained(ruleEngine_);
+      78                 :            :     }
+      79                 :            : 
+      80                 :            :     /*//////////////////////////////////////////////////////////////
+      81                 :            :                             PUBLIC/EXTERNAL FUNCTIONS
+      82                 :            :     //////////////////////////////////////////////////////////////*/
+      83                 :            :     /* ============ State functions ============ */
+      84                 :            :     /**
+      85                 :            :      * @notice Updates the RuleEngine applied to the dividend payouts.
+      86                 :            :      * @param ruleEngine_ the new RuleEngine, or the zero address to disable the rule checks
+      87                 :            :      */
+      88                 :         20 :     function setRuleEngine(IRuleEngine ruleEngine_) public virtual onlyRuleEngineManager {
+      89            [ + ]:         17 :         if (address(ruleEngine_) == address(ruleEngine())) {
+      90                 :          1 :             revert IncomeVault_SameValue();
+      91                 :            :         }
+      92                 :         16 :         _setRuleEngine(ruleEngine_);
+      93                 :            :     }
+      94                 :            : 
+      95                 :            :     /* ============ View functions ============ */
+      96                 :            :     /**
+      97                 :            :      * @notice Returns true if the vault is allowed to pay `value` to `to`.
+      98                 :            :      * @param from the address sending the payment, always the vault itself
+      99                 :            :      * @param to the token holder receiving the dividends
+     100                 :            :      * @param value the amount of payment token
+     101                 :            :      * @return True if the pause, freeze and RuleEngine checks all allow the payout
+     102                 :            :      */
+     103                 :         11 :     function canTransfer(address from, address to, uint256 value) public view virtual returns (bool) {
+     104            [ + ]:       1599 :         if (PauseModule.paused()) {
+     105                 :        887 :             return false;
+     106                 :            :         }
+     107            [ + ]:        712 :         if (EnforcementModule.isFrozen(from) || EnforcementModule.isFrozen(to)) {
+     108                 :        218 :             return false;
+     109                 :            :         }
+     110                 :        494 :         IRuleEngine ruleEngine_ = ruleEngine();
+     111            [ + ]:        494 :         if (address(ruleEngine_) != address(0)) {
+     112                 :          8 :             return ruleEngine_.canTransfer(from, to, value);
+     113                 :            :         }
+     114                 :        486 :         return true;
+     115                 :            :     }
+     116                 :            : 
+     117                 :            :     /**
+     118                 :            :      * @notice ERC-1404 restriction code for a payout from the vault, or `0` when it would be accepted.
+     119                 :            :      * @dev Answers for the **whole** payout decision, in the same order {canTransfer} evaluates it:
+     120                 :            :      * deactivation, pause, either party frozen, then the RuleEngine. The codes are CMTAT's
+     121                 :            :      * `REJECTED_CODE_BASE`, so a caller written against a CMTAT reads them unchanged.
+     122                 :            :      *
+     123                 :            :      * This returns `0` exactly when {canTransfer} returns true, and the two must not be allowed to
+     124                 :            :      * drift apart: consulting only the RuleEngine here would report a paused vault or a frozen holder as
+     125                 :            :      * unrestricted, and the claim would then revert.
+     126                 :            :      * @param from the address sending the payment, always the vault itself
+     127                 :            :      * @param to the token holder receiving the dividends
+     128                 :            :      * @param value the amount of payment token
+     129                 :            :      * @return The ERC-1404 restriction code, `0` when the rules allow the payout
+     130                 :            :      */
+     131                 :         15 :     function detectTransferRestriction(address from, address to, uint256 value) public view virtual returns (uint8) {
+     132                 :            :         // Deactivation implies pause, so the more specific code is tested first.
+     133            [ + ]:         15 :         if (PauseModule.deactivated()) {
+     134                 :          2 :             return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_DEACTIVATED);
+     135                 :            :         }
+     136            [ + ]:         13 :         if (PauseModule.paused()) {
+     137                 :          2 :             return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_PAUSED);
+     138                 :            :         }
+     139            [ + ]:         11 :         if (EnforcementModule.isFrozen(from)) {
+     140                 :          2 :             return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_FROM_FROZEN);
+     141                 :            :         }
+     142            [ + ]:          9 :         if (EnforcementModule.isFrozen(to)) {
+     143                 :          2 :             return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_TO_FROZEN);
+     144                 :            :         }
+     145                 :          7 :         IRuleEngine ruleEngine_ = ruleEngine();
+     146            [ + ]:          7 :         if (address(ruleEngine_) == address(0)) {
+     147                 :          3 :             return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+     148                 :            :         }
+     149                 :          4 :         return IRuleEngineERC1404(address(ruleEngine_)).detectTransferRestriction(from, to, value);
+     150                 :            :     }
+     151                 :            : 
+     152                 :            :     /**
+     153                 :            :      * @notice Human readable message matching a code returned by {detectTransferRestriction}.
+     154                 :            :      * @param restrictionCode the ERC-1404 restriction code to translate
+     155                 :            :      * @return The message associated with `restrictionCode`
+     156                 :            :      */
+     157                 :         14 :     function messageForTransferRestriction(uint8 restrictionCode) public view virtual returns (string memory) {
+     158            [ + ]:         14 :         if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK)) {
+     159                 :          3 :             return TEXT_TRANSFER_OK;
+     160                 :            :         }
+     161            [ + ]:         11 :         if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_DEACTIVATED)) {
+     162                 :          1 :             return TEXT_TRANSFER_REJECTED_DEACTIVATED;
+     163                 :            :         }
+     164            [ + ]:         10 :         if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_PAUSED)) {
+     165                 :          2 :             return TEXT_TRANSFER_REJECTED_PAUSED;
+     166                 :            :         }
+     167            [ + ]:          8 :         if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_FROM_FROZEN)) {
+     168                 :          2 :             return TEXT_TRANSFER_REJECTED_FROM_FROZEN;
+     169                 :            :         }
+     170            [ + ]:          6 :         if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_TO_FROZEN)) {
+     171                 :          1 :             return TEXT_TRANSFER_REJECTED_TO_FROZEN;
+     172                 :            :         }
+     173                 :          5 :         IRuleEngine ruleEngine_ = ruleEngine();
+     174            [ + ]:          5 :         if (address(ruleEngine_) == address(0)) {
+     175                 :            :             // The vault answers for its own codes above; anything else could only have come from a
+     176                 :            :             // RuleEngine, and there is none. Saying "no restriction" here would repeat the defect
+     177                 :            :             // this function's siblings were fixed for.
+     178                 :          2 :             return TEXT_UNKNOWN_CODE;
+     179                 :            :         }
+     180                 :          3 :         return IRuleEngineERC1404(address(ruleEngine_)).messageForTransferRestriction(restrictionCode);
+     181                 :            :     }
+     182                 :            : 
+     183                 :            :     /*//////////////////////////////////////////////////////////////
+     184                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+     185                 :            :     //////////////////////////////////////////////////////////////*/
+     186                 :            :     /* ============ Access Control ============ */
+     187                 :            :     /**
+     188                 :            :      * @dev Authorization hook invoked before {setRuleEngine}.
+     189                 :            :      * Implemented by the deployment contract with the desired access-control policy.
+     190                 :            :      *
+     191                 :            :      * @dev CMTAT's {ValidationModuleRuleEngine} declares a hook with this same name and parameters.
+     192                 :            :      * That is **not** a collision to be renamed away: both this module and CMTAT's wrapper sit on the
+     193                 :            :      * same {ValidationModuleRuleEngineInternal}, whose ERC-7201 slot is a hardcoded constant, so a
+     194                 :            :      * contract inheriting both has exactly **one** RuleEngine. One capability, therefore one hook — and
+     195                 :            :      * a single override answering both declarations is the correct resolution, not an accident. Giving
+     196                 :            :      * the two hooks different names would create two doors to one slot, each able to carry a different
+     197                 :            :      * policy, and the weaker one would win. See finding M-4.
+     198                 :            :      */
+     199                 :          0 :     function _authorizeRuleEngineManagement() internal view virtual;
+     200                 :            : 
+     201                 :            :     /* ============ View functions ============ */
+     202                 :            :     /**
+     203                 :            :      * @inheritdoc IncomeVaultValidationCore
+     204                 :            :      * @dev The standalone vault's answer: its own pause state, the frozen status of both parties, and
+     205                 :            :      * the RuleEngine if one is configured.
+     206                 :            :      */
+     207                 :       1588 :     function _validateTransfer(address from, address to, uint256 value)
+     208                 :            :         internal
+     209                 :            :         view
+     210                 :            :         virtual
+     211                 :            :         override(IncomeVaultValidationCore)
+     212                 :            :     {
+     213            [ + ]:       1588 :         if (!canTransfer(from, to, value)) {
+     214                 :       1103 :             revert IncomeVault_InvalidTransfer(from, to, value);
+     215                 :            :         }
+     216                 :            :     }
+     217                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/Ownable2StepERC165Module.sol.func-sort-c.html b/doc/coverage/src/modules/Ownable2StepERC165Module.sol.func-sort-c.html new file mode 100644 index 0000000..5c05cd4 --- /dev/null +++ b/doc/coverage/src/modules/Ownable2StepERC165Module.sol.func-sort-c.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - src/modules/Ownable2StepERC165Module.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - Ownable2StepERC165Module.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:33100.0 %
Date:2026-08-31 13:21:44Functions:11100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
Ownable2StepERC165Module.supportsInterface3
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/Ownable2StepERC165Module.sol.func.html b/doc/coverage/src/modules/Ownable2StepERC165Module.sol.func.html new file mode 100644 index 0000000..8378683 --- /dev/null +++ b/doc/coverage/src/modules/Ownable2StepERC165Module.sol.func.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - src/modules/Ownable2StepERC165Module.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - Ownable2StepERC165Module.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:33100.0 %
Date:2026-08-31 13:21:44Functions:11100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
Ownable2StepERC165Module.supportsInterface3
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/test/coverage/script/RuleEngineScript.s.sol.gcov.html b/doc/coverage/src/modules/Ownable2StepERC165Module.sol.gcov.html similarity index 52% rename from doc/test/coverage/script/RuleEngineScript.s.sol.gcov.html rename to doc/coverage/src/modules/Ownable2StepERC165Module.sol.gcov.html index c6daceb..b6fae45 100644 --- a/doc/test/coverage/script/RuleEngineScript.s.sol.gcov.html +++ b/doc/coverage/src/modules/Ownable2StepERC165Module.sol.gcov.html @@ -4,22 +4,22 @@ - LCOV - lcov.info - script/RuleEngineScript.s.sol - + LCOV - lcov.info - src/modules/Ownable2StepERC165Module.sol + - + - +
LCOV - code coverage report
- + @@ -31,18 +31,18 @@ - - - + + + - + - - + + @@ -50,15 +50,15 @@ - - + + - +
Current view:top level - script - RuleEngineScript.s.sol (source / functions)top level - src/modules - Ownable2StepERC165Module.sol (source / functions) Hitlcov.info Lines:0120.0 %33100.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:0 10.0 %1100.0 %
Branches: 020.0 %0-
@@ -69,40 +69,40 @@ - + - - - - - - + + + + + + - + - - - - + + + + - +
           Branch data     Line data    Source code
-       1                 :            : // SPDX-License-Identifier: UNLICENSED
-       2                 :            : // Documentation :
-       3                 :            : // https://book.getfoundry.sh/tutorials/solidity-scripting
-       4                 :            : pragma solidity ^0.8.17;
-       5                 :            : 
-       6                 :            : import "forge-std/Script.sol";
-       7                 :            : import "CMTAT/CMTAT_STANDALONE.sol";
-       8                 :            : import "src/RuleEngine.sol";
-       9                 :            : import "src/rules/RuleWhitelist.sol";
-      10                 :            : import "CMTAT/modules/wrapper/controllers/ValidationModule.sol";
-      11                 :            : 
-      12                 :            : /**
-      13                 :            : @title Deploy a RuleWhitelist and a RuleEngine. The CMTAT is considred already deployed
-      14                 :            : */
-      15                 :            : contract RuleEngineScript is Script {
-      16                 :            :     function run() external {
-      17                 :            :         // Get env variable
-      18                 :          0 :         uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
-      19                 :          0 :         address ADMIN = vm.addr(deployerPrivateKey);
-      20                 :          0 :         address CMTAT_Address = vm.envAddress("CMTAT_ADDRESS");
-      21                 :          0 :         vm.startBroadcast(deployerPrivateKey);
-      22                 :            :         //whitelist
-      23                 :          0 :         RuleWhitelist ruleWhitelist = new RuleWhitelist(ADMIN, address(0));
-      24                 :          0 :         console.log("whitelist: ", address(ruleWhitelist));
-      25                 :            :         // ruleEngine
-      26                 :          0 :         RuleEngine RULE_ENGINE = new RuleEngine(ADMIN, address(0));
-      27                 :          0 :         console.log("RuleEngine: ", address(RULE_ENGINE));
-      28                 :          0 :         RULE_ENGINE.addRule(ruleWhitelist);
-      29                 :            :         // Configure the new ruleEngine for CMTAT
-      30                 :          0 :         (bool success, ) = address(CMTAT_Address).call(
-      31                 :            :             abi.encodeCall(ValidationModule.setRuleEngine, RULE_ENGINE)
-      32                 :            :         );
-      33         [ #  # ]:          0 :         require(success);
-      34                 :          0 :         vm.stopBroadcast();
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol";
+       7                 :            : 
+       8                 :            : /**
+       9                 :            :  * @title ERC-165 advertisement of the ERC-173 / Ownable2Step access control
+      10                 :            :  * @dev
+      11                 :            :  * Kept in its own module so it is declared once instead of being repeated in every Ownable variant.
+      12                 :            :  * The two identifiers are hardcoded because `type(I).interfaceId` XORs only the selectors declared
+      13                 :            :  * directly on the interface, and OpenZeppelin ships no `IERC173` interface to compute them from.
+      14                 :            :  */
+      15                 :            : abstract contract Ownable2StepERC165Module is ERC165Upgradeable {
+      16                 :            :     /**
+      17                 :            :      * @notice ERC-165 interface ID of ERC-173 (contract ownership standard)
+      18                 :            :      * @dev bytes4(keccak256("owner()")) ^ bytes4(keccak256("transferOwnership(address)"))
+      19                 :            :      */
+      20                 :            :     bytes4 public constant IERC173_INTERFACE_ID = 0x7f5828d0;
+      21                 :            :     /**
+      22                 :            :      * @notice ERC-165 interface ID of the Ownable2Step-specific functions
+      23                 :            :      * @dev bytes4(keccak256("acceptOwnership()")) ^ bytes4(keccak256("pendingOwner()"))
+      24                 :            :      */
+      25                 :            :     bytes4 public constant IOWNABLE2STEP_INTERFACE_ID = 0x9ab669ef;
+      26                 :            : 
+      27                 :            :     /**
+      28                 :            :      * @notice ERC-165 interface detection
+      29                 :            :      * @param interfaceId The interface identifier to check
+      30                 :            :      * @return True if the interface is supported, false otherwise
+      31                 :            :      */
+      32                 :          3 :     function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165Upgradeable) returns (bool) {
+      33                 :          3 :         return interfaceId == IERC173_INTERFACE_ID || interfaceId == IOWNABLE2STEP_INTERFACE_ID
+      34                 :          1 :             || ERC165Upgradeable.supportsInterface(interfaceId);
       35                 :            :     }
       36                 :            : }
 
@@ -112,7 +112,7 @@
- +
Generated by: LCOV version 1.16

diff --git a/doc/test/coverage/src/modules/MetaTxModuleStandalone.sol.func-sort-c.html b/doc/coverage/src/modules/VersionModule.sol.func-sort-c.html similarity index 74% rename from doc/test/coverage/src/modules/MetaTxModuleStandalone.sol.func-sort-c.html rename to doc/coverage/src/modules/VersionModule.sol.func-sort-c.html index 8576efa..2fc86c7 100644 --- a/doc/test/coverage/src/modules/MetaTxModuleStandalone.sol.func-sort-c.html +++ b/doc/coverage/src/modules/VersionModule.sol.func-sort-c.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/modules/MetaTxModuleStandalone.sol - functions + LCOV - lcov.info - src/modules/VersionModule.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - + + - + - - + + @@ -65,16 +65,12 @@
Current view:top level - src/modules - MetaTxModuleStandalone.sol (source / functions)top level - src/modules - VersionModule.sol (source / functions) Hitlcov.info Lines:1 250.0 %2100.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions: 1250.0 %1100.0 %
- + - - - - - - + +

Function Name Sort by function nameFunction Name Sort by function name Hit count Sort by hit count
MetaTxModuleStandalone._msgData0
MetaTxModuleStandalone._msgSender92VersionModule.version7

diff --git a/doc/test/coverage/src/modules/MetaTxModuleStandalone.sol.func.html b/doc/coverage/src/modules/VersionModule.sol.func.html similarity index 74% rename from doc/test/coverage/src/modules/MetaTxModuleStandalone.sol.func.html rename to doc/coverage/src/modules/VersionModule.sol.func.html index d1a0432..7850e55 100644 --- a/doc/test/coverage/src/modules/MetaTxModuleStandalone.sol.func.html +++ b/doc/coverage/src/modules/VersionModule.sol.func.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/modules/MetaTxModuleStandalone.sol - functions + LCOV - lcov.info - src/modules/VersionModule.sol - functions @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - + + - + - - + + @@ -66,15 +66,11 @@ - + - - - - - - + +
Current view:top level - src/modules - MetaTxModuleStandalone.sol (source / functions)top level - src/modules - VersionModule.sol (source / functions) Hitlcov.info Lines:1 250.0 %2100.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions: 1250.0 %1100.0 %

Function Name Sort by function nameHit count Sort by hit countHit count Sort by hit count
MetaTxModuleStandalone._msgData0
MetaTxModuleStandalone._msgSender92VersionModule.version7

diff --git a/doc/test/coverage/src/modules/MetaTxModuleStandalone.sol.gcov.html b/doc/coverage/src/modules/VersionModule.sol.gcov.html similarity index 69% rename from doc/test/coverage/src/modules/MetaTxModuleStandalone.sol.gcov.html rename to doc/coverage/src/modules/VersionModule.sol.gcov.html index da4c72c..1a88637 100644 --- a/doc/test/coverage/src/modules/MetaTxModuleStandalone.sol.gcov.html +++ b/doc/coverage/src/modules/VersionModule.sol.gcov.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/modules/MetaTxModuleStandalone.sol + LCOV - lcov.info - src/modules/VersionModule.sol @@ -19,7 +19,7 @@ - + @@ -31,18 +31,18 @@ - - + + - + - - + + @@ -69,40 +69,39 @@ diff --git a/doc/coverage/src/modules/index-sort-b.html b/doc/coverage/src/modules/index-sort-b.html new file mode 100644 index 0000000..f7195a7 --- /dev/null +++ b/doc/coverage/src/modules/index-sort-b.html @@ -0,0 +1,203 @@ + + + + + + + LCOV - lcov.info - src/modules + + + + + +
Current view:top level - src/modules - MetaTxModuleStandalone.sol (source / functions)top level - src/modules - VersionModule.sol (source / functions) Hitlcov.info Lines:1 250.0 %2100.0 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions: 1250.0 %1100.0 %
           Branch data     Line data    Source code
-       1                 :            : //SPDX-License-Identifier: MPL-2.0
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
        2                 :            : 
-       3                 :            : pragma solidity ^0.8.20;
+       3                 :            : pragma solidity ^0.8.24;
        4                 :            : 
-       5                 :            : import "../../lib/openzeppelin-contracts/contracts/metatx/ERC2771Context.sol";
-       6                 :            : 
-       7                 :            : /**
-       8                 :            :  * @dev Meta transaction (gasless) module.
-       9                 :            :  */
-      10                 :            : abstract contract MetaTxModuleStandalone is ERC2771Context {
-      11                 :            :     constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {
-      12                 :            :         // Nothing to do
-      13                 :            :     }
-      14                 :            : 
-      15                 :            :     function _msgSender()
-      16                 :            :         internal
-      17                 :            :         view
-      18                 :            :         virtual
-      19                 :            :         override
-      20                 :            :         returns (address sender)
-      21                 :            :     {
-      22                 :         92 :         return ERC2771Context._msgSender();
-      23                 :            :     }
-      24                 :            : 
-      25                 :            :     function _msgData()
-      26                 :            :         internal
-      27                 :            :         view
-      28                 :            :         virtual
-      29                 :            :         override
-      30                 :            :         returns (bytes calldata)
-      31                 :            :     {
-      32                 :          0 :         return ERC2771Context._msgData();
-      33                 :            :     }
-      34                 :            : }
+       5                 :            : /* ==== CMTAT === */
+       6                 :            : import {IERC3643Version} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol";
+       7                 :            : 
+       8                 :            : /**
+       9                 :            :  * @title VersionModule
+      10                 :            :  * @notice Exposes the IncomeVault release version through the ERC-3643 version interface.
+      11                 :            :  * @dev
+      12                 :            :  * Same shape as the CMTAT, RuleEngine and SnapshotEngine version modules: a single compile-time
+      13                 :            :  * constant read through {IERC3643Version-version}. Bump `VERSION` together with the `CHANGELOG.md`
+      14                 :            :  * entry of the release.
+      15                 :            :  */
+      16                 :            : abstract contract VersionModule is IERC3643Version {
+      17                 :            :     /* ============ State Variables ============ */
+      18                 :            :     /**
+      19                 :            :      * @dev
+      20                 :            :      * Get the current version of the smart contract
+      21                 :            :      */
+      22                 :            :     string private constant VERSION = "2.0.0";
+      23                 :            : 
+      24                 :            :     /*//////////////////////////////////////////////////////////////
+      25                 :            :                             PUBLIC/EXTERNAL FUNCTIONS
+      26                 :            :     //////////////////////////////////////////////////////////////*/
+      27                 :            :     /**
+      28                 :            :      * @inheritdoc IERC3643Version
+      29                 :            :      */
+      30                 :          7 :     function version() public view virtual override(IERC3643Version) returns (string memory version_) {
+      31                 :          7 :         return VERSION;
+      32                 :            :     }
+      33                 :            : }
 
+ + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modulesHitTotalCoverage
Test:lcov.infoLines:20921696.8 %
Date:2026-08-31 13:21:44Functions:465288.5 %
Branches:424397.7 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
IncomeVaultInternal.sol +
98.9%98.9%
+
98.9 %90 / 91100.0 %19 / 1994.7 %18 / 19
IncomeVaultSnapshotCore.sol +
0.0%
+
0.0 %0 / 30.0 %0 / 3-0 / 0
Ownable2StepERC165Module.sol +
100.0%
+
100.0 %3 / 3100.0 %1 / 1-0 / 0
IncomeVaultValidationCore.sol +
0.0%
+
0.0 %0 / 10.0 %0 / 1-0 / 0
VersionModule.sol +
100.0%
+
100.0 %2 / 2100.0 %1 / 1-0 / 0
IncomeVaultOperatorModule.sol +
100.0%
+
100.0 %16 / 16100.0 %5 / 5100.0 %1 / 1
IncomeVaultSnapshotModule.sol +
96.3%96.3%
+
96.3 %26 / 2788.9 %8 / 9100.0 %3 / 3
ERC7741Module.sol +
100.0%
+
100.0 %24 / 24100.0 %5 / 5100.0 %4 / 4
IncomeVaultValidationModule.sol +
98.0%98.0%
+
98.0 %48 / 4987.5 %7 / 8100.0 %16 / 16
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/index-sort-f.html b/doc/coverage/src/modules/index-sort-f.html new file mode 100644 index 0000000..2cee4bc --- /dev/null +++ b/doc/coverage/src/modules/index-sort-f.html @@ -0,0 +1,203 @@ + + + + + + + LCOV - lcov.info - src/modules + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modulesHitTotalCoverage
Test:lcov.infoLines:20921696.8 %
Date:2026-08-31 13:21:44Functions:465288.5 %
Branches:424397.7 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
IncomeVaultValidationCore.sol +
0.0%
+
0.0 %0 / 10.0 %0 / 1-0 / 0
IncomeVaultSnapshotCore.sol +
0.0%
+
0.0 %0 / 30.0 %0 / 3-0 / 0
IncomeVaultValidationModule.sol +
98.0%98.0%
+
98.0 %48 / 4987.5 %7 / 8100.0 %16 / 16
IncomeVaultSnapshotModule.sol +
96.3%96.3%
+
96.3 %26 / 2788.9 %8 / 9100.0 %3 / 3
Ownable2StepERC165Module.sol +
100.0%
+
100.0 %3 / 3100.0 %1 / 1-0 / 0
VersionModule.sol +
100.0%
+
100.0 %2 / 2100.0 %1 / 1-0 / 0
IncomeVaultOperatorModule.sol +
100.0%
+
100.0 %16 / 16100.0 %5 / 5100.0 %1 / 1
ERC7741Module.sol +
100.0%
+
100.0 %24 / 24100.0 %5 / 5100.0 %4 / 4
IncomeVaultInternal.sol +
98.9%98.9%
+
98.9 %90 / 91100.0 %19 / 1994.7 %18 / 19
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/index-sort-l.html b/doc/coverage/src/modules/index-sort-l.html new file mode 100644 index 0000000..6de41a9 --- /dev/null +++ b/doc/coverage/src/modules/index-sort-l.html @@ -0,0 +1,203 @@ + + + + + + + LCOV - lcov.info - src/modules + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modulesHitTotalCoverage
Test:lcov.infoLines:20921696.8 %
Date:2026-08-31 13:21:44Functions:465288.5 %
Branches:424397.7 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
IncomeVaultValidationCore.sol +
0.0%
+
0.0 %0 / 10.0 %0 / 1-0 / 0
IncomeVaultSnapshotCore.sol +
0.0%
+
0.0 %0 / 30.0 %0 / 3-0 / 0
IncomeVaultSnapshotModule.sol +
96.3%96.3%
+
96.3 %26 / 2788.9 %8 / 9100.0 %3 / 3
IncomeVaultValidationModule.sol +
98.0%98.0%
+
98.0 %48 / 4987.5 %7 / 8100.0 %16 / 16
IncomeVaultInternal.sol +
98.9%98.9%
+
98.9 %90 / 91100.0 %19 / 1994.7 %18 / 19
VersionModule.sol +
100.0%
+
100.0 %2 / 2100.0 %1 / 1-0 / 0
Ownable2StepERC165Module.sol +
100.0%
+
100.0 %3 / 3100.0 %1 / 1-0 / 0
IncomeVaultOperatorModule.sol +
100.0%
+
100.0 %16 / 16100.0 %5 / 5100.0 %1 / 1
ERC7741Module.sol +
100.0%
+
100.0 %24 / 24100.0 %5 / 5100.0 %4 / 4
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/modules/index.html b/doc/coverage/src/modules/index.html new file mode 100644 index 0000000..c4bc65b --- /dev/null +++ b/doc/coverage/src/modules/index.html @@ -0,0 +1,203 @@ + + + + + + + LCOV - lcov.info - src/modules + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modulesHitTotalCoverage
Test:lcov.infoLines:20921696.8 %
Date:2026-08-31 13:21:44Functions:465288.5 %
Branches:424397.7 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
ERC7741Module.sol +
100.0%
+
100.0 %24 / 24100.0 %5 / 5100.0 %4 / 4
IncomeVaultInternal.sol +
98.9%98.9%
+
98.9 %90 / 91100.0 %19 / 1994.7 %18 / 19
IncomeVaultOperatorModule.sol +
100.0%
+
100.0 %16 / 16100.0 %5 / 5100.0 %1 / 1
IncomeVaultSnapshotCore.sol +
0.0%
+
0.0 %0 / 30.0 %0 / 3-0 / 0
IncomeVaultSnapshotModule.sol +
96.3%96.3%
+
96.3 %26 / 2788.9 %8 / 9100.0 %3 / 3
IncomeVaultValidationCore.sol +
0.0%
+
0.0 %0 / 10.0 %0 / 1-0 / 0
IncomeVaultValidationModule.sol +
98.0%98.0%
+
98.0 %48 / 4987.5 %7 / 8100.0 %16 / 16
Ownable2StepERC165Module.sol +
100.0%
+
100.0 %3 / 3100.0 %1 / 1-0 / 0
VersionModule.sol +
100.0%
+
100.0 %2 / 2100.0 %1 / 1-0 / 0
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/public/IncomeVaultOpen.sol.func-sort-c.html b/doc/coverage/src/public/IncomeVaultOpen.sol.func-sort-c.html new file mode 100644 index 0000000..473f17e --- /dev/null +++ b/doc/coverage/src/public/IncomeVaultOpen.sol.func-sort-c.html @@ -0,0 +1,117 @@ + + + + + + + LCOV - lcov.info - src/public/IncomeVaultOpen.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/public - IncomeVaultOpen.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:4444100.0 %
Date:2026-08-31 13:21:44Functions:99100.0 %
Branches:44100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultOpen.claimDividendBatchFor1
IncomeVaultOpen.validateTimeBatch4
IncomeVaultOpen.validateTime5
IncomeVaultOpen.validateTimeCode10
IncomeVaultOpen.claimDividendFor11
IncomeVaultOpen.claimDividendBatch2224
IncomeVaultOpen._claimDividendBatch2225
IncomeVaultOpen.claimDividend2378
IncomeVaultOpen._claimDividend2386
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/public/IncomeVaultOpen.sol.func.html b/doc/coverage/src/public/IncomeVaultOpen.sol.func.html new file mode 100644 index 0000000..333cdc9 --- /dev/null +++ b/doc/coverage/src/public/IncomeVaultOpen.sol.func.html @@ -0,0 +1,117 @@ + + + + + + + LCOV - lcov.info - src/public/IncomeVaultOpen.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/public - IncomeVaultOpen.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:4444100.0 %
Date:2026-08-31 13:21:44Functions:99100.0 %
Branches:44100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultOpen._claimDividend2386
IncomeVaultOpen._claimDividendBatch2225
IncomeVaultOpen.claimDividend2378
IncomeVaultOpen.claimDividendBatch2224
IncomeVaultOpen.claimDividendBatchFor1
IncomeVaultOpen.claimDividendFor11
IncomeVaultOpen.validateTime5
IncomeVaultOpen.validateTimeBatch4
IncomeVaultOpen.validateTimeCode10
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/public/IncomeVaultOpen.sol.gcov.html b/doc/coverage/src/public/IncomeVaultOpen.sol.gcov.html new file mode 100644 index 0000000..bd2de15 --- /dev/null +++ b/doc/coverage/src/public/IncomeVaultOpen.sol.gcov.html @@ -0,0 +1,242 @@ + + + + + + + LCOV - lcov.info - src/public/IncomeVaultOpen.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/public - IncomeVaultOpen.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:4444100.0 %
Date:2026-08-31 13:21:44Functions:99100.0 %
Branches:44100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
+       7                 :            : /* ==== IncomeVault === */
+       8                 :            : import {IncomeVaultValidationCore} from "../modules/IncomeVaultValidationCore.sol";
+       9                 :            : import {IncomeVaultSnapshotCore} from "../modules/IncomeVaultSnapshotCore.sol";
+      10                 :            : import {ERC7741Module} from "../modules/ERC7741Module.sol";
+      11                 :            : 
+      12                 :            : /**
+      13                 :            :  * @title Permissionless functions
+      14                 :            :  */
+      15                 :            : abstract contract IncomeVaultOpen is
+      16                 :            :     IncomeVaultValidationCore,
+      17                 :            :     IncomeVaultSnapshotCore,
+      18                 :            :     ERC7741Module,
+      19                 :            :     ReentrancyGuardTransient
+      20                 :            : {
+      21                 :            :     /*//////////////////////////////////////////////////////////////
+      22                 :            :                             PUBLIC/EXTERNAL FUNCTIONS
+      23                 :            :     //////////////////////////////////////////////////////////////*/
+      24                 :            :     /* ============ State functions ============ */
+      25                 :            :     /**
+      26                 :            :      * @notice claim your payment
+      27                 :            :      * @param time provide the date where you want to receive your payment
+      28                 :            :      */
+      29                 :       2378 :     function claimDividend(uint256 time) public virtual nonReentrant {
+      30                 :       2378 :         _claimDividend(_msgSender(), time);
+      31                 :            :     }
+      32                 :            : 
+      33                 :            :     /**
+      34                 :            :      * @notice Claim on behalf of a token holder
+      35                 :            :      * @dev
+      36                 :            :      * Callable by the holder, or by an address the holder authorised through {setOperator}. The
+      37                 :            :      * dividends always go to **the holder** — an operator pays the gas and chooses the moment, it can
+      38                 :            :      * never redirect the payment. Every other rule is unchanged: the claim window, the
+      39                 :            :      * already-claimed check and the transfer restrictions all apply exactly as for {claimDividend}.
+      40                 :            :      * @param holder the token holder to claim for
+      41                 :            :      * @param time provide the date of the payment
+      42                 :            :      */
+      43                 :         11 :     function claimDividendFor(address holder, uint256 time) public virtual nonReentrant {
+      44                 :         11 :         _requireHolderOrOperator(holder);
+      45                 :          8 :         _claimDividend(holder, time);
+      46                 :            :     }
+      47                 :            : 
+      48                 :            :     /**
+      49                 :            :      * @notice Batch version of {claimDividendFor}
+      50                 :            :      * @param holder the token holder to claim for
+      51                 :            :      * @param times provide the dates of the payments
+      52                 :            :      */
+      53                 :          1 :     function claimDividendBatchFor(address holder, uint256[] calldata times) public virtual nonReentrant {
+      54                 :          1 :         _requireHolderOrOperator(holder);
+      55                 :          1 :         _claimDividendBatch(holder, times);
+      56                 :            :     }
+      57                 :            : 
+      58                 :            :     /**
+      59                 :            :      * @notice batch version of {claimDividend}
+      60                 :            :      * @param times provide the dates where you want to receive your payment
+      61                 :            :      * @dev Don't check if the dividends have been already claimed before external call to the snapshot source.
+      62                 :            :      */
+      63                 :       2224 :     function claimDividendBatch(uint256[] calldata times) public virtual nonReentrant {
+      64                 :       2224 :         _claimDividendBatch(_msgSender(), times);
+      65                 :            :     }
+      66                 :            : 
+      67                 :            :     /* ============ View functions ============ */
+      68                 :            :     /**
+      69                 :            :      * @notice validate if a time is valid, return 0 if valid
+      70                 :            :      * @param time the dividend time to check
+      71                 :            :      * @return code the reason the time is invalid, or `TIME_ERROR_CODE.OK`
+      72                 :            :      */
+      73                 :         10 :     function validateTimeCode(uint256 time) public view virtual returns (TIME_ERROR_CODE code) {
+      74                 :       2399 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+      75                 :       2399 :         return _timeCode($, time, $._timeLimitToWithdraw);
+      76                 :            :     }
+      77                 :            : 
+      78                 :            :     /**
+      79                 :            :      * @notice validate if a time is valid, revert if invalid
+      80                 :            :      * @param time the dividend time to check
+      81                 :            :      */
+      82                 :          5 :     function validateTime(uint256 time) public view virtual {
+      83                 :       2391 :         _revertOnInvalidTime(validateTimeCode(time));
+      84                 :            :     }
+      85                 :            : 
+      86                 :            :     /**
+      87                 :            :      * @notice batch version of {validateTime}
+      88                 :            :      * @param times the dividend times to check
+      89                 :            :      */
+      90                 :          4 :     function validateTimeBatch(uint256[] calldata times) public view virtual {
+      91                 :       2229 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+      92                 :            :         // `_timeLimitToWithdraw` is the same slot for every element: read it once
+      93                 :       2229 :         uint256 timeLimit = $._timeLimitToWithdraw;
+      94                 :       2229 :         for (uint256 i = 0; i < times.length; ++i) {
+      95                 :       2894 :             _revertOnInvalidTime(_timeCode($, times[i], timeLimit));
+      96                 :            :         }
+      97                 :            :     }
+      98                 :            : 
+      99                 :            :     /*//////////////////////////////////////////////////////////////
+     100                 :            :                             INTERNAL/PRIVATE FUNCTIONS
+     101                 :            :     //////////////////////////////////////////////////////////////*/
+     102                 :            :     /* ============ State functions ============ */
+     103                 :            :     /**
+     104                 :            :      * @dev {claimDividend} for an explicit holder
+     105                 :            :      * @param sender the token holder being paid
+     106                 :            :      * @param time the dividend time
+     107                 :            :      */
+     108                 :       2386 :     function _claimDividend(address sender, uint256 time) internal virtual {
+     109                 :       2386 :         validateTime(time);
+     110                 :        655 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     111                 :            :         // At the beginning since no external call to do
+     112            [ + ]:         69 :         if ($._claimedDividend[sender][time]) {
+     113                 :         69 :             revert IncomeVault_DividendAlreadyClaimed();
+     114                 :            :         }
+     115                 :            : 
+     116                 :            :         // External call to the snapshot source to retrieve the total supply and the sender balance
+     117                 :        586 :         (uint256 senderBalance, uint256 TokenTotalSupply) = _snapshotInfo(time, sender);
+     118            [ + ]:        586 :         if (senderBalance == 0) {
+     119                 :          1 :             revert IncomeVault_TokenBalanceIsZero();
+     120                 :            :         }
+     121                 :            : 
+     122                 :        585 :         uint256 senderDividend = _computeDividend(time, senderBalance, TokenTotalSupply);
+     123            [ + ]:        585 :         if (senderDividend == 0) {
+     124                 :        197 :             revert IncomeVault_NoDividendToClaim();
+     125                 :            :         }
+     126                 :            : 
+     127                 :            :         // Transfer restriction
+     128                 :        388 :         _validateTransfer(address(this), sender, senderDividend);
+     129                 :        129 :         _transferDividend(time, sender, senderDividend);
+     130                 :            :     }
+     131                 :            : 
+     132                 :            :     /**
+     133                 :            :      * @dev {claimDividendBatch} for an explicit holder
+     134                 :            :      * @param sender the token holder being paid
+     135                 :            :      * @param times the dividend times
+     136                 :            :      */
+     137                 :       2225 :     function _claimDividendBatch(address sender, uint256[] calldata times) internal virtual {
+     138                 :            :         // Check if the claim is activated for each times
+     139                 :       2225 :         validateTimeBatch(times);
+     140                 :         77 :         address[] memory senders = new address[](1);
+     141                 :         77 :         senders[0] = sender;
+     142                 :         77 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     143                 :            :         // External call to the snapshot source to retrieve the total supply and the sender balance
+     144                 :         77 :         (uint256[][] memory senderBalances, uint256[] memory TokenTotalSupplys) = _snapshotInfoBatch(times, senders);
+     145                 :         77 :         for (uint256 i = 0; i < times.length; ++i) {
+     146            [ + ]:        115 :             if (!$._claimedDividend[sender][times[i]] && (senderBalances[i][0] > 0)) {
+     147                 :         88 :                 uint256 senderDividend = _computeDividend(times[i], senderBalances[i][0], TokenTotalSupplys[i]);
+     148                 :            :                 // Transfer restriction
+     149                 :         88 :                 _validateTransfer(address(this), sender, senderDividend);
+     150                 :            :                 // internal call performing an ERC-20 external call
+     151                 :         30 :                 _transferDividend(times[i], sender, senderDividend);
+     152                 :            :             }
+     153                 :            :         }
+     154                 :            :     }
+     155                 :            : 
+     156                 :            :     /* ============ View functions ============ */
+     157                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/public/IncomeVaultRestricted.sol.func-sort-c.html b/doc/coverage/src/public/IncomeVaultRestricted.sol.func-sort-c.html new file mode 100644 index 0000000..f048584 --- /dev/null +++ b/doc/coverage/src/public/IncomeVaultRestricted.sol.func-sort-c.html @@ -0,0 +1,153 @@ + + + + + + + LCOV - lcov.info - src/public/IncomeVaultRestricted.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/public - IncomeVaultRestricted.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:768095.0 %
Date:2026-08-31 13:21:44Functions:141877.8 %
Branches:99100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultRestricted._authorizeDeposit0
IncomeVaultRestricted._authorizeDistribute0
IncomeVaultRestricted._authorizeOperator0
IncomeVaultRestricted._authorizeWithdraw0
IncomeVaultRestricted.withdrawAll5
IncomeVaultRestricted.onlyVaultOperator8
IncomeVaultRestricted.setTimeLimitToWithdraw8
IncomeVaultRestricted.depositBatch10
IncomeVaultRestricted.onlyDepositManager10
IncomeVaultRestricted.__IncomeVaultRestricted_init_unchained286
IncomeVaultRestricted.transferDividendSelf820
IncomeVaultRestricted.onlyWithdrawManager1223
IncomeVaultRestricted.withdraw1223
IncomeVaultRestricted.setStatusClaim2097
IncomeVaultRestricted.distributeDividendBestEffort2106
IncomeVaultRestricted.onlyDistributeManager2106
IncomeVaultRestricted.deposit2251
IncomeVaultRestricted.distributeDividend2349
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/public/IncomeVaultRestricted.sol.func.html b/doc/coverage/src/public/IncomeVaultRestricted.sol.func.html new file mode 100644 index 0000000..f5ea4fd --- /dev/null +++ b/doc/coverage/src/public/IncomeVaultRestricted.sol.func.html @@ -0,0 +1,153 @@ + + + + + + + LCOV - lcov.info - src/public/IncomeVaultRestricted.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/public - IncomeVaultRestricted.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:768095.0 %
Date:2026-08-31 13:21:44Functions:141877.8 %
Branches:99100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
IncomeVaultRestricted.__IncomeVaultRestricted_init_unchained286
IncomeVaultRestricted._authorizeDeposit0
IncomeVaultRestricted._authorizeDistribute0
IncomeVaultRestricted._authorizeOperator0
IncomeVaultRestricted._authorizeWithdraw0
IncomeVaultRestricted.deposit2251
IncomeVaultRestricted.depositBatch10
IncomeVaultRestricted.distributeDividend2349
IncomeVaultRestricted.distributeDividendBestEffort2106
IncomeVaultRestricted.onlyDepositManager10
IncomeVaultRestricted.onlyDistributeManager2106
IncomeVaultRestricted.onlyVaultOperator8
IncomeVaultRestricted.onlyWithdrawManager1223
IncomeVaultRestricted.setStatusClaim2097
IncomeVaultRestricted.setTimeLimitToWithdraw8
IncomeVaultRestricted.transferDividendSelf820
IncomeVaultRestricted.withdraw1223
IncomeVaultRestricted.withdrawAll5
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/src/public/IncomeVaultRestricted.sol.gcov.html b/doc/coverage/src/public/IncomeVaultRestricted.sol.gcov.html new file mode 100644 index 0000000..b585c9e --- /dev/null +++ b/doc/coverage/src/public/IncomeVaultRestricted.sol.gcov.html @@ -0,0 +1,403 @@ + + + + + + + LCOV - lcov.info - src/public/IncomeVaultRestricted.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/public - IncomeVaultRestricted.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:768095.0 %
Date:2026-08-31 13:21:44Functions:141877.8 %
Branches:99100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : 
+       3                 :            : pragma solidity ^0.8.24;
+       4                 :            : 
+       5                 :            : /* ==== OpenZeppelin === */
+       6                 :            : import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+       7                 :            : import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+       8                 :            : import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
+       9                 :            : import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
+      10                 :            : /* ==== IncomeVault === */
+      11                 :            : import {IncomeVaultValidationCore} from "../modules/IncomeVaultValidationCore.sol";
+      12                 :            : import {IncomeVaultSnapshotCore} from "../modules/IncomeVaultSnapshotCore.sol";
+      13                 :            : import {IncomeVaultInternal} from "../modules/IncomeVaultInternal.sol";
+      14                 :            : 
+      15                 :            : /**
+      16                 :            :  * @title Restricted functions
+      17                 :            :  */
+      18                 :            : abstract contract IncomeVaultRestricted is
+      19                 :            :     IncomeVaultValidationCore,
+      20                 :            :     IncomeVaultSnapshotCore,
+      21                 :            :     ContextUpgradeable,
+      22                 :            :     IncomeVaultInternal,
+      23                 :            :     ReentrancyGuardTransient
+      24                 :            : {
+      25                 :            :     // Security
+      26                 :            :     using SafeERC20 for IERC20;
+      27                 :            : 
+      28                 :            :     /* ============ Modifier ============ */
+      29                 :            :     /// @dev Restricts the deposit of dividends
+      30                 :         10 :     modifier onlyDepositManager() {
+      31                 :         10 :         _authorizeDeposit();
+      32                 :            :         _;
+      33                 :            :     }
+      34                 :            : 
+      35                 :            :     /// @dev Restricts the withdrawal of the deposited funds
+      36                 :       1223 :     modifier onlyWithdrawManager() {
+      37                 :       1223 :         _authorizeWithdraw();
+      38                 :            :         _;
+      39                 :            :     }
+      40                 :            : 
+      41                 :            :     /// @dev Restricts the issuer-driven distribution of the dividends
+      42                 :       2106 :     modifier onlyDistributeManager() {
+      43                 :       2106 :         _authorizeDistribute();
+      44                 :            :         _;
+      45                 :            :     }
+      46                 :            : 
+      47                 :            :     /// @dev Restricts the configuration of the claim window
+      48                 :          8 :     modifier onlyVaultOperator() {
+      49                 :          8 :         _authorizeOperator();
+      50                 :            :         _;
+      51                 :            :     }
+      52                 :            : 
+      53                 :            :     /* ============  Initializer Function ============ */
+      54                 :            :     /**
+      55                 :            :      * @dev calls the different initialize functions from the different modules
+      56                 :            :      * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted
+      57                 :            :      */
+      58                 :        286 :     function __IncomeVaultRestricted_init_unchained(uint256 timeLimitToWithdraw_) internal onlyInitializing {
+      59                 :        286 :         _setTimeLimitToWithdraw(timeLimitToWithdraw_);
+      60                 :            :     }
+      61                 :            : 
+      62                 :            :     /*//////////////////////////////////////////////////////////////
+      63                 :            :                             PUBLIC/EXTERNAL FUNCTIONS
+      64                 :            :     //////////////////////////////////////////////////////////////*/
+      65                 :            :     /* ============ State restricted functions ============ */
+      66                 :            :     /**
+      67                 :            :      * @notice deposit an amount to pay the dividends.
+      68                 :            :      * @param time provide the date where you want to perform a deposit
+      69                 :            :      * @param amount the amount to deposit
+      70                 :            :      */
+      71                 :       2251 :     function deposit(uint256 time, uint256 amount) public virtual onlyDepositManager {
+      72                 :       2248 :         address sender = _msgSender();
+      73                 :       2248 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+      74                 :       2248 :         _deposit($, sender, time, amount);
+      75                 :            :         // Will revert in case of failure
+      76                 :       2247 :         $._ERC20TokenPayment.safeTransferFrom(sender, address(this), amount);
+      77                 :            :     }
+      78                 :            : 
+      79                 :            :     /**
+      80                 :            :      * @notice Deposit for several dividend times in one transaction
+      81                 :            :      * @dev
+      82                 :            :      * Equivalent to calling {deposit} once per entry — same accounting, same `newDeposit` event per
+      83                 :            :      * entry — but the payment token is pulled **once** for the total instead of once per time. That is
+      84                 :            :      * the reason the function exists; the common case is an issuer opening a year of coupon periods.
+      85                 :            :      *
+      86                 :            :      * Repeating a `time` is allowed and accumulates, exactly as separate calls would.
+      87                 :            :      *
+      88                 :            :      * @param times the dividend times to deposit for
+      89                 :            :      * @param amounts the amount to deposit for each time, must be the same length and each non-zero
+      90                 :            :      */
+      91                 :         10 :     function depositBatch(uint256[] calldata times, uint256[] calldata amounts) public virtual onlyDepositManager {
+      92            [ + ]:          9 :         if (times.length != amounts.length) {
+      93                 :          1 :             revert IncomeVault_InvalidLengths(times.length, amounts.length);
+      94                 :            :         }
+      95            [ + ]:          8 :         if (times.length == 0) {
+      96                 :          1 :             revert IncomeVault_NoAmountSend();
+      97                 :            :         }
+      98                 :          7 :         address sender = _msgSender();
+      99                 :          7 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     100                 :          7 :         uint256 total;
+     101                 :          7 :         for (uint256 i = 0; i < times.length; ++i) {
+     102                 :         19 :             _deposit($, sender, times[i], amounts[i]);
+     103                 :         18 :             total += amounts[i];
+     104                 :            :         }
+     105                 :            :         // One transfer for the whole batch. Will revert in case of failure.
+     106                 :          6 :         $._ERC20TokenPayment.safeTransferFrom(sender, address(this), total);
+     107                 :            :     }
+     108                 :            : 
+     109                 :            :     /**
+     110                 :            :      * @notice withdraw a certain amount at a specified time.
+     111                 :            :      * @dev
+     112                 :            :      * Bounded by {unclaimedDividend}, so a sweep can never reach funds deposited for another dividend
+     113                 :            :      * time. Intended for after the claim window closes, when what remains is rounding dust and
+     114                 :            :      * unclaimed shares.
+     115                 :            :      *
+     116                 :            :      * @custom:security Withdrawing **before** the window closes is still destructive to this period:
+     117                 :            :      * the amount taken is money the remaining holders are entitled to, and it also lowers
+     118                 :            :      * `segregatedDividend`, which re-prices every claim that has not happened yet. The bound stops the
+     119                 :            :      * damage spreading to other periods; it does not make an early sweep safe.
+     120                 :            :      *
+     121                 :            :      * @param time provide the date where you want to perform a deposit
+     122                 :            :      * @param amount the amount to withdraw
+     123                 :            :      * @param withdrawAddress address to receive `amount`of tokens
+     124                 :            :      */
+     125                 :       1223 :     function withdraw(uint256 time, uint256 amount, address withdrawAddress) public virtual onlyWithdrawManager {
+     126                 :       1220 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     127                 :            :         // Bound by what this period STILL holds, not by what was deposited into it. `_segregatedDividend`
+     128                 :            :         // is the pro-rata denominator and is never reduced by a payout, so checking against it alone
+     129                 :            :         // would let a fully-claimed period be swept again — taking another period's money.
+     130                 :            :         // {unclaimedDividend} saturates at zero, so an over-drawn period simply allows nothing.
+     131            [ + ]:       1220 :         if (unclaimedDividend(time) < amount) {
+     132                 :         88 :             revert IncomeVault_NotEnoughAmount();
+     133                 :            :         }
+     134                 :       1132 :         $._segregatedDividend[time] -= amount;
+     135                 :       1132 :         emit Withdraw(time, withdrawAddress, amount);
+     136                 :            :         // Will revert in case of failure
+     137                 :       1132 :         $._ERC20TokenPayment.safeTransfer(withdrawAddress, amount);
+     138                 :            :     }
+     139                 :            : 
+     140                 :            :     /**
+     141                 :            :      * @notice withdraw all tokens from ERC20TokenPayment contracts deposited
+     142                 :            :      * @param amount the amount to withdraw
+     143                 :            :      * @param withdrawAddress address to receive `amount`of tokens
+     144                 :            :      */
+     145                 :          5 :     function withdrawAll(uint256 amount, address withdrawAddress) public virtual onlyWithdrawManager {
+     146                 :          3 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     147                 :          3 :         emit WithdrawAll(withdrawAddress, amount);
+     148                 :            :         // Will revert in case of failure
+     149                 :          3 :         $._ERC20TokenPayment.safeTransfer(withdrawAddress, amount);
+     150                 :            :     }
+     151                 :            : 
+     152                 :            :     /**
+     153                 :            :      * @notice distribute the dividends
+     154                 :            :      * @param addresses compute and transfer dividend for these holders
+     155                 :            :      * @param time dividend time
+     156                 :            :      * @dev The dividends are distributed only if they have not yet been claimed by the token holder.
+     157                 :            :      * Subject to the same claim window **and** the same transfer restrictions as
+     158                 :            :      * {IncomeVaultOpen-claimDividend}: a holder the pause, freeze or RuleEngine refuses cannot be paid
+     159                 :            :      * by the issuer either, and one blocked holder reverts the whole distribution.
+     160                 :            :      */
+     161                 :       2349 :     function distributeDividend(address[] calldata addresses, uint256 time) public virtual onlyDistributeManager {
+     162                 :       2347 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     163                 :            :         // Same window as a holder-driven claim: the claims must be open, `time` must have passed so the
+     164                 :            :         // snapshot is recorded, and the withdraw limit must not have expired. Distributing before `time`
+     165                 :            :         // would read the *live* balances, because {ISnapshotSource} falls back to them when no snapshot
+     166                 :            :         // exists yet, and would consume the holder's claim for that period at the wrong amount.
+     167                 :       2347 :         _revertOnInvalidTime(_timeCode($, time, $._timeLimitToWithdraw));
+     168                 :            :         // Get info from the snapshot source
+     169                 :        602 :         (uint256[] memory tokenHolderBalance, uint256 totalSupply) = _snapshotInfoBatch(time, addresses);
+     170                 :            :         // Compute dividend for all token holders
+     171                 :        602 :         uint256[] memory tokenHolderDividend = _computeDividendBatch(time, addresses, tokenHolderBalance, totalSupply);
+     172                 :            :         // transfer the dividends for all token holders
+     173                 :        602 :         for (uint256 i = 0; i < addresses.length; ++i) {
+     174                 :            :             // The dividends are distributed only if they have not yet been claimed by the token holder
+     175            [ + ]:        604 :             if (!$._claimedDividend[addresses[i]][time]) {
+     176                 :            :                 // transfer dividends
+     177            [ + ]:        510 :                 if (tokenHolderDividend[i] > 0) {
+     178                 :            :                     // Same transfer restriction as a holder-driven claim: pause, freeze and RuleEngine.
+     179                 :            :                     // Reverts the whole distribution rather than skipping the holder, so a blocked
+     180                 :            :                     // address cannot be silently dropped from a payout the operator believes succeeded.
+     181                 :            :                     // The error carries the address, so it can be removed from the list and retried.
+     182                 :        294 :                     _validateTransfer(address(this), addresses[i], tokenHolderDividend[i]);
+     183                 :         90 :                     _transferDividend(time, addresses[i], tokenHolderDividend[i]);
+     184                 :            :                 }
+     185                 :            :             }
+     186                 :            :         }
+     187                 :            :     }
+     188                 :            : 
+     189                 :            :     /**
+     190                 :            :      * @notice Distribute the dividends, skipping any holder whose payout is refused
+     191                 :            :      * @dev
+     192                 :            :      * Same computation as {distributeDividend}, but a holder the ValidationModule or the payment token
+     193                 :            :      * refuses is **skipped** instead of reverting the whole call. Use it when one non-compliant address
+     194                 :            :      * must not block a large payout run; use {distributeDividend} when the distribution should be
+     195                 :            :      * all-or-nothing.
+     196                 :            :      *
+     197                 :            :      * Each payout is attempted through an external self-call so it can be wrapped in `try`/`catch`,
+     198                 :            :      * which gives **per-holder atomicity**: a holder is either fully paid — marked claimed *and*
+     199                 :            :      * transferred — or left completely untouched and still able to claim later. A partial state where
+     200                 :            :      * a holder is marked as claimed without receiving the tokens is not reachable.
+     201                 :            :      *
+     202                 :            :      * Every skip emits {DividendDistributionSkipped} carrying the raw revert data, so the cause can be
+     203                 :            :      * decoded off-chain, and the skipped holders are returned for the caller to act on directly.
+     204                 :            :      *
+     205                 :            :      * @custom:security `catch` cannot distinguish a refused payout from an out-of-gas failure. The two
+     206                 :            :      * contracts that can consume gas here — the payment token and the RuleEngine — are both set by the
+     207                 :            :      * admin and trusted; a malicious RuleEngine could nonetheless make holders appear skipped. That is
+     208                 :            :      * within the existing trust assumption for the RuleEngine, not a new one.
+     209                 :            :      *
+     210                 :            :      * @param addresses compute and transfer dividend for these holders
+     211                 :            :      * @param time dividend time
+     212                 :            :      * @return paidCount how many holders were paid
+     213                 :            :      * @return skipped the holders that were not paid, trimmed to `paidCount` subtracted from the input
+     214                 :            :      */
+     215                 :       2106 :     function distributeDividendBestEffort(address[] calldata addresses, uint256 time)
+     216                 :            :         public
+     217                 :            :         virtual
+     218                 :            :         nonReentrant
+     219                 :            :         onlyDistributeManager
+     220                 :            :         returns (uint256 paidCount, address[] memory skipped)
+     221                 :            :     {
+     222                 :       2105 :         IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage();
+     223                 :       2105 :         _revertOnInvalidTime(_timeCode($, time, $._timeLimitToWithdraw));
+     224                 :            : 
+     225                 :        545 :         (uint256[] memory tokenHolderBalance, uint256 totalSupply) = _snapshotInfoBatch(time, addresses);
+     226                 :        545 :         uint256[] memory tokenHolderDividend = _computeDividendBatch(time, addresses, tokenHolderBalance, totalSupply);
+     227                 :            : 
+     228                 :        545 :         address[] memory skippedBuffer = new address[](addresses.length);
+     229                 :        545 :         uint256 skippedCount;
+     230                 :            : 
+     231                 :        545 :         for (uint256 i = 0; i < addresses.length; ++i) {
+     232            [ + ]:       1631 :             if ($._claimedDividend[addresses[i]][time] || tokenHolderDividend[i] == 0) {
+     233                 :       1631 :                 continue;
+     234                 :            :             }
+     235                 :            :             // External self-call: `try` needs one, and it is what bounds the revert to this holder.
+     236            [ + ]:        818 :             try this.transferDividendSelf(time, addresses[i], tokenHolderDividend[i]) {
+     237                 :        235 :                 ++paidCount;
+     238            [ + ]:        583 :             } catch (bytes memory reason) {
+     239                 :        583 :                 skippedBuffer[skippedCount] = addresses[i];
+     240                 :        583 :                 ++skippedCount;
+     241                 :        583 :                 emit DividendDistributionSkipped(time, addresses[i], reason);
+     242                 :            :             }
+     243                 :            :         }
+     244                 :            : 
+     245                 :        545 :         skipped = new address[](skippedCount);
+     246                 :        545 :         for (uint256 i = 0; i < skippedCount; ++i) {
+     247                 :        583 :             skipped[i] = skippedBuffer[i];
+     248                 :            :         }
+     249                 :            :     }
+     250                 :            : 
+     251                 :            :     /**
+     252                 :            :      * @notice Validate and pay one dividend — callable **only by the vault itself**
+     253                 :            :      * @dev
+     254                 :            :      * This exists solely so {distributeDividendBestEffort} can wrap a payout in `try`/`catch`, which
+     255                 :            :      * requires an external call. It carries no access control of its own beyond the self-call check,
+     256                 :            :      * so that check is what stands between it and an unauthorized payout: reverts
+     257                 :            :      * {IncomeVault_OnlySelfCall} for every caller other than `address(this)`.
+     258                 :            :      *
+     259                 :            :      * `msg.sender` is used deliberately rather than `_msgSender()`. The check must identify the real
+     260                 :            :      * caller; an ERC-2771 forwarder must never be able to present itself as the vault.
+     261                 :            :      *
+     262                 :            :      * @param time dividend time
+     263                 :            :      * @param tokenHolder the holder to pay
+     264                 :            :      * @param tokenHolderDividend the amount to pay
+     265                 :            :      */
+     266                 :        820 :     function transferDividendSelf(uint256 time, address tokenHolder, uint256 tokenHolderDividend) public virtual {
+     267            [ + ]:        820 :         if (msg.sender != address(this)) {
+     268                 :          2 :             revert IncomeVault_OnlySelfCall();
+     269                 :            :         }
+     270                 :        818 :         _validateTransfer(address(this), tokenHolder, tokenHolderDividend);
+     271                 :        236 :         _transferDividend(time, tokenHolder, tokenHolderDividend);
+     272                 :            :     }
+     273                 :            : 
+     274                 :            :     /**
+     275                 :            :      * @notice set the status to open or close the claims for a given time
+     276                 :            :      * @param time target time
+     277                 :            :      * @param status boolean (true or false)
+     278                 :            :      *
+     279                 :            :      */
+     280                 :       2097 :     function setStatusClaim(uint256 time, bool status) public virtual onlyVaultOperator {
+     281                 :       2092 :         _setStatusClaim(time, status);
+     282                 :            :     }
+     283                 :            : 
+     284                 :            :     /**
+     285                 :            :      * @notice configure the time limit to withdraw
+     286                 :            :      * @dev reverts if `timeLimitToWithdraw_` is zero: that would leave a one-second claim window
+     287                 :            :      * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted,
+     288                 :            :      * must be greater than zero
+     289                 :            :      */
+     290                 :          8 :     function setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) public virtual onlyVaultOperator {
+     291                 :          6 :         _setTimeLimitToWithdraw(timeLimitToWithdraw_);
+     292                 :            :     }
+     293                 :            : 
+     294                 :            :     /* ============ Access Control ============ */
+     295                 :            :     /**
+     296                 :            :      * @dev Authorization hook invoked before a deposit.
+     297                 :            :      * Implemented by the deployment contract with the desired access-control policy.
+     298                 :            :      */
+     299                 :          0 :     function _authorizeDeposit() internal view virtual;
+     300                 :            : 
+     301                 :            :     /**
+     302                 :            :      * @dev Authorization hook invoked before {withdraw} and {withdrawAll}.
+     303                 :            :      * Implemented by the deployment contract with the desired access-control policy.
+     304                 :            :      */
+     305                 :          0 :     function _authorizeWithdraw() internal view virtual;
+     306                 :            : 
+     307                 :            :     /**
+     308                 :            :      * @dev Authorization hook invoked before {distributeDividend}.
+     309                 :            :      * Implemented by the deployment contract with the desired access-control policy.
+     310                 :            :      */
+     311                 :          0 :     function _authorizeDistribute() internal view virtual;
+     312                 :            : 
+     313                 :            :     /**
+     314                 :            :      * @dev Authorization hook invoked before {setStatusClaim} and {setTimeLimitToWithdraw}.
+     315                 :            :      * Implemented by the deployment contract with the desired access-control policy.
+     316                 :            :      */
+     317                 :          0 :     function _authorizeOperator() internal view virtual;
+     318                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/test/coverage/src/rules/index-sort-b.html b/doc/coverage/src/public/index-sort-b.html similarity index 68% rename from doc/test/coverage/src/rules/index-sort-b.html rename to doc/coverage/src/public/index-sort-b.html index cb998f5..1cf3722 100644 --- a/doc/test/coverage/src/rules/index-sort-b.html +++ b/doc/coverage/src/public/index-sort-b.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/rules + LCOV - lcov.info - src/public @@ -19,7 +19,7 @@ - + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - - + + +
Current view:top level - src/rulestop level - src/public Hitlcov.info Lines:525496.3 %12012496.8 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:171989.5 %232785.2 %
Branches:242692.3 %1313100.0 %
@@ -82,28 +82,28 @@
Branches Sort by branch coverage
RuleWhitelist.solIncomeVaultOpen.sol -
97.4%97.4%
+
100.0%
97.4 %37 / 3891.7 %11 / 1287.5 %14 / 16100.0 %44 / 44100.0 %9 / 9100.0 %4 / 4
RuleSanctionList.solIncomeVaultRestricted.sol -
93.8%93.8%
+
95.0%95.0%
93.8 %15 / 1685.7 %6 / 795.0 %76 / 8077.8 %14 / 18 100.0 %10 / 109 / 9
diff --git a/doc/test/coverage/src/rules/index-sort-f.html b/doc/coverage/src/public/index-sort-f.html similarity index 68% rename from doc/test/coverage/src/rules/index-sort-f.html rename to doc/coverage/src/public/index-sort-f.html index 700086c..4740087 100644 --- a/doc/test/coverage/src/rules/index-sort-f.html +++ b/doc/coverage/src/public/index-sort-f.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/rules + LCOV - lcov.info - src/public @@ -19,7 +19,7 @@ - + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - - + + +
Current view:top level - src/rulestop level - src/public Hitlcov.info Lines:525496.3 %12012496.8 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:171989.5 %232785.2 %
Branches:242692.3 %1313100.0 %
@@ -82,28 +82,28 @@ Branches Sort by branch coverage - RuleSanctionList.sol + IncomeVaultRestricted.sol -
93.8%93.8%
+
95.0%95.0%
- 93.8 % - 15 / 16 - 85.7 % - 6 / 7 + 95.0 % + 76 / 80 + 77.8 % + 14 / 18 100.0 % - 10 / 10 + 9 / 9 - RuleWhitelist.sol + IncomeVaultOpen.sol -
97.4%97.4%
+
100.0%
- 97.4 % - 37 / 38 - 91.7 % - 11 / 12 - 87.5 % - 14 / 16 + 100.0 % + 44 / 44 + 100.0 % + 9 / 9 + 100.0 % + 4 / 4 diff --git a/doc/test/coverage/src/rules/index-sort-l.html b/doc/coverage/src/public/index-sort-l.html similarity index 68% rename from doc/test/coverage/src/rules/index-sort-l.html rename to doc/coverage/src/public/index-sort-l.html index bab51ca..4462866 100644 --- a/doc/test/coverage/src/rules/index-sort-l.html +++ b/doc/coverage/src/public/index-sort-l.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/rules + LCOV - lcov.info - src/public @@ -19,7 +19,7 @@ - + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - - + + +
Current view:top level - src/rulestop level - src/public Hitlcov.info Lines:525496.3 %12012496.8 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:171989.5 %232785.2 %
Branches:242692.3 %1313100.0 %
@@ -82,28 +82,28 @@ Branches Sort by branch coverage - RuleSanctionList.sol + IncomeVaultRestricted.sol -
93.8%93.8%
+
95.0%95.0%
- 93.8 % - 15 / 16 - 85.7 % - 6 / 7 + 95.0 % + 76 / 80 + 77.8 % + 14 / 18 100.0 % - 10 / 10 + 9 / 9 - RuleWhitelist.sol + IncomeVaultOpen.sol -
97.4%97.4%
+
100.0%
- 97.4 % - 37 / 38 - 91.7 % - 11 / 12 - 87.5 % - 14 / 16 + 100.0 % + 44 / 44 + 100.0 % + 9 / 9 + 100.0 % + 4 / 4 diff --git a/doc/test/coverage/src/rules/index.html b/doc/coverage/src/public/index.html similarity index 68% rename from doc/test/coverage/src/rules/index.html rename to doc/coverage/src/public/index.html index 060773a..3a8c370 100644 --- a/doc/test/coverage/src/rules/index.html +++ b/doc/coverage/src/public/index.html @@ -4,7 +4,7 @@ - LCOV - lcov.info - src/rules + LCOV - lcov.info - src/public @@ -19,7 +19,7 @@ - + @@ -31,27 +31,27 @@ - - - + + + - + - - - + + + - - - + + +
Current view:top level - src/rulestop level - src/public Hitlcov.info Lines:525496.3 %12012496.8 %
Date:2023-11-21 13:10:432026-08-31 13:21:44 Functions:171989.5 %232785.2 %
Branches:242692.3 %1313100.0 %
@@ -82,28 +82,28 @@ Branches Sort by branch coverage - RuleSanctionList.sol + IncomeVaultOpen.sol -
93.8%93.8%
+
100.0%
- 93.8 % - 15 / 16 - 85.7 % - 6 / 7 100.0 % - 10 / 10 + 44 / 44 + 100.0 % + 9 / 9 + 100.0 % + 4 / 4 - RuleWhitelist.sol + IncomeVaultRestricted.sol -
97.4%97.4%
+
95.0%95.0%
- 97.4 % - 37 / 38 - 91.7 % - 11 / 12 - 87.5 % - 14 / 16 + 95.0 % + 76 / 80 + 77.8 % + 14 / 18 + 100.0 % + 9 / 9 diff --git a/doc/test/coverage/updown.png b/doc/coverage/updown.png similarity index 100% rename from doc/test/coverage/updown.png rename to doc/coverage/updown.png diff --git a/doc/schema/classDiagram.png b/doc/schema/classDiagram.png index dbfd6d4..e490ca8 100644 Binary files a/doc/schema/classDiagram.png and b/doc/schema/classDiagram.png differ diff --git a/doc/schema/classDiagram.svg b/doc/schema/classDiagram.svg index abfce51..9fced7e 100644 --- a/doc/schema/classDiagram.svg +++ b/doc/schema/classDiagram.svg @@ -4,162 +4,666 @@ - - + + UmlClassDiagram - + 0 - -IncomeVault -src/IncomeVault.sol - -Private: -   __gap: uint256[50] - -Internal: -    __IncomeVault_init(admin: address, ERC20TokenPayment_: IERC20, cmtat_token: ICMTATSnapshot, ruleEngine_: IRuleEngine, authorizationEngineIrrevocable: IAuthorizationEngine, timeLimitToWithdraw_: uint256) <<onlyInitializing>> -    _msgSender(): (sender: address) -    _msgData(): bytes -    _contextSuffixLength(): uint256 -Public: -    constructor(forwarderIrrevocable: address) -    initialize(admin: address, ERC20TokenPayment_: IERC20, cmtat_token: ICMTATSnapshot, ruleEngine_: IRuleEngine, authorizationEngineIrrevocable: IAuthorizationEngine, timeLimitToWithdraw_: uint256) <<initializer>> + +IncomeVault +src/IncomeVault.sol + +Internal: +    _msgSender(): (sender: address) +    _msgData(): bytes +    _contextSuffixLength(): uint256 +    _authorizeDeposit() <<onlyRole>> +    _authorizeWithdraw() <<onlyRole>> +    _authorizeDistribute() <<onlyRole>> +    _authorizeOperator() <<onlyRole>> +    _authorizeSnapshotSourceManagement() <<onlyRole>> +    _authorizeRuleEngineManagement() <<onlyRole>> +    _authorizePause() <<onlyRole>> +    _authorizeDeactivate() <<onlyRole>> +    _authorizeFreeze() <<onlyRole>> +Public: +    constructor(forwarderIrrevocable: address) +    initialize(admin: address, ERC20TokenPayment_: IERC20, snapshotSource_: ISnapshotSource, ruleEngine_: IRuleEngine, timeLimitToWithdraw_: uint256) <<initializer>> +    supportsInterface(interfaceId: bytes4): bool - - -3 - -<<Abstract>> -IncomeVaultOpen -src/public/IncomeVaultOpen.sol - -Private: -   __gap: uint256[50] - -Public: -    validateTimeCode(time: uint256): (code: TIME_ERROR_CODE) -    validateTime(time: uint256) -    validateTimeBatch(times: uint256[]) -    claimDividend(time: uint256) <<nonReentrant>> -    claimDividendBatch(times: uint256[]) <<nonReentrant>> - - + + +1 + +<<Abstract>> +IncomeVaultBase +src/IncomeVaultBase.sol + +Internal: +    __IncomeVaultBase_init_unchained(ERC20TokenPayment_: IERC20, snapshotSource_: ISnapshotSource, timeLimitToWithdraw_: uint256) <<onlyInitializing>> +    _msgSender(): (sender: address) +    _msgData(): bytes +    _contextSuffixLength(): uint256 +Public: +    constructor(forwarderIrrevocable: address) + + -0->3 - - +0->1 + + + + + +4 + +<<Interface>> +IERC7741 +src/interfaces/IERC7741.sol + +External: +     authorizeOperator(controller: address, operator: address, approved: bool, nonce: bytes32, deadline: uint256, signature: bytes): (success: bool) +     invalidateNonce(nonce: bytes32) +     authorizations(controller: address, nonce: bytes32): (used: bool) +     DOMAIN_SEPARATOR(): bytes32 + + + +0->4 + + 5 - -<<Abstract>> -IncomeVaultRestricted -src/public/IncomeVaultRestricted.sol - -Private: -   __gap: uint256[50] - -Internal: -    __IncomeVaultRestricted_init_unchained(timeLimitToWithdraw_: uint256) <<onlyInitializing>> -Public: -    deposit(time: uint256, amount: uint256) <<onlyRole>> -    withdraw(time: uint256, amount: uint256, withdrawAddress: address) <<onlyRole>> -    withdrawAll(amount: uint256, withdrawAddress: address) <<onlyRole>> -    distributeDividend(addresses: address[], time: uint256) <<onlyRole>> -    setStatusClaim(time: uint256, status: bool) <<onlyRole>> -    setTimeLimitToWithdraw(timeLimitToWithdraw_: uint256) <<onlyRole>> + +<<Interface>> +ISnapshotSource +src/interfaces/ISnapshotSource.sol + +External: +     snapshotInfo(time: uint256, tokenHolder: address): (tokenHolderBalance: uint256, totalSupply: uint256) +     snapshotInfoBatch(time: uint256, addresses: address[]): (tokenHolderBalances: uint256[], totalSupply: uint256) +     snapshotInfoBatch(times: uint256[], addresses: address[]): (tokenHolderBalances: uint256[][], totalSupplies: uint256[]) - + 0->5 - - + + - - -1 - -<<Abstract>> -IncomeVaultInternal -src/libraries/IncomeVaultInternal.sol - -Public: -   CMTAT_TOKEN: ICMTATSnapshot -   ERC20TokenPayment: IERC20 -   claimedDividend: mapping(address=>mapping(uint256=>bool)) -   segregatedDividend: mapping(uint256=>uint256) -   segregatedClaim: mapping(uint256=>bool) -   timeLimitToWithdraw: uint256 - -Internal: -    _computeDividendBatch(time: uint256, tokenHolders: address[], tokenHoldersBalance: uint256[], tokenTotalSupply: uint256): (tokenHolderDividend: uint256[]) -    _computeDividend(time: uint256, senderBalance: uint256, tokenTotalSupply: uint256): (tokenHolderDividend: uint256) -    _transferDividend(time: uint256, tokenHolder: address, tokenHolderDividend: uint256) + + +10 + +<<Abstract>> +IncomeVaultRolesStorage +src/libraries/IncomeVaultRolesStorage.sol + +Public: +   INCOME_VAULT_OPERATOR_ROLE: bytes32 +   INCOME_VAULT_DEPOSIT_ROLE: bytes32 +   INCOME_VAULT_DISTRIBUTE_ROLE: bytes32 +   INCOME_VAULT_WITHDRAW_ROLE: bytes32 + + + +0->10 + + + + + +18 + +<<Abstract>> +IncomeVaultValidationModule +src/modules/IncomeVaultValidationModule.sol + +Internal: +    <<abstract>> _authorizeRuleEngineManagement() +    __IncomeVaultValidation_init_unchained(ruleEngine_: IRuleEngine) <<onlyInitializing>> +    _validateTransfer(from: address, to: address, value: uint256) +Public: +    <<modifier>> onlyRuleEngineManager() +    setRuleEngine(ruleEngine_: IRuleEngine) <<onlyRuleEngineManager>> +    canTransfer(from: address, to: address, value: uint256): bool +    detectTransferRestriction(from: address, to: address, value: uint256): uint8 +    messageForTransferRestriction(restrictionCode: uint8): string + + + +0->18 + + + + + +1->5 + + + + + +15 + +<<Abstract>> +IncomeVaultSnapshotModule +src/modules/IncomeVaultSnapshotModule.sol + +Private: +   SnapshotSourceStorageLocation: bytes32 + +Internal: +    <<abstract>> _authorizeSnapshotSourceManagement() +    _setDividendSnapshotSource(source: ISnapshotSource) +    _snapshotInfo(time: uint256, tokenHolder: address): (uint256, uint256) +    _snapshotInfoBatch(time: uint256, addresses: address[]): (uint256[], uint256) +    _snapshotInfoBatch(times: uint256[], addresses: address[]): (uint256[][], uint256[]) +    _getSnapshotSourceStorage(): ($: SnapshotSourceStorage) +Public: +    <<modifier>> onlySnapshotSourceManager() +    setDividendSnapshotSource(source: ISnapshotSource) <<onlySnapshotSourceManager>> +    dividendSnapshotSource(): ISnapshotSource + + + +1->15 + + + + + +17 + +<<Abstract>> +IncomeVaultValidationCore +src/modules/IncomeVaultValidationCore.sol + +Internal: +    <<abstract>> _validateTransfer(from: address, to: address, value: uint256) + + + +1->17 + + + + + +19 + +<<Abstract>> +VersionModule +src/modules/VersionModule.sol + +Private: +   VERSION: string + +Public: +    version(): (version_: string) + + + +1->19 + + + + + +20 + +<<Abstract>> +IncomeVaultOpen +src/public/IncomeVaultOpen.sol + +Internal: +    _claimDividend(sender: address, time: uint256) +    _claimDividendBatch(sender: address, times: uint256[]) +    _requireHolderOrOperator(holder: address) +Public: +    claimDividend(time: uint256) <<nonReentrant>> +    claimDividendFor(holder: address, time: uint256) <<nonReentrant>> +    claimDividendBatchFor(holder: address, times: uint256[]) <<nonReentrant>> +    claimDividendBatch(times: uint256[]) <<nonReentrant>> +    setOperator(operator: address, approved: bool): bool +    validateTimeCode(time: uint256): (code: TIME_ERROR_CODE) +    validateTime(time: uint256) +    validateTimeBatch(times: uint256[]) + + + +1->20 + + + + + +21 + +<<Abstract>> +IncomeVaultRestricted +src/public/IncomeVaultRestricted.sol + +Internal: +    <<abstract>> _authorizeDeposit() +    <<abstract>> _authorizeWithdraw() +    <<abstract>> _authorizeDistribute() +    <<abstract>> _authorizeOperator() +    __IncomeVaultRestricted_init_unchained(timeLimitToWithdraw_: uint256) <<onlyInitializing>> +Public: +    <<modifier>> onlyDepositManager() +    <<modifier>> onlyWithdrawManager() +    <<modifier>> onlyDistributeManager() +    <<modifier>> onlyVaultOperator() +    deposit(time: uint256, amount: uint256) <<onlyDepositManager>> +    depositBatch(times: uint256[], amounts: uint256[]) <<onlyDepositManager>> +    withdraw(time: uint256, amount: uint256, withdrawAddress: address) <<onlyWithdrawManager>> +    withdrawAll(amount: uint256, withdrawAddress: address) <<onlyWithdrawManager>> +    distributeDividend(addresses: address[], time: uint256) <<onlyDistributeManager>> +    distributeDividendBestEffort(addresses: address[], time: uint256): (paidCount: uint256, skipped: address[]) <<nonReentrant, onlyDistributeManager>> +    transferDividendSelf(time: uint256, tokenHolder: address, tokenHolderDividend: uint256) +    setStatusClaim(time: uint256, status: bool) <<onlyVaultOperator>> +    setTimeLimitToWithdraw(timeLimitToWithdraw_: uint256) <<onlyVaultOperator>> + + + +1->21 + + 2 - -<<Abstract>> -IncomeVaultInvariantStorage -src/libraries/IncomeVaultInvariantStorage.sol - -Public: -   INCOME_VAULT_OPERATOR_ROLE: bytes32 -   INCOME_VAULT_DEPOSIT_ROLE: bytes32 -   INCOME_VAULT_DISTRIBUTE_ROLE: bytes32 -   INCOME_VAULT_WITHDRAW_ROLE: bytes32 - -Public: -    <<event>> newDeposit(time: uint256, sender: address, dividend: uint256) -    <<event>> DividendClaimed(time: uint256, sender: address, dividend: uint256) - - - -1->2 - - + +IncomeVaultOwnable2Step +src/IncomeVaultOwnable2Step.sol + +Internal: +    _msgSender(): (sender: address) +    _msgData(): bytes +    _contextSuffixLength(): uint256 +    _authorizeDeposit() <<onlyOwner>> +    _authorizeWithdraw() <<onlyOwner>> +    _authorizeDistribute() <<onlyOwner>> +    _authorizeOperator() <<onlyOwner>> +    _authorizeSnapshotSourceManagement() <<onlyOwner>> +    _authorizeRuleEngineManagement() <<onlyOwner>> +    _authorizePause() <<onlyOwner>> +    _authorizeDeactivate() <<onlyOwner>> +    _authorizeFreeze() <<onlyOwner>> +Public: +    constructor(forwarderIrrevocable: address) +    initialize(owner_: address, ERC20TokenPayment_: IERC20, snapshotSource_: ISnapshotSource, ruleEngine_: IRuleEngine, timeLimitToWithdraw_: uint256) <<initializer>> +    supportsInterface(interfaceId: bytes4): bool - + + +2->1 + + + + + +2->4 + + + + + +2->5 + + + + + +11 + +<<Abstract>> +Ownable2StepERC165Module +src/libraries/Ownable2StepERC165Module.sol + +Public: +   IERC173_INTERFACE_ID: bytes4 +   IOWNABLE2STEP_INTERFACE_ID: bytes4 + +Public: +    supportsInterface(interfaceId: bytes4): bool + + + +2->11 + + + + + +2->18 + + + + -4 - -<<Enum>> -TIME_ERROR_CODE -src/public/IncomeVaultOpen.sol - -OK: 0 -CLAIM_NOT_ACTIVATED: 1 -TOO_LATE_TO_WITHDRAW: 2 -TOO_EARLY_TO_WITHDRAW: 3 - - - -4->3 - - +3 + +<<Interface>> +IERC7540Operator +src/interfaces/IERC7540Operator.sol + +External: +     setOperator(operator: address, approved: bool): (success: bool) +     isOperator(controller: address, operator: address): (status: bool) +Public: +    <<event>> OperatorSet(controller: address, operator: address, approved: bool) - - -3->1 - - + + +6 + +<<Abstract>> +IncomeVaultInternal +src/libraries/IncomeVaultInternal.sol + +Private: +   IncomeVaultInternalStorageLocation: bytes32 + +Internal: +    _transferDividend(time: uint256, tokenHolder: address, tokenHolderDividend: uint256) +    _setOperator(controller: address, operator: address, approved: bool) +    _setERC20TokenPayment(ERC20TokenPayment_: IERC20) +    _setTimeLimitToWithdraw(timeLimitToWithdraw_: uint256) +    _setStatusClaim(time: uint256, status: bool) +    _computeDividendBatch(time: uint256, tokenHolders: address[], tokenHoldersBalance: uint256[], tokenTotalSupply: uint256): (tokenHolderDividend: uint256[]) +    _computeDividend(time: uint256, senderBalance: uint256, tokenTotalSupply: uint256): (tokenHolderDividend: uint256) +    _revertOnInvalidTime(code: TIME_ERROR_CODE) +    _timeCode($: IncomeVaultInternalStorage, time: uint256, timeLimit: uint256): (code: TIME_ERROR_CODE) +    _getIncomeVaultInternalStorage(): ($: IncomeVaultInternalStorage) +Public: +    ERC20TokenPayment(): IERC20 +    claimedDividend(tokenHolder: address, time: uint256): bool +    segregatedDividend(time: uint256): uint256 +    segregatedClaim(time: uint256): bool +    isOperator(controller: address, operator: address): bool +    paidDividend(time: uint256): uint256 +    unclaimedDividend(time: uint256): uint256 +    openClaimCount(): uint256 +    timeLimitToWithdraw(): uint256 - - -3->4 - - + + +6->3 + + - - -5->1 - - + + +7 + +<<Enum>> +TIME_ERROR_CODE +src/libraries/IncomeVaultInternal.sol + +OK: 0 +CLAIM_NOT_ACTIVATED: 1 +TOO_LATE_TO_WITHDRAW: 2 +TOO_EARLY_TO_WITHDRAW: 3 + + + +6->7 + + + + + +8 + +<<Struct>> +IncomeVaultInternalStorage +src/libraries/IncomeVaultInternal.sol + +_ERC20TokenPayment: IERC20 +_claimedDividend: mapping(address=>mapping(uint256=>bool)) +_segregatedDividend: mapping(uint256=>uint256) +_segregatedClaim: mapping(uint256=>bool) +_timeLimitToWithdraw: uint256 +_openClaimCount: uint256 +_paidDividend: mapping(uint256=>uint256) +_isOperator: mapping(address=>mapping(address=>bool)) + + + +6->8 + + + + + +9 + +<<Abstract>> +IncomeVaultInvariantStorage +src/libraries/IncomeVaultInvariantStorage.sol + +Public: +    <<event>> newDeposit(time: uint256, sender: address, dividend: uint256) +    <<event>> DividendClaimed(time: uint256, sender: address, dividend: uint256) +    <<event>> ERC20TokenPaymentSet(newERC20TokenPayment: IERC20) +    <<event>> ClaimStatusSet(time: uint256, status: bool) +    <<event>> TimeLimitToWithdrawSet(timeLimitToWithdraw: uint256) +    <<event>> Withdraw(time: uint256, withdrawAddress: address, amount: uint256) +    <<event>> DividendDistributionSkipped(time: uint256, tokenHolder: address, reason: bytes) +    <<event>> WithdrawAll(withdrawAddress: address, amount: uint256) +    <<event>> DividendSnapshotSourceSet(newSource: ISnapshotSource) + + + +6->9 + + + + + +7->6 + + + + + +8->6 + + + + + +9->5 + + + + + +12 + +<<Abstract>> +ERC7741Module +src/modules/ERC7741Module.sol + +Private: +   ERC7741ModuleStorageLocation: bytes32 +Public: +   AUTHORIZE_OPERATOR_TYPEHASH: bytes32 + +Internal: +    _getERC7741ModuleStorage(): ($: ERC7741ModuleStorage) +Public: +    authorizeOperator(controller: address, operator: address, approved: bool, nonce: bytes32, deadline: uint256, signature: bytes): (success: bool) +    invalidateNonce(nonce: bytes32) +    authorizations(controller: address, nonce: bytes32): (used: bool) +    DOMAIN_SEPARATOR(): bytes32 + + + +12->4 + + + + + +12->6 + + + + + +13 + +<<Struct>> +ERC7741ModuleStorage +src/modules/ERC7741Module.sol + +_authorizations: mapping(address=>mapping(bytes32=>bool)) + + + +12->13 + + + + + +13->12 + + + + + +14 + +<<Abstract>> +IncomeVaultSnapshotCore +src/modules/IncomeVaultSnapshotCore.sol + +Internal: +    <<abstract>> _snapshotInfo(time: uint256, tokenHolder: address): (tokenHolderBalance: uint256, totalSupply: uint256) +    <<abstract>> _snapshotInfoBatch(time: uint256, addresses: address[]): (tokenHolderBalances: uint256[], totalSupply: uint256) +    <<abstract>> _snapshotInfoBatch(times: uint256[], addresses: address[]): (tokenHolderBalances: uint256[][], totalSupplies: uint256[]) + + + +15->5 + + +snapshotInfo +snapshotInfoBatch + + + +15->6 + + + + + +15->14 + + + + + +16 + +<<Struct>> +SnapshotSourceStorage +src/modules/IncomeVaultSnapshotModule.sol + +_source: ISnapshotSource + + + +15->16 + + + + + +16->5 + + + + + +16->15 + + + + + +18->9 + + + + + +18->17 + + + + + +20->7 + + + + + +20->8 + + + + + +20->12 + + + + + +20->14 + + + + + +20->17 + + + + + +21->6 + + + + + +21->8 + + + + + +21->14 + + + + + +21->17 + + diff --git a/doc/schema/drawio/IncomeVault-Global.drawio.png b/doc/schema/drawio/IncomeVault-Global.drawio.png deleted file mode 100644 index 740a12b..0000000 Binary files a/doc/schema/drawio/IncomeVault-Global.drawio.png and /dev/null differ diff --git a/doc/schema/drawio/IncomeVault-RuleEngine.drawio.png b/doc/schema/drawio/IncomeVault-RuleEngine.drawio.png deleted file mode 100644 index 50b4d6a..0000000 Binary files a/doc/schema/drawio/IncomeVault-RuleEngine.drawio.png and /dev/null differ diff --git a/doc/schema/drawio/IncomeVault-Segragated Deposit.drawio.png b/doc/schema/drawio/IncomeVault-Segragated Deposit.drawio.png deleted file mode 100644 index 799606d..0000000 Binary files a/doc/schema/drawio/IncomeVault-Segragated Deposit.drawio.png and /dev/null differ diff --git a/doc/schema/drawio/IncomeVault-claimDividend.drawio.png b/doc/schema/drawio/IncomeVault-claimDividend.drawio.png deleted file mode 100644 index e3e2436..0000000 Binary files a/doc/schema/drawio/IncomeVault-claimDividend.drawio.png and /dev/null differ diff --git a/doc/schema/plantuml/incomevault-architecture.png b/doc/schema/plantuml/incomevault-architecture.png new file mode 100644 index 0000000..f7dd281 Binary files /dev/null and b/doc/schema/plantuml/incomevault-architecture.png differ diff --git a/doc/schema/plantuml/incomevault-architecture.puml b/doc/schema/plantuml/incomevault-architecture.puml new file mode 100644 index 0000000..afab372 --- /dev/null +++ b/doc/schema/plantuml/incomevault-architecture.puml @@ -0,0 +1,36 @@ +@startuml +title IncomeVault — architecture at a glance + +skinparam shadowing false +skinparam componentStyle rectangle +skinparam ComponentBackgroundColor #EEF5FF +skinparam ComponentBorderColor #4A6FA5 +skinparam ActorBorderColor #4A6FA5 +skinparam NoteBackgroundColor #FDF6E3 +skinparam NoteBorderColor #B58900 + +actor "Issuer" as ISSUER +actor "Token holder" as HOLDER + +component "IncomeVault" as VAULT + +component "Security token\nCMTAT or any ERC-20" as TOKEN #FFF6E5 +component "Snapshot source\n<>" as SNAPSHOT #FFF6E5 +component "RuleEngine\noptional" as RULE #FFF6E5 +component "Payment token\nERC-20, e.g. USDC" as PAYMENT #FFF6E5 + +ISSUER --> VAULT : deposits the dividends,\nopens the claims +HOLDER --> VAULT : claims a dividend +HOLDER ..> TOKEN : holds +TOKEN --> SNAPSHOT : records who held\nhow much, and when +VAULT --> SNAPSHOT : reads the balances\nat the dividend date +VAULT --> RULE : may this holder\nbe paid? +VAULT --> PAYMENT : pays the holder + +note bottom of VAULT + The vault never talks to the security token. + It reads balances through <>, + so any token exposing that interface works. +end note + +@enduml diff --git a/doc/schema/plantuml/incomevault-claimdividend.png b/doc/schema/plantuml/incomevault-claimdividend.png new file mode 100644 index 0000000..0c0f2f2 Binary files /dev/null and b/doc/schema/plantuml/incomevault-claimdividend.png differ diff --git a/doc/schema/plantuml/incomevault-claimdividend.puml b/doc/schema/plantuml/incomevault-claimdividend.puml new file mode 100644 index 0000000..33eb1c7 --- /dev/null +++ b/doc/schema/plantuml/incomevault-claimdividend.puml @@ -0,0 +1,79 @@ +@startuml +title claimDividend(time) + +skinparam shadowing false +skinparam ActivityBackgroundColor #EEF5FF +skinparam ActivityBorderColor #4A6FA5 +skinparam ActivityDiamondBackgroundColor #FDF6E3 +skinparam ActivityDiamondBorderColor #B58900 +skinparam NoteBackgroundColor #FDF6E3 +skinparam NoteBorderColor #B58900 + +start +:claimDividend(time) +called by the token holder;<<#EEF5FF>> +note right: nonReentrant + +partition "validateTime(time)" { + if (segregatedClaim[time]) then (open) + else (closed) + :revert IncomeVault_ClaimNotActivated;<<#FDEEEE>> + stop + endif + + if (block.timestamp > time + timeLimitToWithdraw) then (too late) + :revert IncomeVault_TooLateToWithdraw;<<#FDEEEE>> + stop + else (in window) + endif + + if (block.timestamp < time) then (too early) + :revert IncomeVault_TooEarlyToWithdraw;<<#FDEEEE>> + stop + else (in window) + endif +} + +if (claimedDividend[sender][time]) then (already claimed) + :revert IncomeVault_DividendAlreadyClaimed;<<#FDEEEE>> + stop +else (not claimed yet) +endif + +:(senderBalance, tokenTotalSupply) = +snapshotEngine.snapshotInfo(time, sender);<<#FFF6E5>> +note right + External call to the + <> source +end note + +if (senderBalance == 0) then (yes) + :revert IncomeVault_TokenBalanceIsZero;<<#FDEEEE>> + stop +else (no) +endif + +:senderDividend = senderBalance * segregatedDividend[time] / tokenTotalSupply; +note right: rounded down + +if (senderDividend == 0) then (yes) + :revert IncomeVault_NoDividendToClaim;<<#FDEEEE>> + stop +else (no) +endif + +if (canTransfer(vault, sender, senderDividend)\npause, freeze and RuleEngine) then (allowed) +else (forbidden) + :revert IncomeVault_InvalidTransfer;<<#FDEEEE>> + stop +endif + +partition "_transferDividend" { + :claimedDividend[sender][time] = true; + note right: set before the external call + :emit DividendClaimed(time, sender, senderDividend); + :ERC20TokenPayment.safeTransfer(sender, senderDividend);<<#FFF6E5>> +} +stop + +@enduml diff --git a/doc/schema/plantuml/incomevault-global.png b/doc/schema/plantuml/incomevault-global.png new file mode 100644 index 0000000..5ac7bd0 Binary files /dev/null and b/doc/schema/plantuml/incomevault-global.png differ diff --git a/doc/schema/plantuml/incomevault-global.puml b/doc/schema/plantuml/incomevault-global.puml new file mode 100644 index 0000000..ac3c026 --- /dev/null +++ b/doc/schema/plantuml/incomevault-global.puml @@ -0,0 +1,54 @@ +@startuml +title IncomeVault — global flow + +skinparam shadowing false +skinparam sequenceMessageAlign center +skinparam ParticipantBackgroundColor #EEF5FF +skinparam ParticipantBorderColor #4A6FA5 +skinparam ActorBorderColor #4A6FA5 +skinparam NoteBackgroundColor #FDF6E3 +skinparam NoteBorderColor #B58900 + +actor "SNAPSHOOTER_ROLE" as SNAPSHOOTER +actor "INCOME_VAULT_DEPOSIT_ROLE" as DEPOSITOR +actor "INCOME_VAULT_OPERATOR_ROLE" as OPERATOR +actor "Token holder" as HOLDER + +participant "IncomeVault" as VAULT +participant "SnapshotEngine\n<>" as SNAPSHOT +participant "Payment token\n<>" as PAYMENT + +== 0. Register the dividend date == +SNAPSHOOTER -> SNAPSHOT : scheduleSnapshot(time) +note right of SNAPSHOT + Records the holder balances + and the total supply at "time" +end note + +== 1. Fund the vault == +DEPOSITOR -> VAULT : deposit(time, amount) +VAULT -> PAYMENT : safeTransferFrom(depositor, vault, amount) +VAULT -> VAULT : segregatedDividend[time] += amount + +== 2. Open the claims == +OPERATOR -> VAULT : setStatusClaim(time, true) +note right of VAULT + Do not deposit for a "time" + whose claims are already open: + it dilutes the holders who + have not claimed yet +end note + +== 3. Claim the dividends == +HOLDER -> VAULT : claimDividend(time) +activate VAULT +VAULT -> SNAPSHOT : snapshotInfo(time, holder) +SNAPSHOT --> VAULT : (holderBalance, totalSupply) +VAULT -> VAULT : dividend = holderBalance * segregatedDividend[time] / totalSupply +note right of VAULT : rounded down +VAULT -> VAULT : validate the payout\n(pause, freeze, RuleEngine) +VAULT -> PAYMENT : safeTransfer(holder, dividend) +PAYMENT --> HOLDER : dividend +deactivate VAULT + +@enduml diff --git a/doc/schema/plantuml/incomevault-ruleengine.png b/doc/schema/plantuml/incomevault-ruleengine.png new file mode 100644 index 0000000..beb76b8 Binary files /dev/null and b/doc/schema/plantuml/incomevault-ruleengine.png differ diff --git a/doc/schema/plantuml/incomevault-ruleengine.puml b/doc/schema/plantuml/incomevault-ruleengine.puml new file mode 100644 index 0000000..22abbd4 --- /dev/null +++ b/doc/schema/plantuml/incomevault-ruleengine.puml @@ -0,0 +1,45 @@ +@startuml +title Contracts called when a token holder claims a dividend + +skinparam shadowing false +skinparam componentStyle rectangle +skinparam ComponentBackgroundColor #EEF5FF +skinparam ComponentBorderColor #4A6FA5 +skinparam ActorBorderColor #4A6FA5 +skinparam NoteBackgroundColor #FDF6E3 +skinparam NoteBorderColor #B58900 + +actor "Token holder" as HOLDER + +component "IncomeVault" as VAULT +component "SnapshotEngine\n<>" as SNAPSHOT +component "RuleEngine\n<>" as RULE +component "Rules\nallowlist, blocklist,\nsanction list, ..." as RULES #FFF6E5 +component "Payment token\n<>" as PAYMENT #FFF6E5 + +' solid = call, dashed = return value +HOLDER -right-> VAULT : 1. claimDividend(time) +VAULT -down-> SNAPSHOT : 2. snapshotInfo(time, holder) +SNAPSHOT ..> VAULT : 3. (holderBalance, totalSupply) +VAULT -up-> RULE : 4. canTransfer(vault, holder, dividend) +RULE -left-> RULES : 5. detectTransferRestriction(...) +RULE ..> VAULT : 6. true / false +VAULT -down-> PAYMENT : 7. safeTransfer(holder, dividend) + +note bottom of VAULT + Before step 4 the vault checks its own + pause state and the frozen status of the + holder. A false at step 6 reverts the claim + with IncomeVault_InvalidTransfer. +end note + +note right of RULE + Steps 4 to 6 are a **view** call. + The vault is not a token bound to the + engine, so it never calls transferred(): + a payout moves the payment token, not + the security token, and must not update + the stateful rules. +end note + +@enduml diff --git a/doc/schema/plantuml/incomevault-segregated-deposit.png b/doc/schema/plantuml/incomevault-segregated-deposit.png new file mode 100644 index 0000000..83d7eaa Binary files /dev/null and b/doc/schema/plantuml/incomevault-segregated-deposit.png differ diff --git a/doc/schema/plantuml/incomevault-segregated-deposit.puml b/doc/schema/plantuml/incomevault-segregated-deposit.puml new file mode 100644 index 0000000..f9a6fcc --- /dev/null +++ b/doc/schema/plantuml/incomevault-segregated-deposit.puml @@ -0,0 +1,33 @@ +@startuml +title IncomeVault — segregated deposit + +skinparam shadowing false +skinparam NoteBackgroundColor #FDF6E3 +skinparam NoteBorderColor #B58900 + +database "IncomeVault\npayment-token balance" as VAULT #FDF6E3 + +map "segregatedDividend" as SD #EEF5FF { + timeX => amountX + timeY => amountY + timeZ => amountZ +} + +VAULT -down-> SD : split per dividend time + +note right of SD + amountX + amountY + amountZ + equals + ERC20TokenPayment.balanceOf(IncomeVault) +end note + +note bottom of SD + Each deposit is segregated in its dividend time: + deposit(timeY, x) increases amountY only, + withdraw(timeY, x) decreases amountY only. + + A claim or a withdraw for one time never + touches the amount held for another. +end note + +@enduml diff --git a/doc/script/check_sizes.py b/doc/script/check_sizes.py new file mode 100755 index 0000000..dc19715 --- /dev/null +++ b/doc/script/check_sizes.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Fail if any DEPLOYABLE contract exceeds the EIP-170 runtime limit. + +`forge build --sizes` checks every compiled contract, including the ones under `test/`, +and fails the whole build if any is over. Today that is `test/mocks/CMTATDividendHostMock` +at ~33 KB: a CMTAT with the distribution logic embedded, which exists only to prove that +combination still compiles and is never deployed. Verify with: + + forge build --sizes | awk -F'|' 'NF>3 {gsub(/[ ,]/,"",$3); if ($3+0>24576) print $2, $3}' + +`--skip` cannot exclude it: skipping any file makes the compilation partial, and the +OpenZeppelin Upgrades plugin then rejects the build-info with "is not from a full +compilation", which fails every test. So the build stays full and the size check is +applied here instead, scoped to `src/` by reading each artifact's own compilationTarget. + +If that mock is ever removed and nothing else under `test/` exceeds the limit, this +script stops being necessary and `forge build --sizes` can go back into the Makefile. +""" +import json, pathlib, sys + +LIMIT = 24576 # EIP-170 runtime size limit +WARN = 0.9 # report anything already this close + +over, near, checked = [], [], 0 +for art in pathlib.Path('out').rglob('*.json'): + try: + j = json.loads(art.read_text()) + except Exception: + continue + targets = j.get('metadata', {}).get('settings', {}).get('compilationTarget', {}) + src = next((p for p in targets if p.startswith('src/')), None) + if not src: + continue + obj = (j.get('deployedBytecode') or {}).get('object', '') + size = max(0, (len(obj) - 2) // 2) + if size == 0: + continue # interface or fully abstract + checked += 1 + name = targets[src] + if size > LIMIT: + over.append((name, src, size)) + elif size > LIMIT * WARN: + near.append((name, src, size)) + +for name, src, size in sorted(near, key=lambda x: -x[2]): + print(f" NOTE: {name} is {size} bytes, {LIMIT - size} under the EIP-170 limit ({src})") + +# Fail loudly rather than pass silently. Every filter above can legitimately empty the +# set, so "nothing to report" and "nothing was looked at" print the same reassuring line. +# If `metadata` ever stops being emitted, `compilationTarget` disappears, every contract +# is skipped, and this check would otherwise report success having measured nothing. +if checked == 0: + print(" EIP-170: measured NOTHING — no deployable contract found under src/.") + print(" Either the build did not run, or the artifacts carry no `metadata." + "settings.compilationTarget` to scope by.") + sys.exit(1) + +if over: + print(f" EIP-170: {len(over)} deployable contract(s) exceed {LIMIT} bytes:") + for name, src, size in sorted(over, key=lambda x: -x[2]): + print(f" {name} {size} (+{size - LIMIT}) {src}") + sys.exit(1) + +print(f" EIP-170: {checked} deployable contract(s) in src/, all within {LIMIT} bytes") diff --git a/doc/script/convert_links_for_pdf.sh b/doc/script/convert_links_for_pdf.sh new file mode 100755 index 0000000..24ac8dc --- /dev/null +++ b/doc/script/convert_links_for_pdf.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Script to replace relative markdown links with GitHub links for PDF generation +# Preserves image links (they render in PDF) and external links + +set -e + +if [ -z "$1" ]; then + echo "Usage: $0 [input-file] [output-file]" + echo "" + echo "The release link may point at the repository root or at the input file's" + echo "own directory; the missing part is derived from where the file sits." + echo "The output defaults to README_UPDATE.md beside the input file, so the" + echo "image links it keeps relative still resolve." + echo "" + echo "Example:" + echo " $0 https://github.com/CMTA/CMTAT/blob/v3.0.0" + echo " $0 https://github.com/CMTA/CMTAT/blob/v3.0.0/doc" + echo " $0 https://github.com/CMTA/CMTAT/blob/v3.0.0 ../README.md README_UPDATE.md" + exit 1 +fi + +GITHUB_LINK="${1%/}" # Remove trailing slash if present + +INPUT_FILE="${2:-../README.md}" # doc/README.md, the full reference (the root README is a short summary) + +if [ ! -f "$INPUT_FILE" ]; then + echo "Error: Input file '$INPUT_FILE' not found" + exit 1 +fi + +# The links in the input file are relative to the file, so the base URL has to be +# too. Accept either form -- the repository root (".../blob/") or the file's +# own directory (".../blob//doc") -- and derive whichever half is missing +# from the file's path inside the repository. Passing the root form used to +# rewrite every "./" link one directory too high, silently: doc/README.md's +# "./technical/x.md" became ".../blob//technical/x.md", a 404 in the PDF. +INPUT_DIR=$(cd "$(dirname "$INPUT_FILE")" && pwd) +REPO_ROOT=$(git -C "$INPUT_DIR" rev-parse --show-toplevel 2>/dev/null || true) +REL_DIR="" +if [ -n "$REPO_ROOT" ]; then + REL_DIR="${INPUT_DIR#"$REPO_ROOT"}" + REL_DIR="${REL_DIR#/}" # "doc", or "" when the input file is at the root +fi + +if [ -n "$REL_DIR" ] && [ "${GITHUB_LINK%/$REL_DIR}" = "$GITHUB_LINK" ]; then + GITHUB_LINK="$GITHUB_LINK/$REL_DIR" +fi + +# Base URL for the parent of the input file's directory, used by Step 0 to +# rewrite "../path" links -- how doc/README.md must reference repository-root +# siblings such as test/ and src/. Empty when the input file is itself at the +# root, where "../" points outside the repository and cannot be expressed. +GITHUB_LINK_PARENT="" +if [ -n "$REL_DIR" ]; then + GITHUB_LINK_PARENT="${GITHUB_LINK%/*}" +fi + +# Default the output to the input file's own directory, not the caller's working +# directory. The conversion leaves image links relative on purpose, so the +# converted file only renders correctly from where the original sits: written +# anywhere else, doc/README.md's "./schema/x.png" points at nothing. +OUTPUT_FILE="${3:-$INPUT_DIR/README_UPDATE.md}" + +# Create a temporary file +TMP_FILE=$(mktemp) +cp "$INPUT_FILE" "$TMP_FILE" + +# Use a placeholder to avoid sed escaping issues +PLACEHOLDER="__GITHUB_LINK__" +PLACEHOLDER_PARENT="__GITHUB_LINK_PARENT__" + +# Step 0: convert parent-relative links [text](../...) before Step 1, which only +# recognizes the "./" form and would leave these relative and dead in the PDF. +if grep -qE '\]\(\.\./[^)]+\)' "$TMP_FILE"; then + if [ -z "$GITHUB_LINK_PARENT" ]; then + echo "Error: '$INPUT_FILE' contains '../' links, which point outside the repository from this location." >&2 + echo "Rewrite them as absolute URLs, or generate the PDF from a file in a subdirectory such as doc/." >&2 + rm -f "$TMP_FILE" + exit 1 + fi + sed -i -E "s|\[([^]]+)\]\(\.\./([^)]+)\)|[\1]($PLACEHOLDER_PARENT/\2)|g" "$TMP_FILE" +fi + +# Step 1: Convert ALL relative links [text](./...) to placeholder +sed -i -E "s|\[([^]]+)\]\(\./([^)]+)\)|[\1]($PLACEHOLDER/\2)|g" "$TMP_FILE" + +# Step 2: Restore image links back to relative (images render inline in PDF) +for ext in png jpg jpeg gif svg ico webp bmp tiff; do + sed -i -E "s|\[([^]]+)\]\($PLACEHOLDER/([^)]+\.$ext)\)|[\1](./\2)|gi" "$TMP_FILE" + sed -i -E "s|\[([^]]+)\]\($PLACEHOLDER_PARENT/([^)]+\.$ext)\)|[\1](../\2)|gi" "$TMP_FILE" +done + +# Step 3: Replace placeholders with actual GitHub links (parent first) +sed -i "s|$PLACEHOLDER_PARENT|$GITHUB_LINK_PARENT|g" "$TMP_FILE" +sed -i "s|$PLACEHOLDER|$GITHUB_LINK|g" "$TMP_FILE" + +mv "$TMP_FILE" "$OUTPUT_FILE" + +echo "Created '$OUTPUT_FILE' with GitHub links pointing to:" +echo " $GITHUB_LINK" diff --git a/doc/script/convert_links_for_pdf_root.sh b/doc/script/convert_links_for_pdf_root.sh new file mode 100755 index 0000000..9bd89aa --- /dev/null +++ b/doc/script/convert_links_for_pdf_root.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Same conversion as convert_links_for_pdf.sh, applied to the root README.md +# (the short summary) instead of doc/README.md (the full reference). +# +# It differs only in its default input, so it delegates rather than duplicating: +# - input : the repository's root README.md +# The output follows the input's directory, which the other script already does, +# so it needs no default here. Link rewriting, image handling and the base-URL +# derivation all stay there too; change behaviour there and both entry points +# follow. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONVERT="$SCRIPT_DIR/convert_links_for_pdf.sh" +REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || (cd "$SCRIPT_DIR/../.." && pwd))" + +if [ -z "$1" ]; then + echo "Usage: $0 [input-file] [output-file]" + echo "" + echo "Converts the root README.md. For doc/README.md use convert_links_for_pdf.sh." + echo "" + echo "Example:" + echo " $0 https://github.com/CMTA/Rules/blob/v0.6.0" + exit 1 +fi + +if [ ! -x "$CONVERT" ]; then + echo "Error: '$CONVERT' not found or not executable" + exit 1 +fi + +exec "$CONVERT" "$1" "${2:-$REPO_ROOT/README.md}" "${3:-}" diff --git a/doc/script/coverage-README.md b/doc/script/coverage-README.md new file mode 100644 index 0000000..532d2fd --- /dev/null +++ b/doc/script/coverage-README.md @@ -0,0 +1,28 @@ +# Coverage report + +Generated output. **Do not edit** — regenerate it instead: + +```bash +make coverage-report # HTML here, needs lcov/genhtml +make coverage # summary table in the terminal, no extra tooling +``` + +`make coverage-report` deletes and recreates this directory, so this note is copied back in by the Makefile from `doc/script/coverage-README.md`. Edit it there. + +This report is **committed**, so it is only as trustworthy as its last run: regenerate it in the same commit as any change under `src/`. A tracked report describing a different codebase is worse than no report at all. + +## Reading the numbers + +Two files report **0%** and that is expected, not a gap: `IncomeVaultSnapshotCore` and `IncomeVaultValidationCore` declare hooks with **no bodies**. There is no code in them to execute, so nothing can cover them. They exist to be inherited and answered elsewhere. + +Function coverage is the lowest figure and the least useful one here. It counts `internal` helpers and the `_authorize*` overrides — empty bodies whose whole purpose is to carry a modifier — so a payout path exercised end to end still leaves several "uncovered" functions behind. + +## Scope + +`src/` only. Tests, mocks and `script/` are excluded, via: + +``` +forge coverage --ffi --exclude-tests --no-match-coverage '(test|mocks?|script)/' +``` + +Without `--ffi` every test fails: the OpenZeppelin Upgrades plugin shells out to `@openzeppelin/upgrades-core`. diff --git a/doc/script/gen_toc.py b/doc/script/gen_toc.py new file mode 100644 index 0000000..d1fe073 --- /dev/null +++ b/doc/script/gen_toc.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Generate a GitHub-compatible table of contents for Markdown files. + + gen_toc.py FILE [FILE...] [--max-level N] [--min-level N] [--check] + +GitHub does not render `[TOC]`, `[[_TOC_]]` or `{:toc}` — those are Doxygen, GitLab +and Kramdown syntax. On GitHub they display as literal text, so a file relying on one +has a dead line where its navigation should be. This writes a real Markdown list. + +Placement, in order of preference: + 1. between `` and `` (replaced in place; use this) + 2. in place of a `[TOC]` / `[[_TOC_]]` / `{:toc}` line, which is then replaced by + marker-wrapped output so later runs update in place + +If neither is present the file is left alone and the tool says so. It does not guess +an insertion point. + +Every generated link is verified against the headings before the file is written. +Exit codes: 0 ok, 1 a file needs regenerating (--check) or a link failed to verify. +""" +import sys, re, pathlib + +PLACEHOLDER = re.compile(r'^[ \t]*(\[TOC\]|\[\[_TOC_\]\]|\{:toc\})[ \t]*$', re.M) +MARKERS = re.compile(r'.*?', re.S) +ATX = re.compile(r'^(#{1,6}) +(.*?)[ \t]*#*[ \t]*$') + + +def clean(text): + """Heading text as GitHub renders it: markup stripped, content kept.""" + text = re.sub(r'`([^`]*)`', r'\1', text) + text = re.sub(r'\*\*([^*]*)\*\*', r'\1', text) + text = re.sub(r'__([^_]*)__', r'\1', text) + text = re.sub(r'\*([^*]*)\*', r'\1', text) + text = re.sub(r'!?\[([^\]]*)\]\([^)]*\)', r'\1', text) + return text.strip() + + +def anchor(text, seen): + """GitHub's slug: lowercase, drop all but word/space/hyphen, spaces to hyphens, + then a -1/-2 suffix for repeats. Unicode letters are kept (\\w with re.UNICODE).""" + a = re.sub(r'[^\w\s-]', '', text.lower(), flags=re.UNICODE).replace(' ', '-') + n = seen.get(a, 0) + seen[a] = n + 1 + return a if n == 0 else f"{a}-{n}" + + +def headings(src): + """(level, cleaned_text) for every ATX heading outside a fenced code block.""" + out, fence = [], None + for line in src.splitlines(): + s = line.strip() + m = re.match(r'^(`{3,}|~{3,})', s) + if m: + tok = m.group(1)[0] + if fence is None: + fence = tok + elif fence == tok: + fence = None + continue + if fence: + continue + m = ATX.match(line) + if m: + out.append((len(m.group(1)), clean(m.group(2)))) + return out + + +def build(src, lo, hi): + seen, lines, valid = {}, [], set() + for lvl, text in headings(src): + a = anchor(text, seen) # EVERY heading consumes a slot, listed or not + valid.add(a) + if lo <= lvl <= hi and text: + lines.append(f"{' ' * (lvl - lo)}- [{text}](#{a})") + return lines, valid + + +def process(path, lo, hi, check): + p = pathlib.Path(path) + src = p.read_text(encoding='utf-8') + lines, valid = build(src, lo, hi) + if not lines: + print(f" {path}: no headings between level {lo} and {hi} — skipped") + return 0 + + broken = [l for l in lines if l.split('](#')[1][:-1] not in valid] + if broken: + print(f" !! {path}: {len(broken)} link(s) failed verification — NOT written") + for b in broken[:5]: + print(f" {b}") + return 1 + + block = "\n\n" + "\n".join(lines) + "\n\n" + if MARKERS.search(src): + new = MARKERS.sub(lambda _: block, src, count=1) + how = "updated" + elif PLACEHOLDER.search(src): + new = PLACEHOLDER.sub(lambda _: block, src, count=1) + how = "replaced placeholder" + else: + print(f" -- {path}: no markers and no [TOC] placeholder — left alone.") + print(f" Add `` / `` where the contents should go.") + return 0 + + if new == src: + print(f" ok (up to date): {path} — {len(lines)} entries") + return 0 + if check: + print(f" STALE: {path} — {len(lines)} entries would change") + return 1 + p.write_text(new, encoding='utf-8') + print(f" {how}: {path} — {len(lines)} entries, all links verified") + return 0 + + +def main(): + args = sys.argv[1:] + lo, hi, check, files = 2, 3, False, [] + i = 0 + while i < len(args): + a = args[i] + if a == '--check': + check = True + elif a.startswith('--max-level'): + hi = int(a.split('=', 1)[1]) if '=' in a else int(args[(i := i + 1)]) + elif a.startswith('--min-level'): + lo = int(a.split('=', 1)[1]) if '=' in a else int(args[(i := i + 1)]) + elif a in ('-h', '--help'): + print(__doc__); return 0 + else: + files.append(a) + i += 1 + if not files: + print(__doc__); return 2 + if lo < 1 or hi > 6 or lo > hi: + print(f" !! bad level range {lo}..{hi}"); return 2 + return max(process(f, lo, hi, check) for f in files) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/doc/solidityAPI/index.md b/doc/solidityAPI/index.md index efc03fb..86d0b95 100644 --- a/doc/solidityAPI/index.md +++ b/doc/solidityAPI/index.md @@ -1,415 +1,2942 @@ # Solidity API -## IncomeVault - -### constructor - -```solidity -constructor(address forwarderIrrevocable) public -``` +## IncomeVaultBase -### initialize +### __IncomeVaultBase_init_unchained ```solidity -function initialize(address admin, contract IERC20 ERC20TokenPayment_, contract ICMTATSnapshot cmtat_token, contract IRuleEngine ruleEngine_, contract IAuthorizationEngine authorizationEngineIrrevocable, uint256 timeLimitToWithdraw_) public +function __IncomeVaultBase_init_unchained(contract IERC20 ERC20TokenPayment_, contract ISnapshotSource snapshotSource_, uint256 timeLimitToWithdraw_) internal ``` -@notice -initialize the proxy contract -The calls to this function will revert if the contract was deployed without a proxy +_calls the initialize functions of the policy-agnostic modules_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| admin | address | Address of the contract (Access Control) | -| ERC20TokenPayment_ | contract IERC20 | ERC20 token to perform the payment | -| cmtat_token | contract ICMTATSnapshot | | -| ruleEngine_ | contract IRuleEngine | | -| authorizationEngineIrrevocable | contract IAuthorizationEngine | | -| timeLimitToWithdraw_ | uint256 | | +| ERC20TokenPayment_ | contract IERC20 | ERC20 token used to perform the payment | +| snapshotSource_ | contract ISnapshotSource | contract implementing {ISnapshotSource}, source of the holder balances | +| timeLimitToWithdraw_ | uint256 | delay, after the dividend time, during which a claim is accepted | -### __IncomeVault_init +## IncomeVaultBaseERC2771 + +### constructor ```solidity -function __IncomeVault_init(address admin, contract IERC20 ERC20TokenPayment_, contract ICMTATSnapshot cmtat_token, contract IRuleEngine ruleEngine_, contract IAuthorizationEngine authorizationEngineIrrevocable, uint256 timeLimitToWithdraw_) internal +constructor(address forwarderIrrevocable) internal ``` -_calls the different initialize functions from the different modules_ - ### _msgSender ```solidity -function _msgSender() internal view returns (address sender) +function _msgSender() internal view virtual returns (address sender) ``` -_This surcharge is not necessary if you do not use the MetaTxModule_ +_Resolves the {ERC2771ContextUpgradeable} / {ContextUpgradeable} diamond in favour of the +ERC-2771 answer, so a forwarded call is attributed to the original sender._ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | address | the forwarded sender when the call came through the trusted forwarder | ### _msgData ```solidity -function _msgData() internal view returns (bytes) +function _msgData() internal view virtual returns (bytes) ``` -_This surcharge is not necessary if you do not use the MetaTxModule_ +_Resolves the same diamond for the calldata, stripping the appended sender suffix._ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes | The calldata with the ERC-2771 suffix removed | ### _contextSuffixLength ```solidity -function _contextSuffixLength() internal view returns (uint256) +function _contextSuffixLength() internal view virtual returns (uint256) ``` -## IncomeVaultInternal +_Resolves the same diamond for the length of that suffix._ -### CMTAT_TOKEN +#### Return Values -```solidity -contract ICMTATSnapshot CMTAT_TOKEN -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The number of trailing calldata bytes carrying the forwarded sender | -### ERC20TokenPayment +## IncomeVault + +### constructor ```solidity -contract IERC20 ERC20TokenPayment +constructor(address forwarderIrrevocable) public ``` -### claimedDividend +### initialize ```solidity -mapping(address => mapping(uint256 => bool)) claimedDividend +function initialize(address admin, contract IERC20 ERC20TokenPayment_, contract ISnapshotSource snapshotSource_, contract IRuleEngine ruleEngine_, uint256 timeLimitToWithdraw_) public ``` -### segregatedDividend +@notice +initialize the proxy contract +The calls to this function will revert if the contract was deployed without a proxy -```solidity -mapping(uint256 => uint256) segregatedDividend -``` +#### Parameters -### segregatedClaim +| Name | Type | Description | +| ---- | ---- | ----------- | +| admin | address | Address of the contract (Access Control) | +| ERC20TokenPayment_ | contract IERC20 | ERC20 token used to perform the payment | +| snapshotSource_ | contract ISnapshotSource | contract implementing {ISnapshotSource}, source of the holder balances | +| ruleEngine_ | contract IRuleEngine | optional RuleEngine applied to the payouts, or the zero address | +| timeLimitToWithdraw_ | uint256 | delay, after the dividend time, during which a claim is accepted | + +### supportsInterface ```solidity -mapping(uint256 => bool) segregatedClaim +function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) ``` -### timeLimitToWithdraw +ERC-165 interface detection -```solidity -uint256 timeLimitToWithdraw -``` +_Adds ERC-7741, whose specification requires a contract implementing it to answer `true` +for `0xa9e50872`. The ERC-7540 operator id is deliberately **not** advertised — this is not an +asynchronous vault; see {IERC7540Operator}._ -### _computeDividendBatch +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| interfaceId | bytes4 | The interface identifier to check | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | True if the interface is supported, false otherwise | + +### _msgSender ```solidity -function _computeDividendBatch(uint256 time, address[] tokenHolders, uint256[] tokenHoldersBalance, uint256 tokenTotalSupply) internal view returns (uint256[] tokenHolderDividend) +function _msgSender() internal view virtual returns (address sender) ``` -#### Parameters +_Resolves the {ERC2771ContextUpgradeable} / {ContextUpgradeable} diamond in favour of the +ERC-2771 answer, so a forwarded call is attributed to the original sender._ + +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| time | uint256 | dividend time | -| tokenHolders | address[] | addresses to compute dividend | -| tokenHoldersBalance | uint256[] | the sender balance | -| tokenTotalSupply | uint256 | the total supply | +| sender | address | the forwarded sender when the call came through the trusted forwarder | -### _computeDividend +### _msgData ```solidity -function _computeDividend(uint256 time, uint256 senderBalance, uint256 tokenTotalSupply) internal view returns (uint256 tokenHolderDividend) +function _msgData() internal view virtual returns (bytes) ``` -#### Parameters +_Resolves the same diamond for the calldata, stripping the appended sender suffix._ + +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| time | uint256 | dividend time | -| senderBalance | uint256 | token holder balance | -| tokenTotalSupply | uint256 | the total supply | +| [0] | bytes | The calldata with the ERC-2771 suffix removed | -### _transferDividend +### _contextSuffixLength ```solidity -function _transferDividend(uint256 time, address tokenHolder, uint256 tokenHolderDividend) internal +function _contextSuffixLength() internal view virtual returns (uint256) ``` -#### Parameters +_Resolves the same diamond for the length of that suffix._ + +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| time | uint256 | dividend time | -| tokenHolder | address | addresses to send the dividends | -| tokenHolderDividend | uint256 | the computed dividends | +| [0] | uint256 | The number of trailing calldata bytes carrying the forwarded sender | -## IncomeVaultInvariantStorage +### _authorizeDeposit -### INCOME_VAULT_OPERATOR_ROLE +```solidity +function _authorizeDeposit() internal view virtual +``` + +_Authorization hook invoked before a deposit. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeWithdraw ```solidity -bytes32 INCOME_VAULT_OPERATOR_ROLE +function _authorizeWithdraw() internal view virtual ``` -### INCOME_VAULT_DEPOSIT_ROLE +_Authorization hook invoked before {withdraw} and {withdrawAll}. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeDistribute ```solidity -bytes32 INCOME_VAULT_DEPOSIT_ROLE +function _authorizeDistribute() internal view virtual ``` -### INCOME_VAULT_DISTRIBUTE_ROLE +_Authorization hook invoked before {distributeDividend}. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeOperator ```solidity -bytes32 INCOME_VAULT_DISTRIBUTE_ROLE +function _authorizeOperator() internal view virtual ``` -### INCOME_VAULT_WITHDRAW_ROLE +_Authorization hook invoked before {setStatusClaim} and {setTimeLimitToWithdraw}. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeSnapshotSourceManagement ```solidity -bytes32 INCOME_VAULT_WITHDRAW_ROLE +function _authorizeSnapshotSourceManagement() internal view virtual ``` -### IncomeVault_ClaimNotActivated +_Authorization hook invoked before {setDividendSnapshotSource}. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeRuleEngineManagement ```solidity -error IncomeVault_ClaimNotActivated() +function _authorizeRuleEngineManagement() internal view virtual ``` -### IncomeVault_DividendAlreadyClaimed +_Authorization hook invoked before {setRuleEngine}. +Implemented by the deployment contract with the desired access-control policy. + +CMTAT's {ValidationModuleRuleEngine} declares a hook with this same name and parameters. +That is **not** a collision to be renamed away: both this module and CMTAT's wrapper sit on the +same {ValidationModuleRuleEngineInternal}, whose ERC-7201 slot is a hardcoded constant, so a +contract inheriting both has exactly **one** RuleEngine. One capability, therefore one hook — and +a single override answering both declarations is the correct resolution, not an accident. Giving +the two hooks different names would create two doors to one slot, each able to carry a different +policy, and the weaker one would win. See finding M-4._ + +### _authorizePause ```solidity -error IncomeVault_DividendAlreadyClaimed() +function _authorizePause() internal view virtual ``` -### IncomeVault_NoDividendToClaim +### _authorizeDeactivate ```solidity -error IncomeVault_NoDividendToClaim() +function _authorizeDeactivate() internal view virtual ``` -### IncomeVault_AdminWithAddressZeroNotAllowed +### _authorizeFreeze ```solidity -error IncomeVault_AdminWithAddressZeroNotAllowed() +function _authorizeFreeze() internal view virtual ``` -### IncomeVault_TokenPaymentWithAddressZeroNotAllowed +## IncomeVaultOwnable2Step + +### constructor ```solidity -error IncomeVault_TokenPaymentWithAddressZeroNotAllowed() +constructor(address forwarderIrrevocable) public ``` -### IncomeVault_CMTATWithAddressZeroNotAllowed +### initialize ```solidity -error IncomeVault_CMTATWithAddressZeroNotAllowed() +function initialize(address owner_, contract IERC20 ERC20TokenPayment_, contract ISnapshotSource snapshotSource_, contract IRuleEngine ruleEngine_, uint256 timeLimitToWithdraw_) public ``` -### IncomeVault_FailApproval +@notice +initialize the proxy contract +The calls to this function will revert if the contract was deployed without a proxy + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| owner_ | address | Address of the initial contract owner (ERC-173) | +| ERC20TokenPayment_ | contract IERC20 | ERC20 token used to perform the payment | +| snapshotSource_ | contract ISnapshotSource | contract implementing {ISnapshotSource}, source of the holder balances | +| ruleEngine_ | contract IRuleEngine | optional RuleEngine applied to the payouts, or the zero address | +| timeLimitToWithdraw_ | uint256 | delay, after the dividend time, during which a claim is accepted | + +### supportsInterface ```solidity -error IncomeVault_FailApproval() +function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) ``` -### IncomeVault_NoAmountSend +ERC-165 interface detection + +_Adds ERC-7741, whose specification requires a contract implementing it to answer `true` +for `0xa9e50872`._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| interfaceId | bytes4 | The interface identifier to check | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | True if the interface is supported, false otherwise | + +### _msgSender ```solidity -error IncomeVault_NoAmountSend() +function _msgSender() internal view virtual returns (address sender) ``` -### IncomeVault_NotEnoughAmount +_Resolves the {ERC2771ContextUpgradeable} / {ContextUpgradeable} diamond in favour of the +ERC-2771 answer, so a forwarded call is attributed to the original sender._ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | address | the forwarded sender when the call came through the trusted forwarder | + +### _msgData ```solidity -error IncomeVault_NotEnoughAmount() +function _msgData() internal view virtual returns (bytes) ``` -### IncomeVault_TokenBalanceIsZero +_Resolves the same diamond for the calldata, stripping the appended sender suffix._ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes | The calldata with the ERC-2771 suffix removed | + +### _contextSuffixLength ```solidity -error IncomeVault_TokenBalanceIsZero() +function _contextSuffixLength() internal view virtual returns (uint256) ``` -### IncomeVault_TooLateToWithdraw +_Resolves the same diamond for the length of that suffix._ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The number of trailing calldata bytes carrying the forwarded sender | + +### _authorizeDeposit ```solidity -error IncomeVault_TooLateToWithdraw(uint256 currentTime) +function _authorizeDeposit() internal view virtual ``` -### IncomeVault_TooEarlyToWithdraw +_Authorization hook invoked before a deposit. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeWithdraw ```solidity -error IncomeVault_TooEarlyToWithdraw(uint256 currentTime) +function _authorizeWithdraw() internal view virtual ``` -### newDeposit +_Authorization hook invoked before {withdraw} and {withdrawAll}. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeDistribute ```solidity -event newDeposit(uint256 time, address sender, uint256 dividend) +function _authorizeDistribute() internal view virtual ``` -### DividendClaimed +_Authorization hook invoked before {distributeDividend}. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeOperator ```solidity -event DividendClaimed(uint256 time, address sender, uint256 dividend) +function _authorizeOperator() internal view virtual ``` -## IncomeVaultOpen +_Authorization hook invoked before {setStatusClaim} and {setTimeLimitToWithdraw}. +Implemented by the deployment contract with the desired access-control policy._ -### TIME_ERROR_CODE +### _authorizeSnapshotSourceManagement ```solidity -enum TIME_ERROR_CODE { - OK, - CLAIM_NOT_ACTIVATED, - TOO_LATE_TO_WITHDRAW, - TOO_EARLY_TO_WITHDRAW -} +function _authorizeSnapshotSourceManagement() internal view virtual ``` -### validateTimeCode +_Authorization hook invoked before {setDividendSnapshotSource}. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeRuleEngineManagement ```solidity -function validateTimeCode(uint256 time) public view returns (enum IncomeVaultOpen.TIME_ERROR_CODE code) +function _authorizeRuleEngineManagement() internal view virtual ``` -validate if a time is valid, return 0 if valid +_Authorization hook invoked before {setRuleEngine}. +Implemented by the deployment contract with the desired access-control policy. -### validateTime +CMTAT's {ValidationModuleRuleEngine} declares a hook with this same name and parameters. +That is **not** a collision to be renamed away: both this module and CMTAT's wrapper sit on the +same {ValidationModuleRuleEngineInternal}, whose ERC-7201 slot is a hardcoded constant, so a +contract inheriting both has exactly **one** RuleEngine. One capability, therefore one hook — and +a single override answering both declarations is the correct resolution, not an accident. Giving +the two hooks different names would create two doors to one slot, each able to carry a different +policy, and the weaker one would win. See finding M-4._ + +### _authorizePause ```solidity -function validateTime(uint256 time) public view +function _authorizePause() internal view virtual ``` -validate if a time is valid, revert if invalid +### _authorizeDeactivate -### validateTimeBatch +```solidity +function _authorizeDeactivate() internal view virtual +``` + +### _authorizeFreeze ```solidity -function validateTimeBatch(uint256[] times) public view +function _authorizeFreeze() internal view virtual ``` -batch version of {validateTime} +## IERC7540Operator -### claimDividend +The operator subset of [ERC-7540](https://eips.ethereum.org/EIPS/eip-7540), verbatim. +@dev +ERC-7540 defines asynchronous ERC-4626 vaults. The {IncomeVault} is **not** one — a 4626 share +entitles whoever holds it now, while a dividend is allocated by record date — but its claim +delegation is exactly the operator mechanism that standard specifies, so the signatures are reused +rather than invented. A custodian or wallet already written against ERC-7540 operators works here unchanged. + +ERC-7540 assigns this subset the ERC-165 identifier **`0xe3bc4e65`**, described there as +"the operator methods that all ERC-7540 Vaults implement". Because this interface inherits nothing, +`type(IERC7540Operator).interfaceId` is exactly the XOR of the two selectors below and equals that +value. That equality is what pins these signatures to the standard: change either one and the id no +longer matches what ERC-7540 assigns. + +### OperatorSet ```solidity -function claimDividend(uint256 time) public +event OperatorSet(address controller, address operator, bool approved) ``` -claim your payment +The `controller` has set the `approved` status to an `operator`. + +_MUST be logged when the operator status is set._ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| time | uint256 | provide the date where you want to receive your payment | +| controller | address | the account granting or revoking | +| operator | address | the account being granted or revoked | +| approved | bool | the status that was set | -### claimDividendBatch +### setOperator ```solidity -function claimDividendBatch(uint256[] times) public +function setOperator(address operator, bool approved) external returns (bool success) ``` -batch version of {claimDividend} +Grants or revokes permissions for `operator` to manage Requests on behalf of the `msg.sender`. -_Don't check if the dividends have been already claimed before external call to CMTAT._ +_MUST set the operator status to the `approved` value, MUST log the {OperatorSet} event and +MUST return true._ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| times | uint256[] | provide the dates where you want to receive your payment | - -## IncomeVaultRestricted +| operator | address | the account to grant or revoke | +| approved | bool | true to grant, false to revoke | -### __IncomeVaultRestricted_init_unchained - -```solidity -function __IncomeVaultRestricted_init_unchained(uint256 timeLimitToWithdraw_) internal -``` +#### Return Values -_calls the different initialize functions from the different modules_ +| Name | Type | Description | +| ---- | ---- | ----------- | +| success | bool | MUST be true | -### deposit +### isOperator ```solidity -function deposit(uint256 time, uint256 amount) public +function isOperator(address controller, address operator) external view returns (bool status) ``` -deposit an amount to pay the dividends. +Returns `true` if the `operator` is approved as an operator for a `controller`. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| time | uint256 | provide the date where you want to perform a deposit | -| amount | uint256 | the amount to deposit | +| controller | address | the account that may have granted | +| operator | address | the account that may have been granted | -### withdraw +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| status | bool | true when `operator` is approved for `controller` | + +## IERC7741 + +[ERC-7741](https://eips.ethereum.org/EIPS/eip-7741) — signed operator authorisation. +@dev +Lets a holder grant or revoke an operator with an EIP-712 signature instead of a transaction, so a +custodian or relayer can submit the authorisation and pay the gas. It complements +{IERC7540Operator}, whose `setOperator` requires the holder to transact. + +The standard assigns this interface the ERC-165 identifier **`0xa9e50872`**. It inherits nothing, +so `type(IERC7741).interfaceId` is the XOR of the four selectors below and equals that value. Adding +or changing a selector here changes the id, and the vault would then advertise one the standard does +not define. + +### authorizeOperator ```solidity -function withdraw(uint256 time, uint256 amount, address withdrawAddress) public +function authorizeOperator(address controller, address operator, bool approved, bytes32 nonce, uint256 deadline, bytes signature) external returns (bool success) ``` -withdraw a certain amount at a specified time. +Grants or revokes permissions for `operator`, authorised by an EIP-712 signature. + +_MUST revert if `deadline` has passed, if the nonce was already used, or if the signature +is invalid. MUST invalidate the nonce, MUST log `OperatorSet` and MUST return true._ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| time | uint256 | provide the date where you want to perform a deposit | -| amount | uint256 | the amount to withdraw | -| withdrawAddress | address | address to receive `amount`of tokens | +| controller | address | the holder whose signature authorises the change | +| operator | address | the account being granted or revoked | +| approved | bool | true to grant, false to revoke | +| nonce | bytes32 | an unordered, single-use value chosen by the signer | +| deadline | uint256 | the timestamp after which the signature is no longer valid | +| signature | bytes | the EIP-712 signature, ECDSA or ERC-1271 | -### withdrawAll +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| success | bool | MUST be true | + +### invalidateNonce ```solidity -function withdrawAll(uint256 amount, address withdrawAddress) public +function invalidateNonce(bytes32 nonce) external ``` -withdraw all tokens from ERC20TokenPayment contracts deposited +Revokes the given `nonce` for `msg.sender`, so a signature using it can never be used. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| amount | uint256 | the amount to withdraw | -| withdrawAddress | address | address to receive `amount`of tokens | +| nonce | bytes32 | the nonce to burn | -### distributeDividend +### authorizations ```solidity -function distributeDividend(address[] addresses, uint256 time) public +function authorizations(address controller, bytes32 nonce) external view returns (bool used) ``` -distribute the dividends - -_The dividends are distributed only if they have not yet been claimed by the token holder_ +Returns whether the given `nonce` has been used for the `controller`. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| addresses | address[] | compute and transfer dividend for these holders | -| time | uint256 | dividend time | +| controller | address | the holder the nonce belongs to | +| nonce | bytes32 | the nonce to check | -### setStatusClaim +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| used | bool | true when the nonce has been spent or invalidated | + +### DOMAIN_SEPARATOR ```solidity -function setStatusClaim(uint256 time, bool status) public +function DOMAIN_SEPARATOR() external view returns (bytes32) ``` -set the status to open or close the claims for a given time +The EIP-712 domain separator of this contract. -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| time | uint256 | target time | -| status | bool | boolean (true or false) | +| [0] | bytes32 | The domain separator, unique to this contract and chain | -### setTimeLimitToWithdraw +## IIncomeVault -```solidity -function setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) public -``` +### TIME_ERROR_CODE -configure the time limit to withdraw +Why a dividend time is not claimable, or `OK` + +_Declared here rather than in the implementation because it is part of the stated API: +{validateTimeCode} returns it. Both the holder-driven claims ({IncomeVaultOpen}) and the +issuer-driven distribution ({IncomeVaultRestricted}) apply the same window through it._ + +```solidity +enum TIME_ERROR_CODE { + OK, + CLAIM_NOT_ACTIVATED, + TOO_LATE_TO_WITHDRAW, + TOO_EARLY_TO_WITHDRAW +} +``` + +### claimDividend + +```solidity +function claimDividend(uint256 time) external +``` + +Claim the caller's dividends for one distribution date + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time identifying the distribution | + +### claimDividendFor + +```solidity +function claimDividendFor(address holder, uint256 time) external +``` + +Claim `holder`'s dividends for one distribution date, as the holder or their operator + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| holder | address | the token holder the dividends are paid to | +| time | uint256 | the dividend time identifying the distribution | + +### claimDividendBatch + +```solidity +function claimDividendBatch(uint256[] times) external +``` + +Claim the caller's dividends for several distribution dates + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| times | uint256[] | the dividend times to claim | + +### claimDividendBatchFor + +```solidity +function claimDividendBatchFor(address holder, uint256[] times) external +``` + +Claim `holder`'s dividends for several dates, as the holder or their operator + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| holder | address | the token holder the dividends are paid to | +| times | uint256[] | the dividend times to claim | + +### deposit + +```solidity +function deposit(uint256 time, uint256 amount) external +``` + +Deposit the payment token for one distribution date + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time the deposit is segregated under | +| amount | uint256 | the amount of payment token to deposit | + +### depositBatch + +```solidity +function depositBatch(uint256[] times, uint256[] amounts) external +``` + +Deposit the payment token for several distribution dates in one call + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| times | uint256[] | the dividend times to deposit for | +| amounts | uint256[] | the amount to deposit for each time, index for index | + +### withdraw + +```solidity +function withdraw(uint256 time, uint256 amount, address withdrawAddress) external +``` + +Recover unclaimed payment token from one distribution date + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time to withdraw from | +| amount | uint256 | the amount of payment token to withdraw | +| withdrawAddress | address | the recipient of the withdrawn funds | + +### withdrawAll + +```solidity +function withdrawAll(uint256 amount, address withdrawAddress) external +``` + +Recover payment token held by the contract without naming a distribution date + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amount | uint256 | the amount of payment token to withdraw | +| withdrawAddress | address | the recipient of the withdrawn funds | + +### distributeDividend + +```solidity +function distributeDividend(address[] addresses, uint256 time) external +``` + +Pay several holders their dividends for one date, reverting if any payout is refused + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| addresses | address[] | the token holders to pay | +| time | uint256 | the dividend time identifying the distribution | + +### distributeDividendBestEffort + +```solidity +function distributeDividendBestEffort(address[] addresses, uint256 time) external returns (uint256 paidCount, address[] skipped) +``` + +Pay several holders for one date, skipping the refused payouts instead of reverting + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| addresses | address[] | the token holders to pay | +| time | uint256 | the dividend time identifying the distribution | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| paidCount | uint256 | how many holders were actually paid | +| skipped | address[] | the holders whose payout was refused | + +### setStatusClaim + +```solidity +function setStatusClaim(uint256 time, bool status) external +``` + +Open or close claiming for one distribution date + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | +| status | bool | true to let holders claim, false to close the period | + +### setTimeLimitToWithdraw + +```solidity +function setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) external +``` + +Set how long after a dividend time a claim is still accepted + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| timeLimitToWithdraw_ | uint256 | the length of the claim window, in seconds | + +### validateTime + +```solidity +function validateTime(uint256 time) external view +``` + +Reverts unless a claim for `time` would be accepted right now + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time to check | + +### validateTimeBatch + +```solidity +function validateTimeBatch(uint256[] times) external view +``` + +Reverts unless a claim for every one of `times` would be accepted right now + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| times | uint256[] | the dividend times to check | + +### validateTimeCode + +```solidity +function validateTimeCode(uint256 time) external view returns (enum IIncomeVault.TIME_ERROR_CODE code) +``` + +Why a claim for `time` would be refused, without reverting + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time to check | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| code | enum IIncomeVault.TIME_ERROR_CODE | the reason, or the no-error member when the claim would be accepted | + +### ERC20TokenPayment + +```solidity +function ERC20TokenPayment() external view returns (contract IERC20) +``` + +The ERC-20 the dividends are paid in + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | contract IERC20 | The payment token | + +### claimedDividend + +```solidity +function claimedDividend(address tokenHolder, uint256 time) external view returns (bool) +``` + +Whether a holder has already claimed a given distribution + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolder | address | the holder to look up | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | True once the holder has been paid for `time` | + +### segregatedDividend + +```solidity +function segregatedDividend(uint256 time) external view returns (uint256) +``` + +The total deposited for a distribution date. This is the pro-rata denominator and is +never reduced by a payout — see {unclaimedDividend} for what the period still holds. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The amount deposited for `time` | + +### segregatedClaim + +```solidity +function segregatedClaim(uint256 time) external view returns (bool) +``` + +Whether claiming is open for a distribution date + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | True when holders may claim for `time` | + +### paidDividend + +```solidity +function paidDividend(uint256 time) external view returns (uint256) +``` + +How much of a date's deposit has already been paid out + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The amount already paid for `time` | + +### unclaimedDividend + +```solidity +function unclaimedDividend(uint256 time) external view returns (uint256) +``` + +How much of a date's deposit the contract still holds + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | `segregatedDividend(time) - paidDividend(time)`, saturating at zero | + +### openClaimCount + +```solidity +function openClaimCount() external view returns (uint256) +``` + +How many distribution dates currently have claiming open + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The number of open claim periods | + +### timeLimitToWithdraw + +```solidity +function timeLimitToWithdraw() external view returns (uint256) +``` + +How long after a dividend time a claim is still accepted + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The claim window length, in seconds | + +## ISnapshotSource + +The read surface the {IncomeVault} needs from a snapshot provider — nothing more. +@dev +This is the **minimum** a contract must expose to be usable as the vault's snapshot source. It is a +strict subset of `ISnapshotState` (defined by the CMTA +[SnapshotEngine](https://github.com/CMTA/SnapshotEngine)), which declares eight functions where the +vault calls three; the five it does not call describe balances and supplies the vault never reads. + +The signatures are copied verbatim from `ISnapshotState`, so **every `ISnapshotState` +implementation already satisfies this interface** — the `SnapshotEngine`, a token embedding the +snapshot modules, or a custom provider. Solidity has no implicit conversion between unrelated +interfaces, so pass one with an explicit cast: `ISnapshotSource(address(engine))`. + +### snapshotInfo + +```solidity +function snapshotInfo(uint256 time, address tokenHolder) external view returns (uint256 tokenHolderBalance, uint256 totalSupply) +``` + +Retrieve both an account's balance and the total supply at the snapshot for a given timestamp in a single call. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | The timestamp identifying the snapshot to query. | +| tokenHolder | address | The address whose balance is being requested. | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolderBalance | uint256 | The recorded balance of the tokenHolder at the snapshot (or current balance if no snapshot). | +| totalSupply | uint256 | The recorded total supply at the snapshot (or current total supply if no snapshot). | + +### snapshotInfoBatch + +```solidity +function snapshotInfoBatch(uint256 time, address[] addresses) external view returns (uint256[] tokenHolderBalances, uint256 totalSupply) +``` + +Retrieve the balances of multiple accounts and the total supply at the snapshot for a given timestamp in a single call. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | The timestamp identifying the snapshot to query. | +| addresses | address[] | The array of addresses to query balances for. | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolderBalances | uint256[] | An array containing each address's balance at the snapshot (or current balance if no snapshot). | +| totalSupply | uint256 | The recorded total supply at the snapshot (or current total supply if no snapshot). | + +### snapshotInfoBatch + +```solidity +function snapshotInfoBatch(uint256[] times, address[] addresses) external view returns (uint256[][] tokenHolderBalances, uint256[] totalSupplies) +``` + +Retrieve balances of multiple accounts at multiple snapshots, as well as the total supply at each snapshot. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| times | uint256[] | An array of timestamps identifying each snapshot to query. | +| addresses | address[] | The array of addresses to query balances for at each snapshot. | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolderBalances | uint256[][] | A 2D array where each row corresponds to the balances of all provided addresses at a given snapshot time. | +| totalSupplies | uint256[] | An array containing the total supply at each snapshot time (or current supply if no snapshot). | + +## ERC7741Module + +### AUTHORIZE_OPERATOR_TYPEHASH + +```solidity +bytes32 AUTHORIZE_OPERATOR_TYPEHASH +``` + +EIP-712 type hash of the authorisation message, exactly as ERC-7741 defines it + +### ERC7741ModuleStorage + +```solidity +struct ERC7741ModuleStorage { + mapping(address => mapping(bytes32 => bool)) _authorizations; +} +``` + +### IncomeVault_AuthorizationExpired + +```solidity +error IncomeVault_AuthorizationExpired(uint256 deadline) +``` + +Thrown when the signature's deadline has passed. + +### IncomeVault_AuthorizationUsed + +```solidity +error IncomeVault_AuthorizationUsed(address controller, bytes32 nonce) +``` + +Thrown when the nonce was already spent or invalidated. + +### IncomeVault_InvalidAuthorization + +```solidity +error IncomeVault_InvalidAuthorization(address controller) +``` + +Thrown when the signature does not recover to `controller`. + +### IncomeVault_ControllerWithAddressZeroNotAllowed + +```solidity +error IncomeVault_ControllerWithAddressZeroNotAllowed() +``` + +Thrown when the controller is the zero address. + +### authorizeOperator + +```solidity +function authorizeOperator(address controller, address operator, bool approved, bytes32 nonce, uint256 deadline, bytes signature) public virtual returns (bool success) +``` + +Grants or revokes permissions for `operator`, authorised by an EIP-712 signature. + +_MUST revert if `deadline` has passed, if the nonce was already used, or if the signature +is invalid. MUST invalidate the nonce, MUST log `OperatorSet` and MUST return true._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| controller | address | the holder whose signature authorises the change | +| operator | address | the account being granted or revoked | +| approved | bool | true to grant, false to revoke | +| nonce | bytes32 | an unordered, single-use value chosen by the signer | +| deadline | uint256 | the timestamp after which the signature is no longer valid | +| signature | bytes | the EIP-712 signature, ECDSA or ERC-1271 | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| success | bool | MUST be true | + +### invalidateNonce + +```solidity +function invalidateNonce(bytes32 nonce) public virtual +``` + +Revokes the given `nonce` for `msg.sender`, so a signature using it can never be used. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| nonce | bytes32 | the nonce to burn | + +### authorizations + +```solidity +function authorizations(address controller, bytes32 nonce) public view virtual returns (bool used) +``` + +Returns whether the given `nonce` has been used for the `controller`. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| controller | address | the holder the nonce belongs to | +| nonce | bytes32 | the nonce to check | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| used | bool | true when the nonce has been spent or invalidated | + +### DOMAIN_SEPARATOR + +```solidity +function DOMAIN_SEPARATOR() public view virtual returns (bytes32) +``` + +The EIP-712 domain separator of this contract. + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | The domain separator, unique to this contract and chain | + +### _getERC7741ModuleStorage + +```solidity +function _getERC7741ModuleStorage() internal pure returns (struct ERC7741Module.ERC7741ModuleStorage $) +``` + +_Returns the ERC-7201 namespaced storage of this module_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| $ | struct ERC7741Module.ERC7741ModuleStorage | the storage struct | + +## IncomeVaultInternal + +### IncomeVaultInternalStorage + +```solidity +struct IncomeVaultInternalStorage { + contract IERC20 _ERC20TokenPayment; + mapping(address => mapping(uint256 => bool)) _claimedDividend; + mapping(uint256 => uint256) _segregatedDividend; + mapping(uint256 => bool) _segregatedClaim; + uint256 _timeLimitToWithdraw; + uint256 _openClaimCount; + mapping(uint256 => uint256) _paidDividend; +} +``` + +### ERC20TokenPayment + +```solidity +function ERC20TokenPayment() public view virtual returns (contract IERC20) +``` + +ERC-20 token used to pay the dividends + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | contract IERC20 | The payment token | + +### claimedDividend + +```solidity +function claimedDividend(address tokenHolder, uint256 time) public view virtual returns (bool) +``` + +Tells whether a token holder already claimed the dividends of a given time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolder | address | the address to check | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | True if the dividends were already claimed or distributed | + +### segregatedDividend + +```solidity +function segregatedDividend(uint256 time) public view virtual returns (uint256) +``` + +Total amount of payment token deposited for a given dividend time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The amount deposited, minus what was already withdrawn | + +### segregatedClaim + +```solidity +function segregatedClaim(uint256 time) public view virtual returns (bool) +``` + +Claim status of a given dividend time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | True when the token holders can claim their dividends | + +### paidDividend + +```solidity +function paidDividend(uint256 time) public view virtual returns (uint256) +``` + +Total already paid out for a dividend time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The amount of payment token already transferred to holders for `time` | + +### unclaimedDividend + +```solidity +function unclaimedDividend(uint256 time) public view virtual returns (uint256) +``` + +What is still held for a dividend time — the deposit minus what has been paid out +@dev +This is the amount an issuer can sweep with {IncomeVaultRestricted-withdraw}, and it is the bound +that function enforces. `segregatedDividend` alone is **not** that amount: it is the pro-rata +denominator and stays fixed at the deposit even after holders are paid. + +After the claim window closes it is exactly the rounding dust plus anything unclaimed. Before it +closes it still includes what the remaining holders are entitled to, so sweeping early takes +money they can no longer be paid — see the note on {IncomeVaultRestricted-withdraw}. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The amount of payment token still attributable to `time` | + +### openClaimCount + +```solidity +function openClaimCount() public view virtual returns (uint256) +``` + +How many dividend times currently have their claims open + +_Maintained exactly by {_setStatusClaim}, the only writer of the claim status. Used by +{IncomeVaultSnapshotModule-setDividendSnapshotSource}, which refuses to change the snapshot source while any +period is open._ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The number of open claim periods | + +### timeLimitToWithdraw + +```solidity +function timeLimitToWithdraw() public view virtual returns (uint256) +``` + +Delay, after the dividend time, during which a claim is still accepted + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The delay in seconds | + +### _transferDividend + +```solidity +function _transferDividend(uint256 time, address tokenHolder, uint256 tokenHolderDividend) internal virtual +``` + +Records the claim then sends the dividends to the token holder + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | dividend time | +| tokenHolder | address | addresses to send the dividends | +| tokenHolderDividend | uint256 | the computed dividends | + +### _setERC20TokenPayment + +```solidity +function _setERC20TokenPayment(contract IERC20 ERC20TokenPayment_) internal virtual +``` + +Sets the ERC-20 token used to pay the dividends + +_reverts if `ERC20TokenPayment_` is the zero address_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| ERC20TokenPayment_ | contract IERC20 | the payment token | + +### _setTimeLimitToWithdraw + +```solidity +function _setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) internal virtual +``` + +Sets the delay, after the dividend time, during which a claim is still accepted + +_reverts if `timeLimitToWithdraw_` is zero — see {IncomeVault_TimeLimitToWithdrawZeroNotAllowed}_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| timeLimitToWithdraw_ | uint256 | the delay in seconds, must be greater than zero | + +### _deposit + +```solidity +function _deposit(struct IncomeVaultInternal.IncomeVaultInternalStorage $, address sender, uint256 time, uint256 amount) internal virtual +``` + +Records a deposit against a dividend time +@dev +The single writer of `_segregatedDividend`, and the only place `newDeposit` is emitted. Both +funding paths go through it — {IncomeVaultRestricted-deposit} once, +{IncomeVaultRestricted-depositBatch} once per element — so validating, writing and announcing a +deposit cannot come apart. Each path carrying its own copy is what lets them diverge, so a new +funding path must call this rather than repeat it. + +The ERC-20 transfer is deliberately **not** here. `depositBatch` makes a single +`safeTransferFrom` for the whole batch, which is the reason it exists; folding the transfer in +would turn that back into one transfer per element. + +Takes the storage pointer rather than fetching it, as {_timeCode} does, so a batch acquires it +once instead of once per element. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| $ | struct IncomeVaultInternal.IncomeVaultInternalStorage | the ERC-7201 storage of the vault | +| sender | address | the account funding the deposit, reported by the event | +| time | uint256 | the dividend time the deposit is segregated under | +| amount | uint256 | the amount of payment token, which may not be zero | + +### _setStatusClaim + +```solidity +function _setStatusClaim(uint256 time, bool status) internal virtual +``` + +Opens or closes the claims for a dividend time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | +| status | bool | true when the token holders can claim | + +### _unclaimed + +```solidity +function _unclaimed(uint256 segregated, uint256 paid) internal pure virtual returns (uint256) +``` + +_How much of a period's deposit is still held, given the two figures that decide it. + +Saturating, not a plain subtraction. Withdrawing mid-period lowers the denominator, so a claim +made afterwards is priced against the reduced figure and can push `paid` above `segregated`. That +state means the period is over-drawn and nothing is left to sweep — this must report zero, never +revert. + +One `pure` rule because both callers must agree on it: {unclaimedDividend} reports it, and +{_transferDividend} enforces it as the bound on a payout. Were they to diverge, a payout could be +funded from another period's deposit._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| segregated | uint256 | the amount deposited for the period, the pro-rata denominator | +| paid | uint256 | the amount already paid out of that period | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The amount still attributable to the period, or zero when it is over-drawn | + +### _computeDividendBatch + +```solidity +function _computeDividendBatch(uint256 time, address[] tokenHolders, uint256[] tokenHoldersBalance, uint256 tokenTotalSupply) internal view virtual returns (uint256[] tokenHolderDividend) +``` + +Computes the dividends owed to several token holders for a given time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | dividend time | +| tokenHolders | address[] | addresses to compute dividend | +| tokenHoldersBalance | uint256[] | the sender balance | +| tokenTotalSupply | uint256 | the total supply | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolderDividend | uint256[] | the dividends owed to each address of `tokenHolders` | + +### _computeDividend + +```solidity +function _computeDividend(uint256 time, uint256 senderBalance, uint256 tokenTotalSupply) internal view virtual returns (uint256 tokenHolderDividend) +``` + +Computes the dividends owed to a single token holder for a given time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | dividend time | +| senderBalance | uint256 | token holder balance | +| tokenTotalSupply | uint256 | the total supply | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolderDividend | uint256 | the dividends owed to the token holder, rounded down | + +### _revertOnInvalidTime + +```solidity +function _revertOnInvalidTime(enum IIncomeVault.TIME_ERROR_CODE code) internal view virtual +``` + +_reverts with the error matching a non-OK {TIME_ERROR_CODE}. Exhaustive over the enum, and +fails closed on an unhandled value — see the comment on the final branch._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| code | enum IIncomeVault.TIME_ERROR_CODE | the code returned by {_timeCode} | + +### _timeCode + +```solidity +function _timeCode(struct IncomeVaultInternal.IncomeVaultInternalStorage $, uint256 time, uint256 timeLimit) internal view virtual returns (enum IIncomeVault.TIME_ERROR_CODE code) +``` + +_{validateTimeCode} with the caller supplying the storage pointer and the withdraw limit, +so a batch can read the limit once instead of once per element._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| $ | struct IncomeVaultInternal.IncomeVaultInternalStorage | the ERC-7201 storage of the vault | +| time | uint256 | the dividend time to check | +| timeLimit | uint256 | the value of `timeLimitToWithdraw` | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| code | enum IIncomeVault.TIME_ERROR_CODE | the reason the time is invalid, or `TIME_ERROR_CODE.OK` | + +### _getIncomeVaultInternalStorage + +```solidity +function _getIncomeVaultInternalStorage() internal pure returns (struct IncomeVaultInternal.IncomeVaultInternalStorage $) +``` + +_Returns the ERC-7201 namespaced storage of the IncomeVault_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| $ | struct IncomeVaultInternal.IncomeVaultInternalStorage | the storage struct | + +## IncomeVaultOperatorModule + +### OperatorStorage + +```solidity +struct OperatorStorage { + mapping(address => mapping(address => bool)) _isOperator; +} +``` + +### setOperator + +```solidity +function setOperator(address operator, bool approved) public virtual returns (bool) +``` + +Grants or revokes permissions for `operator` to manage Requests on behalf of the `msg.sender`. + +_Permissionless on purpose: a holder authorises their own operator, so there is no role to +check. The authorisation only lets the operator trigger a claim; the payout still goes to the +holder. {ERC7741Module-authorizeOperator} is the signed equivalent for a holder who cannot send +the transaction themselves._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| operator | address | the account to grant or revoke | +| approved | bool | true to grant, false to revoke | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | | + +### isOperator + +```solidity +function isOperator(address controller, address operator) public view virtual returns (bool) +``` + +Returns `true` if the `operator` is approved as an operator for a `controller`. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| controller | address | the account that may have granted | +| operator | address | the account that may have been granted | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | | + +### _setOperator + +```solidity +function _setOperator(address controller, address operator, bool approved) internal virtual +``` + +_Records an authorisation and emits the ERC-7540 event. The only writer of the mapping._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| controller | address | the holder granting or revoking the authorisation | +| operator | address | the address being authorised | +| approved | bool | true to authorise, false to revoke | + +### _requireHolderOrOperator + +```solidity +function _requireHolderOrOperator(address holder) internal view virtual +``` + +_Reverts unless the caller is `holder` or an operator `holder` authorised_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| holder | address | the token holder being claimed for | + +### _getOperatorStorage + +```solidity +function _getOperatorStorage() internal pure returns (struct IncomeVaultOperatorModule.OperatorStorage $) +``` + +_Returns the ERC-7201 namespaced storage of this module_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| $ | struct IncomeVaultOperatorModule.OperatorStorage | the storage struct | + +## IncomeVaultSnapshotCore + +### _snapshotInfo + +```solidity +function _snapshotInfo(uint256 time, address tokenHolder) internal view virtual returns (uint256 tokenHolderBalance, uint256 totalSupply) +``` + +_Balance of one holder and the total supply, at `time`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | +| tokenHolder | address | the holder to look up | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolderBalance | uint256 | the holder's recorded balance | +| totalSupply | uint256 | the recorded total supply | + +### _snapshotInfoBatch + +```solidity +function _snapshotInfoBatch(uint256 time, address[] addresses) internal view virtual returns (uint256[] tokenHolderBalances, uint256 totalSupply) +``` + +_Balances of many holders and the total supply, at one `time`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | +| addresses | address[] | the holders to look up | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolderBalances | uint256[] | one balance per address | +| totalSupply | uint256 | the recorded total supply | + +### _snapshotInfoBatch + +```solidity +function _snapshotInfoBatch(uint256[] times, address[] addresses) internal view virtual returns (uint256[][] tokenHolderBalances, uint256[] totalSupplies) +``` + +_Balances of holders across many `time`s_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| times | uint256[] | the dividend times | +| addresses | address[] | the holders to look up | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenHolderBalances | uint256[][] | one row per time | +| totalSupplies | uint256[] | one total supply per time | + +## IncomeVaultSnapshotModule + +### onlySnapshotSourceManager + +```solidity +modifier onlySnapshotSourceManager() +``` + +_Restricts the replacement of the snapshot source_ + +### SnapshotSourceStorage + +```solidity +struct SnapshotSourceStorage { + contract ISnapshotSource _source; +} +``` + +### setDividendSnapshotSource + +```solidity +function setDividendSnapshotSource(contract ISnapshotSource source) public virtual +``` + +Replace the contract the vault reads the holder balances from +@dev +Only accepted while **no claim period is open** — `openClaimCount()` must be zero. Changing the +source under an open period would silently re-price every unclaimed dividend of that period, +because the amounts are computed from the source at claim time, not fixed at deposit. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| source | contract ISnapshotSource | the new snapshot source, must implement {ISnapshotSource} and be non-zero | + +### dividendSnapshotSource + +```solidity +function dividendSnapshotSource() public view virtual returns (contract ISnapshotSource) +``` + +The contract the vault reads the holder balances from + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | contract ISnapshotSource | The configured {ISnapshotSource} | + +### _setDividendSnapshotSource + +```solidity +function _setDividendSnapshotSource(contract ISnapshotSource source) internal virtual +``` + +Sets the snapshot source used to compute the dividends + +_reverts if `source` is the zero address_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| source | contract ISnapshotSource | any contract implementing {ISnapshotSource} | + +### _snapshotInfo + +```solidity +function _snapshotInfo(uint256 time, address tokenHolder) internal view virtual returns (uint256, uint256) +``` + +_Balance of one holder and the total supply, at `time`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | +| tokenHolder | address | the holder to look up | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | | +| [1] | uint256 | | + +### _snapshotInfoBatch + +```solidity +function _snapshotInfoBatch(uint256 time, address[] addresses) internal view virtual returns (uint256[], uint256) +``` + +_Balances of many holders and the total supply, at one `time`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | +| addresses | address[] | the holders to look up | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256[] | | +| [1] | uint256 | | + +### _snapshotInfoBatch + +```solidity +function _snapshotInfoBatch(uint256[] times, address[] addresses) internal view virtual returns (uint256[][], uint256[]) +``` + +_Balances of holders across many `time`s_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| times | uint256[] | the dividend times | +| addresses | address[] | the holders to look up | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256[][] | | +| [1] | uint256[] | | + +### _authorizeSnapshotSourceManagement + +```solidity +function _authorizeSnapshotSourceManagement() internal view virtual +``` + +_Authorization hook invoked before {setDividendSnapshotSource}. +Implemented by the deployment contract with the desired access-control policy._ + +### _getSnapshotSourceStorage + +```solidity +function _getSnapshotSourceStorage() internal pure returns (struct IncomeVaultSnapshotModule.SnapshotSourceStorage $) +``` + +_Returns the ERC-7201 namespaced storage of this module_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| $ | struct IncomeVaultSnapshotModule.SnapshotSourceStorage | the storage struct | + +## IncomeVaultValidationCore + +### _validateTransfer + +```solidity +function _validateTransfer(address from, address to, uint256 value) internal view virtual +``` + +_Reverts if the vault may not pay `value` to `to`. Implemented by the deployment — or by the +host contract, when the dividend logic is embedded in one._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| from | address | the address sending the payment, always the vault itself | +| to | address | the token holder receiving the dividends | +| value | uint256 | the amount of payment token | + +## IncomeVaultValidationModule + +### TEXT_TRANSFER_OK + +```solidity +string TEXT_TRANSFER_OK +``` + +_Human-readable answers for {messageForTransferRestriction}. The strings are CMTAT's +(`ValidationModuleERC1404`) verbatim, so an operator console written against a CMTAT reads a +payout refusal exactly as it reads a transfer refusal. The codes are CMTAT's +`REJECTED_CODE_BASE`, for the same reason._ + +### TEXT_UNKNOWN_CODE + +```solidity +string TEXT_UNKNOWN_CODE +``` + +_Returned when no configured source claims the code_ + +### TEXT_TRANSFER_REJECTED_PAUSED + +```solidity +string TEXT_TRANSFER_REJECTED_PAUSED +``` + +_The vault is paused_ + +### TEXT_TRANSFER_REJECTED_DEACTIVATED + +```solidity +string TEXT_TRANSFER_REJECTED_DEACTIVATED +``` + +_The vault has been permanently deactivated_ + +### TEXT_TRANSFER_REJECTED_FROM_FROZEN + +```solidity +string TEXT_TRANSFER_REJECTED_FROM_FROZEN +``` + +_The paying address is frozen_ + +### TEXT_TRANSFER_REJECTED_TO_FROZEN + +```solidity +string TEXT_TRANSFER_REJECTED_TO_FROZEN +``` + +_The receiving token holder is frozen_ + +### onlyRuleEngineManager + +```solidity +modifier onlyRuleEngineManager() +``` + +_Restricts the management of the RuleEngine_ + +### __IncomeVaultValidation_init_unchained + +```solidity +function __IncomeVaultValidation_init_unchained(contract IRuleEngine ruleEngine_) internal +``` + +Initializes the validation module + +_Writes the RuleEngine slot that CMTAT's {ValidationModuleRuleEngineInternal} owns, at its +hardcoded ERC-7201 location. In the standalone vault that slot belongs to this contract alone. In +a host that also inherits a CMTAT validation stack it is **shared**, so a non-zero `ruleEngine_` +here would replace the *token's* compliance engine from the dividend initializer. Such a host must +pass the zero address, which CMTAT's initializer treats as a no-op, and keep the engine the token +already configured. Embedding the payout logic via {IncomeVaultValidationCore} instead avoids the +question entirely, and is the supported route. Finding M-4._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| ruleEngine_ | contract IRuleEngine | the RuleEngine applied to the payouts, or the zero address for none | + +### setRuleEngine + +```solidity +function setRuleEngine(contract IRuleEngine ruleEngine_) public virtual +``` + +Updates the RuleEngine applied to the dividend payouts. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| ruleEngine_ | contract IRuleEngine | the new RuleEngine, or the zero address to disable the rule checks | + +### canTransfer + +```solidity +function canTransfer(address from, address to, uint256 value) public view virtual returns (bool) +``` + +Returns true if the vault is allowed to pay `value` to `to`. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| from | address | the address sending the payment, always the vault itself | +| to | address | the token holder receiving the dividends | +| value | uint256 | the amount of payment token | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | True if the pause, freeze and RuleEngine checks all allow the payout | + +### detectTransferRestriction + +```solidity +function detectTransferRestriction(address from, address to, uint256 value) public view virtual returns (uint8) +``` + +ERC-1404 restriction code for a payout from the vault, or `0` when it would be accepted. + +_Answers for the **whole** payout decision, in the same order {canTransfer} evaluates it: +deactivation, pause, either party frozen, then the RuleEngine. The codes are CMTAT's +`REJECTED_CODE_BASE`, so a caller written against a CMTAT reads them unchanged. + +This returns `0` exactly when {canTransfer} returns true, and the two must not be allowed to +drift apart: consulting only the RuleEngine here would report a paused vault or a frozen holder as +unrestricted, and the claim would then revert._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| from | address | the address sending the payment, always the vault itself | +| to | address | the token holder receiving the dividends | +| value | uint256 | the amount of payment token | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint8 | The ERC-1404 restriction code, `0` when the rules allow the payout | + +### messageForTransferRestriction + +```solidity +function messageForTransferRestriction(uint8 restrictionCode) public view virtual returns (string) +``` + +Human readable message matching a code returned by {detectTransferRestriction}. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| restrictionCode | uint8 | the ERC-1404 restriction code to translate | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | The message associated with `restrictionCode` | + +### _authorizeRuleEngineManagement + +```solidity +function _authorizeRuleEngineManagement() internal view virtual +``` + +_Authorization hook invoked before {setRuleEngine}. +Implemented by the deployment contract with the desired access-control policy. + +CMTAT's {ValidationModuleRuleEngine} declares a hook with this same name and parameters. +That is **not** a collision to be renamed away: both this module and CMTAT's wrapper sit on the +same {ValidationModuleRuleEngineInternal}, whose ERC-7201 slot is a hardcoded constant, so a +contract inheriting both has exactly **one** RuleEngine. One capability, therefore one hook — and +a single override answering both declarations is the correct resolution, not an accident. Giving +the two hooks different names would create two doors to one slot, each able to carry a different +policy, and the weaker one would win. See finding M-4._ + +### _validateTransfer + +```solidity +function _validateTransfer(address from, address to, uint256 value) internal view virtual +``` + +_The standalone vault's answer: its own pause state, the frozen status of both parties, and +the RuleEngine if one is configured._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| from | address | the address sending the payment, always the vault itself | +| to | address | the token holder receiving the dividends | +| value | uint256 | the amount of payment token | + +## Ownable2StepERC165Module + +### IERC173_INTERFACE_ID + +```solidity +bytes4 IERC173_INTERFACE_ID +``` + +ERC-165 interface ID of ERC-173 (contract ownership standard) + +_bytes4(keccak256("owner()")) ^ bytes4(keccak256("transferOwnership(address)"))_ + +### IOWNABLE2STEP_INTERFACE_ID + +```solidity +bytes4 IOWNABLE2STEP_INTERFACE_ID +``` + +ERC-165 interface ID of the Ownable2Step-specific functions + +_bytes4(keccak256("acceptOwnership()")) ^ bytes4(keccak256("pendingOwner()"))_ + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) +``` + +ERC-165 interface detection + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| interfaceId | bytes4 | The interface identifier to check | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | True if the interface is supported, false otherwise | + +## VersionModule + +Exposes the IncomeVault release version through the ERC-3643 version interface. +@dev +Same shape as the CMTAT, RuleEngine and SnapshotEngine version modules: a single compile-time +constant read through {IERC3643Version-version}. Bump `VERSION` together with the `CHANGELOG.md` +entry of the release. + +### version + +```solidity +function version() public view virtual returns (string version_) +``` + +Returns the current version of the token contract. + +_This value is useful to know which smart contract version has been used_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| version_ | string | A string representing the version of the token implementation (e.g., "1.0.0"). | + +## IncomeVaultOpen + +### claimDividend + +```solidity +function claimDividend(uint256 time) public virtual +``` + +claim your payment + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | provide the date where you want to receive your payment | + +### claimDividendFor + +```solidity +function claimDividendFor(address holder, uint256 time) public virtual +``` + +Claim on behalf of a token holder +@dev +Callable by the holder, or by an address the holder authorised through {setOperator}. The +dividends always go to **the holder** — an operator pays the gas and chooses the moment, it can +never redirect the payment. Every other rule is unchanged: the claim window, the +already-claimed check and the transfer restrictions all apply exactly as for {claimDividend}. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| holder | address | the token holder to claim for | +| time | uint256 | provide the date of the payment | + +### claimDividendBatchFor + +```solidity +function claimDividendBatchFor(address holder, uint256[] times) public virtual +``` + +Batch version of {claimDividendFor} + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| holder | address | the token holder to claim for | +| times | uint256[] | provide the dates of the payments | + +### claimDividendBatch + +```solidity +function claimDividendBatch(uint256[] times) public virtual +``` + +batch version of {claimDividend} + +_Don't check if the dividends have been already claimed before external call to the snapshot source._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| times | uint256[] | provide the dates where you want to receive your payment | + +### validateTimeCode + +```solidity +function validateTimeCode(uint256 time) public view virtual returns (enum IIncomeVault.TIME_ERROR_CODE code) +``` + +validate if a time is valid, return 0 if valid + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time to check | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| code | enum IIncomeVault.TIME_ERROR_CODE | the reason the time is invalid, or `TIME_ERROR_CODE.OK` | + +### validateTime + +```solidity +function validateTime(uint256 time) public view virtual +``` + +validate if a time is valid, revert if invalid + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time to check | + +### validateTimeBatch + +```solidity +function validateTimeBatch(uint256[] times) public view virtual +``` + +batch version of {validateTime} + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| times | uint256[] | the dividend times to check | + +### _claimDividend + +```solidity +function _claimDividend(address sender, uint256 time) internal virtual +``` + +_{claimDividend} for an explicit holder_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | address | the token holder being paid | +| time | uint256 | the dividend time | + +### _claimDividendBatch + +```solidity +function _claimDividendBatch(address sender, uint256[] times) internal virtual +``` + +_{claimDividendBatch} for an explicit holder_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | address | the token holder being paid | +| times | uint256[] | the dividend times | + +## IncomeVaultRestricted + +### onlyDepositManager + +```solidity +modifier onlyDepositManager() +``` + +_Restricts the deposit of dividends_ + +### onlyWithdrawManager + +```solidity +modifier onlyWithdrawManager() +``` + +_Restricts the withdrawal of the deposited funds_ + +### onlyDistributeManager + +```solidity +modifier onlyDistributeManager() +``` + +_Restricts the issuer-driven distribution of the dividends_ + +### onlyVaultOperator + +```solidity +modifier onlyVaultOperator() +``` + +_Restricts the configuration of the claim window_ + +### __IncomeVaultRestricted_init_unchained + +```solidity +function __IncomeVaultRestricted_init_unchained(uint256 timeLimitToWithdraw_) internal +``` + +_calls the different initialize functions from the different modules_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| timeLimitToWithdraw_ | uint256 | delay, after the dividend time, during which a claim is accepted | + +### deposit + +```solidity +function deposit(uint256 time, uint256 amount) public virtual +``` + +deposit an amount to pay the dividends. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | provide the date where you want to perform a deposit | +| amount | uint256 | the amount to deposit | + +### depositBatch + +```solidity +function depositBatch(uint256[] times, uint256[] amounts) public virtual +``` + +Deposit for several dividend times in one transaction +@dev +Equivalent to calling {deposit} once per entry — same accounting, same `newDeposit` event per +entry — but the payment token is pulled **once** for the total instead of once per time. That is +the reason the function exists; the common case is an issuer opening a year of coupon periods. + +Repeating a `time` is allowed and accumulates, exactly as separate calls would. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| times | uint256[] | the dividend times to deposit for | +| amounts | uint256[] | the amount to deposit for each time, must be the same length and each non-zero | + +### withdraw + +```solidity +function withdraw(uint256 time, uint256 amount, address withdrawAddress) public virtual +``` + +withdraw a certain amount at a specified time. +@dev +Bounded by {unclaimedDividend}, so a sweep can never reach funds deposited for another dividend +time. Intended for after the claim window closes, when what remains is rounding dust and +unclaimed shares. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | provide the date where you want to perform a deposit | +| amount | uint256 | the amount to withdraw | +| withdrawAddress | address | address to receive `amount`of tokens | + +### withdrawAll + +```solidity +function withdrawAll(uint256 amount, address withdrawAddress) public virtual +``` + +withdraw all tokens from ERC20TokenPayment contracts deposited + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amount | uint256 | the amount to withdraw | +| withdrawAddress | address | address to receive `amount`of tokens | + +### distributeDividend + +```solidity +function distributeDividend(address[] addresses, uint256 time) public virtual +``` + +distribute the dividends + +_The dividends are distributed only if they have not yet been claimed by the token holder. +Subject to the same claim window **and** the same transfer restrictions as +{IncomeVaultOpen-claimDividend}: a holder the pause, freeze or RuleEngine refuses cannot be paid +by the issuer either, and one blocked holder reverts the whole distribution._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| addresses | address[] | compute and transfer dividend for these holders | +| time | uint256 | dividend time | + +### distributeDividendBestEffort + +```solidity +function distributeDividendBestEffort(address[] addresses, uint256 time) public virtual returns (uint256 paidCount, address[] skipped) +``` + +Distribute the dividends, skipping any holder whose payout is refused +@dev +Same computation as {distributeDividend}, but a holder the ValidationModule or the payment token +refuses is **skipped** instead of reverting the whole call. Use it when one non-compliant address +must not block a large payout run; use {distributeDividend} when the distribution should be +all-or-nothing. + +Each payout is attempted through an external self-call so it can be wrapped in `try`/`catch`, +which gives **per-holder atomicity**: a holder is either fully paid — marked claimed *and* +transferred — or left completely untouched and still able to claim later. A partial state where +a holder is marked as claimed without receiving the tokens is not reachable. + +Every skip emits {DividendDistributionSkipped} carrying the raw revert data, so the cause can be +decoded off-chain, and the skipped holders are returned for the caller to act on directly. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| addresses | address[] | compute and transfer dividend for these holders | +| time | uint256 | dividend time | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| paidCount | uint256 | how many holders were paid | +| skipped | address[] | the holders that were not paid, trimmed to `paidCount` subtracted from the input | + +### transferDividendSelf + +```solidity +function transferDividendSelf(uint256 time, address tokenHolder, uint256 tokenHolderDividend) public virtual +``` + +Validate and pay one dividend — callable **only by the vault itself** +@dev +This exists solely so {distributeDividendBestEffort} can wrap a payout in `try`/`catch`, which +requires an external call. It carries no access control of its own beyond the self-call check, +so that check is what stands between it and an unauthorized payout: reverts +{IncomeVault_OnlySelfCall} for every caller other than `address(this)`. + +`msg.sender` is used deliberately rather than `_msgSender()`. The check must identify the real +caller; an ERC-2771 forwarder must never be able to present itself as the vault. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | dividend time | +| tokenHolder | address | the holder to pay | +| tokenHolderDividend | uint256 | the amount to pay | + +### setStatusClaim + +```solidity +function setStatusClaim(uint256 time, bool status) public virtual +``` + +set the status to open or close the claims for a given time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | target time | +| status | bool | boolean (true or false) | + +### setTimeLimitToWithdraw + +```solidity +function setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) public virtual +``` + +configure the time limit to withdraw + +_reverts if `timeLimitToWithdraw_` is zero: that would leave a one-second claim window_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| timeLimitToWithdraw_ | uint256 | delay, after the dividend time, during which a claim is accepted, must be greater than zero | + +### _authorizeDeposit + +```solidity +function _authorizeDeposit() internal view virtual +``` + +_Authorization hook invoked before a deposit. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeWithdraw + +```solidity +function _authorizeWithdraw() internal view virtual +``` + +_Authorization hook invoked before {withdraw} and {withdrawAll}. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeDistribute + +```solidity +function _authorizeDistribute() internal view virtual +``` + +_Authorization hook invoked before {distributeDividend}. +Implemented by the deployment contract with the desired access-control policy._ + +### _authorizeOperator + +```solidity +function _authorizeOperator() internal view virtual +``` + +_Authorization hook invoked before {setStatusClaim} and {setTimeLimitToWithdraw}. +Implemented by the deployment contract with the desired access-control policy._ + +## IncomeVaultInvariantStorage + +### newDeposit + +```solidity +event newDeposit(uint256 time, address sender, uint256 dividend) +``` + +Emitted when an authorized address deposits dividends for a given time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time the deposit is attached to | +| sender | address | the address performing the deposit | +| dividend | uint256 | the amount of payment token deposited | + +### DividendClaimed + +```solidity +event DividendClaimed(uint256 time, address sender, uint256 dividend) +``` + +Emitted when the dividends of a token holder are claimed or distributed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | +| sender | address | the token holder receiving the dividends | +| dividend | uint256 | the amount of payment token transferred | + +### ERC20TokenPaymentSet + +```solidity +event ERC20TokenPaymentSet(contract IERC20 newERC20TokenPayment) +``` + +Emitted when the ERC-20 used to pay the dividends is set + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newERC20TokenPayment | contract IERC20 | the payment token | + +### ClaimStatusSet + +```solidity +event ClaimStatusSet(uint256 time, bool status) +``` + +Emitted when the claims are opened or closed for a dividend time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | +| status | bool | true when the token holders can claim | + +### TimeLimitToWithdrawSet + +```solidity +event TimeLimitToWithdrawSet(uint256 timeLimitToWithdraw) +``` + +Emitted when the delay during which a claim is accepted is set + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| timeLimitToWithdraw | uint256 | the delay in seconds | + +### Withdraw + +```solidity +event Withdraw(uint256 time, address withdrawAddress, uint256 amount) +``` + +Emitted when an authorized address withdraws the funds deposited for a dividend time + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time the funds were deposited for | +| withdrawAddress | address | the address receiving the funds | +| amount | uint256 | the amount of payment token withdrawn | + +### DividendDistributionSkipped + +```solidity +event DividendDistributionSkipped(uint256 time, address tokenHolder, bytes reason) +``` + +Emitted when a best-effort distribution skips a token holder + +_Reported by {IncomeVaultRestricted-distributeDividendBestEffort}. The holder is left +completely untouched — not marked as claimed — and can still claim later, or be included in a +subsequent distribution._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| time | uint256 | the dividend time | +| tokenHolder | address | the holder who was not paid | +| reason | bytes | the raw revert data of the failed payout, so the cause can be decoded off-chain | + +### WithdrawAll + +```solidity +event WithdrawAll(address withdrawAddress, uint256 amount) +``` + +Emitted when an authorized address withdraws funds without a dividend time + +_the per-time accounting in `segregatedDividend` is left untouched, see {withdrawAll}_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| withdrawAddress | address | the address receiving the funds | +| amount | uint256 | the amount of payment token withdrawn | + +### DividendSnapshotSourceSet + +```solidity +event DividendSnapshotSourceSet(contract ISnapshotSource newSource) +``` + +Emitted when the snapshot source used to compute the dividends is set. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newSource | contract ISnapshotSource | The contract queried for historical balances and total supply. | + +### IncomeVault_ClaimNotActivated + +```solidity +error IncomeVault_ClaimNotActivated() +``` + +### IncomeVault_DividendAlreadyClaimed + +```solidity +error IncomeVault_DividendAlreadyClaimed() +``` + +### IncomeVault_NoDividendToClaim + +```solidity +error IncomeVault_NoDividendToClaim() +``` + +### IncomeVault_AdminWithAddressZeroNotAllowed + +```solidity +error IncomeVault_AdminWithAddressZeroNotAllowed() +``` + +### IncomeVault_TokenPaymentWithAddressZeroNotAllowed + +```solidity +error IncomeVault_TokenPaymentWithAddressZeroNotAllowed() +``` + +### IncomeVault_SnapshotSourceWithAddressZeroNotAllowed + +```solidity +error IncomeVault_SnapshotSourceWithAddressZeroNotAllowed() +``` + +### IncomeVault_TimeLimitToWithdrawZeroNotAllowed + +```solidity +error IncomeVault_TimeLimitToWithdrawZeroNotAllowed() +``` + +Thrown when the withdraw time limit is set to zero. + +_A limit of zero collapses the claim window `[time, time + limit]` to the single instant +`block.timestamp == time`, making the period effectively unclaimable._ + +### IncomeVault_ClaimPeriodOpen + +```solidity +error IncomeVault_ClaimPeriodOpen(uint256 openClaimCount) +``` + +Thrown when the snapshot source is changed while at least one claim period is open. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| openClaimCount | uint256 | how many dividend times currently have their claims open | + +### IncomeVault_OnlySelfCall + +```solidity +error IncomeVault_OnlySelfCall() +``` + +Thrown when {IncomeVaultRestricted-transferDividendSelf} is called by anyone but the vault. + +_That function exists only so the best-effort distribution can wrap a payout in try/catch, +which requires an external call. It must never be reachable from outside._ + +### IncomeVault_InvalidLengths + +```solidity +error IncomeVault_InvalidLengths(uint256 timesLength, uint256 amountsLength) +``` + +Thrown when {IncomeVaultRestricted-depositBatch} is given arrays of different lengths. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| timesLength | uint256 | the number of dividend times supplied | +| amountsLength | uint256 | the number of amounts supplied | + +### IncomeVault_UnauthorizedOperator + +```solidity +error IncomeVault_UnauthorizedOperator(address holder, address caller) +``` + +Thrown when a caller claims for a holder without being that holder or their operator. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| holder | address | the token holder whose dividends were targeted | +| caller | address | the address that attempted the claim | + +### IncomeVault_NoAmountSend + +```solidity +error IncomeVault_NoAmountSend() +``` + +### IncomeVault_NotEnoughAmount + +```solidity +error IncomeVault_NotEnoughAmount() +``` + +### IncomeVault_TokenBalanceIsZero + +```solidity +error IncomeVault_TokenBalanceIsZero() +``` + +### IncomeVault_TooLateToWithdraw + +```solidity +error IncomeVault_TooLateToWithdraw(uint256 currentTime) +``` + +### IncomeVault_TooEarlyToWithdraw + +```solidity +error IncomeVault_TooEarlyToWithdraw(uint256 currentTime) +``` + +### IncomeVault_InvalidTransfer + +```solidity +error IncomeVault_InvalidTransfer(address from, address to, uint256 value) +``` + +Thrown when the ValidationModule (pause, freeze or RuleEngine) forbids the payout. + +### IncomeVault_SameValue + +```solidity +error IncomeVault_SameValue() +``` + +## IncomeVaultRolesStorage + +### INCOME_VAULT_OPERATOR_ROLE + +```solidity +bytes32 INCOME_VAULT_OPERATOR_ROLE +``` + +Role allowed to open/close the claims and to configure the withdraw time limit + +### INCOME_VAULT_DEPOSIT_ROLE + +```solidity +bytes32 INCOME_VAULT_DEPOSIT_ROLE +``` + +Role allowed to deposit the payment token in the vault + +### INCOME_VAULT_DISTRIBUTE_ROLE + +```solidity +bytes32 INCOME_VAULT_DISTRIBUTE_ROLE +``` + +Role allowed to push the dividends to a list of token holders + +### INCOME_VAULT_WITHDRAW_ROLE + +```solidity +bytes32 INCOME_VAULT_WITHDRAW_ROLE +``` + +Role allowed to withdraw the payment token from the vault diff --git a/doc/specification.md b/doc/specification.md deleted file mode 100644 index d374e2c..0000000 --- a/doc/specification.md +++ /dev/null @@ -1,200 +0,0 @@ -# Specification - -[TOC] - -## Introduction - - \0. On the CMTAT, the admin registers the dividend `time` to perform a snapshot and store the holder’s balance at this specified time. - -1. An authorized address perform a deposit in the `IncomeVault` for a specific `time` -2. An authorized address open the claim for this specific `time` -3. Holder claims his dividends by calling the function `claimDividend` - -![IncomeVault-Global.drawio](../doc/schema/drawio/IncomeVault-Global.drawio.png) - -## Access control - -All restricted functions are defined in the file `IncomeVaultRestricted`. - -| Role | Function | -| -------------------------- | ------------------------------------------------------------ | -| DEFAULT_ADMIN_ROLE | Manage all others roles

This role has also all the others roles by default through the `ValidationModule` | -| INCOME_VAULT_DEPOSIT_ROLE | `deposit` | -| INCOME_VAULT_WITHDRAW_ROLE | `withdraw`
`withdrawAll` | -| INCOME_VAULT_OPERATOR_ROLE | `setStatusClaim`
`setTimeLimitToWithdraw` | - - - -## Segregated Deposit - -Each deposit is segregated in its time value. A `time` is the dividends distribution date (Unix Timestamp) to the token holders. - -![IncomeVault-Segragated Deposit.drawio](../doc/schema/drawio/IncomeVault-Segragated Deposit.drawio.png) - -## ValidationModule - -A claim is considered as a transfer from the contract to the sender (token holder). -This transfer can be restricted with the ValidationModule - -This module is imported from the CMTAT which allows to : - -- Freeze/unfreeze an address -- Put the contract in the pause state -- Call the ruleEngine for additional rules - -If the ValidationModule refuses the transfer, the function is reverted. - - - -### RuleEngine - -As for the CMTAT, there is the possibility to configure a ruleEngine with rules to perform transfer rectriction/verification. As relevant rules, we have: - -- Whitelist -- Blacklist -- Sanctionlist -- ConditionalTransfer - - - - - -## Operation - -### Claim dividends - -The distribution of dividends is not automatic. A token holder has to claim his dividends by calling the function `claimDividend`, similar to the Lido protocol. When he claims his dividends, he precises the defined `time`. - -Therefore, a token holder has to know the different `time` when a deposit has been performed. - - - -A function `claimDividend` in batch is also available to claim dividends for several different time. - -### Claim restriction - -An holder can not claim its dividends if: - -a. The claim time is in the future (`IncomeVault_TooEarlyToWithdraw`) - -b. The claim time is too far in the past, specified by `timeLimitToWithdraw` (`IncomeVault_TooLateToWithdraw`) - -c. Claim is not enabled for this specific `time` (`IncomeVault_ClaimNotActivated`) - -d. Holder has already claim its dividends (`IncomeVault_DividendAlreadyClaimed`) - -e. There is no dividend to claim (`IncomeVault_NoDividendToClaim`) - -For the batch function, `claimDividendBatch`, `d` and `e` don't generate an error but instead, there is just no dividends distributed for this specific time. - -### Schema - -This schema describes the different smart contracts called when a token holder claims his dividends. - -![IncomeVault-RuleEngine.drawio](../doc/schema/drawio/IncomeVault-RuleEngine.drawio.png) - -#### Formula - -The computation of dividends is performing according to the following formula - -``` -senderDividend = (senderCMTATBalance * dividendTotalSupply) / TokenTotalSupply; -``` - -The sender dividend will be rounded to the inferior integer. Thus, the issuer should put a “limit” date to claim his dividend in order to withdraw the staying funds (due to rounding) from the smart contract. - -Example with USDC (6 decimal) and a CMTAT (0 decimal) - -tokenSupply CMTAT = 12’351 - -The sender has 4221 tokens. - -21’555.50 $ in USDC are deposited corresponding to a value of 21555500000 tokens since USDC has 6 decimals. - - We have: - -senderDividend = 4221 * 21555500000 / 12351 = 7366671969.880981297 = 7366671969 which correspond to **7366.671969**$ - -#### Schema - -Schema without the `ValidationModule` (see next paragraph) - -![IncomeVault-claimDividend.drawio](../doc/schema/drawio/IncomeVault-claimDividend.drawio.png) - - - -## Withdraw funds - -An authorized user can call the following functions to withdraw funds from the vault: - -``` -1. withdraw(uint256 time, uint256 amount, address withdrawAddress) public onlyRole(DEBT_VAULT_WITHDRAW_ROLE) -``` - -and - -``` -2. withdrawAll(uint256 amount, address withdrawAddress) public onlyRole(DEBT_VAULT_WITHDRAW_ROLE) -``` - -With the function 1, the funds are withdrawn only from the specific time. - -The second function allows to withdraw funds without a specific time, which can lead to an “unstable” state with the different pool of dividend. To be used only in case of emergency or if the vault is closed. - - - -## Distribute dividend - -An authorized user can also decide to distribute the dividend for a given time and a given list of addresses. - -In this situation, the token holder can not decide if he wants to receive his dividends (he is forced to accept) and can not choose the address where he wants to receive his dividends. - - - -Since the function is restricted by access control, it is not possible to use Chainlink Automation to perform an automatic call and distribute the dividends. -Moreover, the list of token holders has to be provided by the transaction’s sender. - -## Improvement - -- An automatic distribution of dividend could be performed through [Chainlink Automation](https://docs.chain.link/chainlink-automation) but it requires several changes to allow that. -- Only ERC20 tokens are supported. We could extends this to support direct native (e.g ether) too. - -## Deployment - -The contract has to be deployed with a transparent proxy and the contract is compatible with the standard [ERC-2771](https://eips.ethereum.org/EIPS/eip-2771) for meta transactions. - - - -## Threat model & FAQ - -### Claim dividend several times - -> What if a holder tries to claim the same dividend several times? - -When a holder claims his dividends for a specific time, a boolean is set to true to indicate the claiming dividend. - -``` - claimedDividend[tokenHolder][time] = true; -``` - -This boolean is set inside the internal function `_transferDividend` - -Moreover, the functions to claim are protected against reentrancy attacks with the modifier `nonReentrant` from OpenZeppelin. - -### New dividend after claim - -> What happens if the authorized address deposit dividend after that a token holder has already claimed his dividends ? - -A token holder can not claim his dividends if the claim status is not opened. Moreover, you can not deposit new dividends if the status is on open (=true). - -The function `setStatusClaim` allows to open (true) or close(false) the claims for a specific time. - -If you close the claim (claim status = false) and deposit new dividends, the previous token holders will be penalized since the dividends total supply for this specific time has improved for all token holders which have not already claimed their dividends, - -In summary, when you have opened the claim, you should not deposit new dividends in the vault for a specific time. - -### Transfer fails - -> What happens if the token transfer fails? - -In this case, the whole transaction is reverted, and the smart contract still considers that dividends have not been claimed by the token holder (sender). diff --git a/doc/specification/.~lock.Cover_incomevault_specificationv2.0.0-rc0.odg# b/doc/specification/.~lock.Cover_incomevault_specificationv2.0.0-rc0.odg# new file mode 100644 index 0000000..d116b2a --- /dev/null +++ b/doc/specification/.~lock.Cover_incomevault_specificationv2.0.0-rc0.odg# @@ -0,0 +1 @@ +,ryan,lau-rsa-lp,31.08.2026 13:57,file:///home/ryan/.config/libreoffice/4; \ No newline at end of file diff --git a/doc/specification/Cover_incomevault_specification_overviewv2.0.0-rc0.odg b/doc/specification/Cover_incomevault_specification_overviewv2.0.0-rc0.odg new file mode 100644 index 0000000..bcfa1e8 Binary files /dev/null and b/doc/specification/Cover_incomevault_specification_overviewv2.0.0-rc0.odg differ diff --git a/doc/specification/Cover_incomevault_specification_overviewv2.0.0-rc0.pdf b/doc/specification/Cover_incomevault_specification_overviewv2.0.0-rc0.pdf new file mode 100644 index 0000000..fd0e563 Binary files /dev/null and b/doc/specification/Cover_incomevault_specification_overviewv2.0.0-rc0.pdf differ diff --git a/doc/specification/Cover_incomevault_specificationv2.0.0-rc0.odg b/doc/specification/Cover_incomevault_specificationv2.0.0-rc0.odg new file mode 100644 index 0000000..69fd443 Binary files /dev/null and b/doc/specification/Cover_incomevault_specificationv2.0.0-rc0.odg differ diff --git a/doc/specification/Cover_incomevault_specificationv2.0.0-rc0.pdf b/doc/specification/Cover_incomevault_specificationv2.0.0-rc0.pdf new file mode 100644 index 0000000..10ca25e Binary files /dev/null and b/doc/specification/Cover_incomevault_specificationv2.0.0-rc0.pdf differ diff --git a/doc/surya/surya_graph/surya_graph_ERC7741Module.sol.png b/doc/surya/surya_graph/surya_graph_ERC7741Module.sol.png new file mode 100644 index 0000000..3a117ea Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_ERC7741Module.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IERC7540Operator.sol.png b/doc/surya/surya_graph/surya_graph_IERC7540Operator.sol.png new file mode 100644 index 0000000..95404dc Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IERC7540Operator.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IERC7741.sol.png b/doc/surya/surya_graph/surya_graph_IERC7741.sol.png new file mode 100644 index 0000000..4297a0a Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IERC7741.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IIncomeVault.sol.png b/doc/surya/surya_graph/surya_graph_IIncomeVault.sol.png new file mode 100644 index 0000000..007cbaa Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IIncomeVault.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_ISnapshotSource.sol.png b/doc/surya/surya_graph/surya_graph_ISnapshotSource.sol.png new file mode 100644 index 0000000..5566e05 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_ISnapshotSource.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVault.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVault.sol.png index ba05fa1..38c3167 100644 Binary files a/doc/surya/surya_graph/surya_graph_IncomeVault.sol.png and b/doc/surya/surya_graph/surya_graph_IncomeVault.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultBase.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultBase.sol.png new file mode 100644 index 0000000..e73d42a Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IncomeVaultBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultBaseERC2771.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultBaseERC2771.sol.png new file mode 100644 index 0000000..b0bef3d Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IncomeVaultBaseERC2771.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultInternal.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultInternal.sol.png index 995da8a..bfc9c6e 100644 Binary files a/doc/surya/surya_graph/surya_graph_IncomeVaultInternal.sol.png and b/doc/surya/surya_graph/surya_graph_IncomeVaultInternal.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultInvariantStorage.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultInvariantStorage.sol.png index e166f8f..5b4fa6b 100644 Binary files a/doc/surya/surya_graph/surya_graph_IncomeVaultInvariantStorage.sol.png and b/doc/surya/surya_graph/surya_graph_IncomeVaultInvariantStorage.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultOpen.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultOpen.sol.png index 626dc06..8e56aa7 100644 Binary files a/doc/surya/surya_graph/surya_graph_IncomeVaultOpen.sol.png and b/doc/surya/surya_graph/surya_graph_IncomeVaultOpen.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultOperatorModule.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultOperatorModule.sol.png new file mode 100644 index 0000000..b2e3401 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IncomeVaultOperatorModule.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultOwnable2Step.sol.png new file mode 100644 index 0000000..708ddab Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IncomeVaultOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultRestricted.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultRestricted.sol.png index 78ad3d6..fb9b6b8 100644 Binary files a/doc/surya/surya_graph/surya_graph_IncomeVaultRestricted.sol.png and b/doc/surya/surya_graph/surya_graph_IncomeVaultRestricted.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultRolesStorage.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultRolesStorage.sol.png new file mode 100644 index 0000000..5b4fa6b Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IncomeVaultRolesStorage.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultSnapshotCore.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultSnapshotCore.sol.png new file mode 100644 index 0000000..e839627 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IncomeVaultSnapshotCore.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultSnapshotModule.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultSnapshotModule.sol.png new file mode 100644 index 0000000..26e75aa Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IncomeVaultSnapshotModule.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultValidationCore.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultValidationCore.sol.png new file mode 100644 index 0000000..d228809 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IncomeVaultValidationCore.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_IncomeVaultValidationModule.sol.png b/doc/surya/surya_graph/surya_graph_IncomeVaultValidationModule.sol.png new file mode 100644 index 0000000..3660e39 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IncomeVaultValidationModule.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_Ownable2StepERC165Module.sol.png b/doc/surya/surya_graph/surya_graph_Ownable2StepERC165Module.sol.png new file mode 100644 index 0000000..3f56112 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_Ownable2StepERC165Module.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_VersionModule.sol.png b/doc/surya/surya_graph/surya_graph_VersionModule.sol.png new file mode 100644 index 0000000..bb072c9 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_VersionModule.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_ERC7741Module.sol.png b/doc/surya/surya_inheritance/surya_inheritance_ERC7741Module.sol.png new file mode 100644 index 0000000..0da7e94 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_ERC7741Module.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IERC7540Operator.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IERC7540Operator.sol.png new file mode 100644 index 0000000..8887812 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IERC7540Operator.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IERC7741.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IERC7741.sol.png new file mode 100644 index 0000000..c3aabd7 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IERC7741.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IIncomeVault.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IIncomeVault.sol.png new file mode 100644 index 0000000..e218541 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IIncomeVault.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_ISnapshotSource.sol.png b/doc/surya/surya_inheritance/surya_inheritance_ISnapshotSource.sol.png new file mode 100644 index 0000000..c1f0fc1 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_ISnapshotSource.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVault.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVault.sol.png index 91f9858..3fc172d 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_IncomeVault.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVault.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultBase.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultBase.sol.png new file mode 100644 index 0000000..de5a6a7 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultBase.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultBaseERC2771.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultBaseERC2771.sol.png new file mode 100644 index 0000000..e8da155 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultBaseERC2771.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultInternal.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultInternal.sol.png index 2247a3b..189da84 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultInternal.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultInternal.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOpen.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOpen.sol.png index 945efb9..ae964f9 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOpen.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOpen.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOperatorModule.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOperatorModule.sol.png new file mode 100644 index 0000000..1caaab9 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOperatorModule.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOwnable2Step.sol.png new file mode 100644 index 0000000..b3c1cbb Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultRestricted.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultRestricted.sol.png index 2ae4a80..fabc834 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultRestricted.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultRestricted.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultRolesStorage.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultRolesStorage.sol.png new file mode 100644 index 0000000..46089a3 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultRolesStorage.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultSnapshotCore.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultSnapshotCore.sol.png new file mode 100644 index 0000000..5d7d2bc Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultSnapshotCore.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultSnapshotModule.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultSnapshotModule.sol.png new file mode 100644 index 0000000..b74e1fe Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultSnapshotModule.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultValidationCore.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultValidationCore.sol.png new file mode 100644 index 0000000..1ad6144 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultValidationCore.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultValidationModule.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultValidationModule.sol.png new file mode 100644 index 0000000..ed9375a Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IncomeVaultValidationModule.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_Ownable2StepERC165Module.sol.png b/doc/surya/surya_inheritance/surya_inheritance_Ownable2StepERC165Module.sol.png new file mode 100644 index 0000000..1b93eea Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_Ownable2StepERC165Module.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_VersionModule.sol.png b/doc/surya/surya_inheritance/surya_inheritance_VersionModule.sol.png new file mode 100644 index 0000000..a0dbc86 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_VersionModule.sol.png differ diff --git a/doc/surya/surya_report/surya_report_ERC7741Module.sol.md b/doc/surya/surya_report/surya_report_ERC7741Module.sol.md new file mode 100644 index 0000000..e257b45 --- /dev/null +++ b/doc/surya/surya_report/surya_report_ERC7741Module.sol.md @@ -0,0 +1,31 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/ERC7741Module.sol | 115b1c10b576d6b8730fd072060bdc8e0aa4fc81 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **ERC7741Module** | Implementation | EIP712Upgradeable, ContextUpgradeable, IncomeVaultOperatorModule, IncomeVaultInternal, IERC7741 ||| +| └ | authorizeOperator | Public ❗️ | 🛑 |NO❗️ | +| └ | invalidateNonce | Public ❗️ | 🛑 |NO❗️ | +| └ | authorizations | Public ❗️ | |NO❗️ | +| └ | DOMAIN_SEPARATOR | Public ❗️ | |NO❗️ | +| └ | _getERC7741ModuleStorage | Internal 🔒 | | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IERC7540Operator.sol.md b/doc/surya/surya_report/surya_report_IERC7540Operator.sol.md new file mode 100644 index 0000000..7085de1 --- /dev/null +++ b/doc/surya/surya_report/surya_report_IERC7540Operator.sol.md @@ -0,0 +1,28 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./interfaces/IERC7540Operator.sol | ca5ad35fa4f6215e0e25ee27376384c488a2e08b | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IERC7540Operator** | Interface | ||| +| └ | setOperator | External ❗️ | 🛑 |NO❗️ | +| └ | isOperator | External ❗️ | |NO❗️ | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IERC7741.sol.md b/doc/surya/surya_report/surya_report_IERC7741.sol.md new file mode 100644 index 0000000..704cb45 --- /dev/null +++ b/doc/surya/surya_report/surya_report_IERC7741.sol.md @@ -0,0 +1,30 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./interfaces/IERC7741.sol | e66067dd0470fe3be485360fa267559830ec12aa | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IERC7741** | Interface | ||| +| └ | authorizeOperator | External ❗️ | 🛑 |NO❗️ | +| └ | invalidateNonce | External ❗️ | 🛑 |NO❗️ | +| └ | authorizations | External ❗️ | |NO❗️ | +| └ | DOMAIN_SEPARATOR | External ❗️ | |NO❗️ | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IIncomeVault.sol.md b/doc/surya/surya_report/surya_report_IIncomeVault.sol.md new file mode 100644 index 0000000..f9630b7 --- /dev/null +++ b/doc/surya/surya_report/surya_report_IIncomeVault.sol.md @@ -0,0 +1,49 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./interfaces/IIncomeVault.sol | 945d7eda1636aeeeee8bb6d9b4d6ada9b3ba1c76 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IIncomeVault** | Interface | ||| +| └ | claimDividend | External ❗️ | 🛑 |NO❗️ | +| └ | claimDividendFor | External ❗️ | 🛑 |NO❗️ | +| └ | claimDividendBatch | External ❗️ | 🛑 |NO❗️ | +| └ | claimDividendBatchFor | External ❗️ | 🛑 |NO❗️ | +| └ | deposit | External ❗️ | 🛑 |NO❗️ | +| └ | depositBatch | External ❗️ | 🛑 |NO❗️ | +| └ | withdraw | External ❗️ | 🛑 |NO❗️ | +| └ | withdrawAll | External ❗️ | 🛑 |NO❗️ | +| └ | distributeDividend | External ❗️ | 🛑 |NO❗️ | +| └ | distributeDividendBestEffort | External ❗️ | 🛑 |NO❗️ | +| └ | setStatusClaim | External ❗️ | 🛑 |NO❗️ | +| └ | setTimeLimitToWithdraw | External ❗️ | 🛑 |NO❗️ | +| └ | validateTime | External ❗️ | |NO❗️ | +| └ | validateTimeBatch | External ❗️ | |NO❗️ | +| └ | validateTimeCode | External ❗️ | |NO❗️ | +| └ | ERC20TokenPayment | External ❗️ | |NO❗️ | +| └ | claimedDividend | External ❗️ | |NO❗️ | +| └ | segregatedDividend | External ❗️ | |NO❗️ | +| └ | segregatedClaim | External ❗️ | |NO❗️ | +| └ | paidDividend | External ❗️ | |NO❗️ | +| └ | unclaimedDividend | External ❗️ | |NO❗️ | +| └ | openClaimCount | External ❗️ | |NO❗️ | +| └ | timeLimitToWithdraw | External ❗️ | |NO❗️ | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_ISnapshotSource.sol.md b/doc/surya/surya_report/surya_report_ISnapshotSource.sol.md new file mode 100644 index 0000000..81fdcc2 --- /dev/null +++ b/doc/surya/surya_report/surya_report_ISnapshotSource.sol.md @@ -0,0 +1,29 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./interfaces/ISnapshotSource.sol | 31c7fd11a246b8abc14abc7bb4ca429cb2d16a14 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **ISnapshotSource** | Interface | ||| +| └ | snapshotInfo | External ❗️ | |NO❗️ | +| └ | snapshotInfoBatch | External ❗️ | |NO❗️ | +| └ | snapshotInfoBatch | External ❗️ | |NO❗️ | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IncomeVault.sol.md b/doc/surya/surya_report/surya_report_IncomeVault.sol.md index 080a99e..b2520dd 100644 --- a/doc/surya/surya_report/surya_report_IncomeVault.sol.md +++ b/doc/surya/surya_report/surya_report_IncomeVault.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./IncomeVault.sol | 32ca196e65692ae173ccb00988a492e5b8e942ed | +| ./deployment/IncomeVault.sol | 13f4d611a5dac650aae2720df488e06e8bf1290c | ### Contracts Description Table @@ -15,13 +15,22 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **IncomeVault** | Implementation | Initializable, ContextUpgradeable, IncomeVaultRestricted, IncomeVaultOpen, MetaTxModule ||| -| └ | | Public ❗️ | 🛑 | MetaTxModule | +| **IncomeVault** | Implementation | IncomeVaultValidationModule, IncomeVaultBaseERC2771, AccessControlModule, IncomeVaultRolesStorage ||| +| └ | | Public ❗️ | 🛑 | IncomeVaultBaseERC2771 | | └ | initialize | Public ❗️ | 🛑 | initializer | -| └ | __IncomeVault_init | Internal 🔒 | 🛑 | onlyInitializing | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | +| └ | _authorizeDeposit | Internal 🔒 | | onlyRole | +| └ | _authorizeWithdraw | Internal 🔒 | | onlyRole | +| └ | _authorizeDistribute | Internal 🔒 | | onlyRole | +| └ | _authorizeOperator | Internal 🔒 | | onlyRole | +| └ | _authorizeSnapshotSourceManagement | Internal 🔒 | | onlyRole | +| └ | _authorizeRuleEngineManagement | Internal 🔒 | | onlyRole | +| └ | _authorizePause | Internal 🔒 | | onlyRole | +| └ | _authorizeDeactivate | Internal 🔒 | | onlyRole | +| └ | _authorizeFreeze | Internal 🔒 | | onlyRole | ### Legend diff --git a/doc/surya/surya_report/surya_report_IncomeVaultBase.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultBase.sol.md new file mode 100644 index 0000000..7ad1e6d --- /dev/null +++ b/doc/surya/surya_report/surya_report_IncomeVaultBase.sol.md @@ -0,0 +1,27 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./IncomeVaultBase.sol | f548d23beccd532a2efc0b506a226025ad5904de | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IncomeVaultBase** | Implementation | IncomeVaultValidationCore, Initializable, ContextUpgradeable, VersionModule, IncomeVaultSnapshotModule, IncomeVaultRestricted, IncomeVaultOpen ||| +| └ | __IncomeVaultBase_init_unchained | Internal 🔒 | 🛑 | onlyInitializing | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IncomeVaultBaseERC2771.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultBaseERC2771.sol.md new file mode 100644 index 0000000..3961db7 --- /dev/null +++ b/doc/surya/surya_report/surya_report_IncomeVaultBaseERC2771.sol.md @@ -0,0 +1,30 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./IncomeVaultBaseERC2771.sol | f1775c8181ab32711ccdb08885baaf57cdb888f7 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IncomeVaultBaseERC2771** | Implementation | IncomeVaultBase, ERC2771Module ||| +| └ | | Public ❗️ | 🛑 | ERC2771Module | +| └ | _msgSender | Internal 🔒 | | | +| └ | _msgData | Internal 🔒 | | | +| └ | _contextSuffixLength | Internal 🔒 | | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IncomeVaultInternal.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultInternal.sol.md index 5940428..481fe3c 100644 --- a/doc/surya/surya_report/surya_report_IncomeVaultInternal.sol.md +++ b/doc/surya/surya_report/surya_report_IncomeVaultInternal.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./libraries/IncomeVaultInternal.sol | 7751f36237e74a1979555ff165eab6bf38f9d1ea | +| ./modules/IncomeVaultInternal.sol | 2f78f7afe0e91d0052b8b2e64d4d9608ac253bf5 | ### Contracts Description Table @@ -15,10 +15,26 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **IncomeVaultInternal** | Implementation | IncomeVaultInvariantStorage ||| +| **IncomeVaultInternal** | Implementation | IncomeVaultInvariantStorage, IIncomeVault ||| +| └ | ERC20TokenPayment | Public ❗️ | |NO❗️ | +| └ | claimedDividend | Public ❗️ | |NO❗️ | +| └ | segregatedDividend | Public ❗️ | |NO❗️ | +| └ | segregatedClaim | Public ❗️ | |NO❗️ | +| └ | paidDividend | Public ❗️ | |NO❗️ | +| └ | unclaimedDividend | Public ❗️ | |NO❗️ | +| └ | openClaimCount | Public ❗️ | |NO❗️ | +| └ | timeLimitToWithdraw | Public ❗️ | |NO❗️ | +| └ | _transferDividend | Internal 🔒 | 🛑 | | +| └ | _setERC20TokenPayment | Internal 🔒 | 🛑 | | +| └ | _setTimeLimitToWithdraw | Internal 🔒 | 🛑 | | +| └ | _deposit | Internal 🔒 | 🛑 | | +| └ | _setStatusClaim | Internal 🔒 | 🛑 | | +| └ | _unclaimed | Internal 🔒 | | | | └ | _computeDividendBatch | Internal 🔒 | | | | └ | _computeDividend | Internal 🔒 | | | -| └ | _transferDividend | Internal 🔒 | 🛑 | | +| └ | _revertOnInvalidTime | Internal 🔒 | | | +| └ | _timeCode | Internal 🔒 | | | +| └ | _getIncomeVaultInternalStorage | Internal 🔒 | | | ### Legend diff --git a/doc/surya/surya_report/surya_report_IncomeVaultInvariantStorage.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultInvariantStorage.sol.md index 0768fff..04f5509 100644 --- a/doc/surya/surya_report/surya_report_IncomeVaultInvariantStorage.sol.md +++ b/doc/surya/surya_report/surya_report_IncomeVaultInvariantStorage.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./libraries/IncomeVaultInvariantStorage.sol | 29271b2dc8a51952990adf824c54e9235f26ac9c | +| ./storage/IncomeVaultInvariantStorage.sol | d3b1b609b486de6f09a3b65fbcb15607afe99479 | ### Contracts Description Table diff --git a/doc/surya/surya_report/surya_report_IncomeVaultOpen.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultOpen.sol.md index f877dfb..3e97486 100644 --- a/doc/surya/surya_report/surya_report_IncomeVaultOpen.sol.md +++ b/doc/surya/surya_report/surya_report_IncomeVaultOpen.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./public/IncomeVaultOpen.sol | a9a9489512e617a87179a89b1c0eb4743ff68917 | +| ./public/IncomeVaultOpen.sol | 24be5507a7317b47d4f480911a6330568595427e | ### Contracts Description Table @@ -15,12 +15,16 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **IncomeVaultOpen** | Implementation | ReentrancyGuardUpgradeable, ValidationModule, IncomeVaultInternal ||| +| **IncomeVaultOpen** | Implementation | IncomeVaultValidationCore, IncomeVaultSnapshotCore, ERC7741Module, ReentrancyGuardTransient ||| +| └ | claimDividend | Public ❗️ | 🛑 | nonReentrant | +| └ | claimDividendFor | Public ❗️ | 🛑 | nonReentrant | +| └ | claimDividendBatchFor | Public ❗️ | 🛑 | nonReentrant | +| └ | claimDividendBatch | Public ❗️ | 🛑 | nonReentrant | | └ | validateTimeCode | Public ❗️ | |NO❗️ | | └ | validateTime | Public ❗️ | |NO❗️ | | └ | validateTimeBatch | Public ❗️ | |NO❗️ | -| └ | claimDividend | Public ❗️ | 🛑 | nonReentrant | -| └ | claimDividendBatch | Public ❗️ | 🛑 | nonReentrant | +| └ | _claimDividend | Internal 🔒 | 🛑 | | +| └ | _claimDividendBatch | Internal 🔒 | 🛑 | | ### Legend diff --git a/doc/surya/surya_report/surya_report_IncomeVaultOperatorModule.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultOperatorModule.sol.md new file mode 100644 index 0000000..650f375 --- /dev/null +++ b/doc/surya/surya_report/surya_report_IncomeVaultOperatorModule.sol.md @@ -0,0 +1,31 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/IncomeVaultOperatorModule.sol | 43916bbbde62208a7ab9b6799b22c23480498432 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IncomeVaultOperatorModule** | Implementation | ContextUpgradeable, IncomeVaultInvariantStorage, IERC7540Operator ||| +| └ | setOperator | Public ❗️ | 🛑 |NO❗️ | +| └ | isOperator | Public ❗️ | |NO❗️ | +| └ | _setOperator | Internal 🔒 | 🛑 | | +| └ | _requireHolderOrOperator | Internal 🔒 | | | +| └ | _getOperatorStorage | Internal 🔒 | | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IncomeVaultOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultOwnable2Step.sol.md new file mode 100644 index 0000000..b02873c --- /dev/null +++ b/doc/surya/surya_report/surya_report_IncomeVaultOwnable2Step.sol.md @@ -0,0 +1,41 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./deployment/IncomeVaultOwnable2Step.sol | f3b2b1bd3aa3b3c493521a48a0089cd874355001 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IncomeVaultOwnable2Step** | Implementation | IncomeVaultValidationModule, IncomeVaultBaseERC2771, Ownable2StepUpgradeable, Ownable2StepERC165Module ||| +| └ | | Public ❗️ | 🛑 | IncomeVaultBaseERC2771 | +| └ | initialize | Public ❗️ | 🛑 | initializer | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | _msgSender | Internal 🔒 | | | +| └ | _msgData | Internal 🔒 | | | +| └ | _contextSuffixLength | Internal 🔒 | | | +| └ | _authorizeDeposit | Internal 🔒 | | onlyOwner | +| └ | _authorizeWithdraw | Internal 🔒 | | onlyOwner | +| └ | _authorizeDistribute | Internal 🔒 | | onlyOwner | +| └ | _authorizeOperator | Internal 🔒 | | onlyOwner | +| └ | _authorizeSnapshotSourceManagement | Internal 🔒 | | onlyOwner | +| └ | _authorizeRuleEngineManagement | Internal 🔒 | | onlyOwner | +| └ | _authorizePause | Internal 🔒 | | onlyOwner | +| └ | _authorizeDeactivate | Internal 🔒 | | onlyOwner | +| └ | _authorizeFreeze | Internal 🔒 | | onlyOwner | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IncomeVaultRestricted.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultRestricted.sol.md index 256bf0e..62d7462 100644 --- a/doc/surya/surya_report/surya_report_IncomeVaultRestricted.sol.md +++ b/doc/surya/surya_report/surya_report_IncomeVaultRestricted.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./public/IncomeVaultRestricted.sol | 2cd9f60df6c3914215089a4a7522b2b8ce9973a6 | +| ./public/IncomeVaultRestricted.sol | 93d0d2ecfc87313ca8f8dd1bbd125d333a06e15d | ### Contracts Description Table @@ -15,14 +15,21 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **IncomeVaultRestricted** | Implementation | ValidationModule, IncomeVaultInternal ||| +| **IncomeVaultRestricted** | Implementation | IncomeVaultValidationCore, IncomeVaultSnapshotCore, ContextUpgradeable, IncomeVaultInternal, ReentrancyGuardTransient ||| | └ | __IncomeVaultRestricted_init_unchained | Internal 🔒 | 🛑 | onlyInitializing | -| └ | deposit | Public ❗️ | 🛑 | onlyRole | -| └ | withdraw | Public ❗️ | 🛑 | onlyRole | -| └ | withdrawAll | Public ❗️ | 🛑 | onlyRole | -| └ | distributeDividend | Public ❗️ | 🛑 | onlyRole | -| └ | setStatusClaim | Public ❗️ | 🛑 | onlyRole | -| └ | setTimeLimitToWithdraw | Public ❗️ | 🛑 | onlyRole | +| └ | deposit | Public ❗️ | 🛑 | onlyDepositManager | +| └ | depositBatch | Public ❗️ | 🛑 | onlyDepositManager | +| └ | withdraw | Public ❗️ | 🛑 | onlyWithdrawManager | +| └ | withdrawAll | Public ❗️ | 🛑 | onlyWithdrawManager | +| └ | distributeDividend | Public ❗️ | 🛑 | onlyDistributeManager | +| └ | distributeDividendBestEffort | Public ❗️ | 🛑 | nonReentrant onlyDistributeManager | +| └ | transferDividendSelf | Public ❗️ | 🛑 |NO❗️ | +| └ | setStatusClaim | Public ❗️ | 🛑 | onlyVaultOperator | +| └ | setTimeLimitToWithdraw | Public ❗️ | 🛑 | onlyVaultOperator | +| └ | _authorizeDeposit | Internal 🔒 | | | +| └ | _authorizeWithdraw | Internal 🔒 | | | +| └ | _authorizeDistribute | Internal 🔒 | | | +| └ | _authorizeOperator | Internal 🔒 | | | ### Legend diff --git a/doc/surya/surya_report/surya_report_IncomeVaultRolesStorage.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultRolesStorage.sol.md new file mode 100644 index 0000000..2749e58 --- /dev/null +++ b/doc/surya/surya_report/surya_report_IncomeVaultRolesStorage.sol.md @@ -0,0 +1,26 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./storage/IncomeVaultRolesStorage.sol | 67a088ca0b5bb9915ee014a72fd2db0ed50a574c | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IncomeVaultRolesStorage** | Implementation | ||| + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IncomeVaultSnapshotCore.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultSnapshotCore.sol.md new file mode 100644 index 0000000..2c5bc9b --- /dev/null +++ b/doc/surya/surya_report/surya_report_IncomeVaultSnapshotCore.sol.md @@ -0,0 +1,29 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/IncomeVaultSnapshotCore.sol | 62d5cd3c23a270269036132d4a0010c05faed59f | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IncomeVaultSnapshotCore** | Implementation | ||| +| └ | _snapshotInfo | Internal 🔒 | | | +| └ | _snapshotInfoBatch | Internal 🔒 | | | +| └ | _snapshotInfoBatch | Internal 🔒 | | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IncomeVaultSnapshotModule.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultSnapshotModule.sol.md new file mode 100644 index 0000000..81b8ed3 --- /dev/null +++ b/doc/surya/surya_report/surya_report_IncomeVaultSnapshotModule.sol.md @@ -0,0 +1,34 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/IncomeVaultSnapshotModule.sol | 852f2f2231a558e3b1eb004a92f5c6193f23e82d | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IncomeVaultSnapshotModule** | Implementation | IncomeVaultSnapshotCore, IncomeVaultInternal ||| +| └ | setDividendSnapshotSource | Public ❗️ | 🛑 | onlySnapshotSourceManager | +| └ | dividendSnapshotSource | Public ❗️ | |NO❗️ | +| └ | _setDividendSnapshotSource | Internal 🔒 | 🛑 | | +| └ | _snapshotInfo | Internal 🔒 | | | +| └ | _snapshotInfoBatch | Internal 🔒 | | | +| └ | _snapshotInfoBatch | Internal 🔒 | | | +| └ | _authorizeSnapshotSourceManagement | Internal 🔒 | | | +| └ | _getSnapshotSourceStorage | Internal 🔒 | | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IncomeVaultValidationCore.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultValidationCore.sol.md new file mode 100644 index 0000000..1747eed --- /dev/null +++ b/doc/surya/surya_report/surya_report_IncomeVaultValidationCore.sol.md @@ -0,0 +1,27 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/IncomeVaultValidationCore.sol | 109ca749149009628d7b58bb65938a1e4a092e02 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IncomeVaultValidationCore** | Implementation | ||| +| └ | _validateTransfer | Internal 🔒 | | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_IncomeVaultValidationModule.sol.md b/doc/surya/surya_report/surya_report_IncomeVaultValidationModule.sol.md new file mode 100644 index 0000000..3fe7480 --- /dev/null +++ b/doc/surya/surya_report/surya_report_IncomeVaultValidationModule.sol.md @@ -0,0 +1,33 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/IncomeVaultValidationModule.sol | 21fe5f150b450330f495152879c605f96f6e7a8c | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **IncomeVaultValidationModule** | Implementation | IncomeVaultValidationCore, PauseModule, EnforcementModule, ValidationModuleRuleEngineInternal, IncomeVaultInvariantStorage ||| +| └ | __IncomeVaultValidation_init_unchained | Internal 🔒 | 🛑 | onlyInitializing | +| └ | setRuleEngine | Public ❗️ | 🛑 | onlyRuleEngineManager | +| └ | canTransfer | Public ❗️ | |NO❗️ | +| └ | detectTransferRestriction | Public ❗️ | |NO❗️ | +| └ | messageForTransferRestriction | Public ❗️ | |NO❗️ | +| └ | _authorizeRuleEngineManagement | Internal 🔒 | | | +| └ | _validateTransfer | Internal 🔒 | | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_Ownable2StepERC165Module.sol.md b/doc/surya/surya_report/surya_report_Ownable2StepERC165Module.sol.md new file mode 100644 index 0000000..b937d06 --- /dev/null +++ b/doc/surya/surya_report/surya_report_Ownable2StepERC165Module.sol.md @@ -0,0 +1,27 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/Ownable2StepERC165Module.sol | e6080457d6b178b4c08b15b4e0d6cae42cb78794 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **Ownable2StepERC165Module** | Implementation | ERC165Upgradeable ||| +| └ | supportsInterface | Public ❗️ | |NO❗️ | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_VersionModule.sol.md b/doc/surya/surya_report/surya_report_VersionModule.sol.md new file mode 100644 index 0000000..7e140ce --- /dev/null +++ b/doc/surya/surya_report/surya_report_VersionModule.sol.md @@ -0,0 +1,27 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/VersionModule.sol | 1d7eea441566b01bb3e33de60ee79ecbb793b030 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **VersionModule** | Implementation | IERC3643Version ||| +| └ | version | Public ❗️ | |NO❗️ | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/technical.md b/doc/technical.md deleted file mode 100644 index b4b6177..0000000 --- a/doc/technical.md +++ /dev/null @@ -1,46 +0,0 @@ -# Technical choice - -[TOC] - -## Functionality - -### Upgradeable - -The `IncomeVault` is upgradeable and can be deployed with a Transparent Proxy. - -### Urgency mechanism - -Through the ValidationModule, the contract can be put in paused, forbidding all claims. - - -### Gasless support - -> The gasless integration was not part of the audit performed by ABDK on the version [1.0.1](https://github.com/CMTA/RuleEngine/releases/tag/1.0.1) - -The `IncomeVault` contract supports client-side gasless transactions using the [Gas Station Network](https://docs.opengsn.org/#the-problem) (GSN) pattern, the main open standard for transfering fee payment to another account than that of the transaction issuer. The contract uses the OpenZeppelin contract `ERC2771Context`, which allows a contract to get the original client with `_msgSender()` instead of the fee payer given by `msg.sender` . - -At deployment, the parameter `forwarder` inside the contract constructor has to be set with the defined address of the forwarder. Please note that the forwarder can not be changed after deployment. - -Please see the OpenGSN [documentation](https://docs.opengsn.org/contracts/#receiving-a-relayed-call) for more details on what is done to support GSN in the contract. - -## Schema - -### UML - -![uml](./schema/classDiagram.svg) - - - -## Graph - -### IncomeVault - -![surya_graph_IncomeVault](../doc/surya/surya_graph/surya_graph_IncomeVault.sol.png) - -### IncomeVaultOpen - -![surya_graph_IncomeVaultOpen](../doc/surya/surya_graph/surya_graph_IncomeVaultOpen.sol.png) - -### IncomeVaultRestricted - -![surya_graph_IncomeVaultRestricted](../doc/surya/surya_graph/surya_graph_IncomeVaultRestricted.sol.png) diff --git a/doc/test/coverage/src/RuleEngine.sol.func-sort-c.html b/doc/test/coverage/src/RuleEngine.sol.func-sort-c.html deleted file mode 100644 index 7ea7c73..0000000 --- a/doc/test/coverage/src/RuleEngine.sol.func-sort-c.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - LCOV - lcov.info - src/RuleEngine.sol - functions - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - src - RuleEngine.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:474995.9 %
Date:2023-11-21 13:10:43Functions:121392.3 %
Branches:182090.0 %
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Function Name Sort by function nameHit count Sort by hit count
RuleEngine._msgData0
RuleEngine.rule1
RuleEngine.clearRules3
RuleEngine.getRuleIndex3
RuleEngine.rules4
RuleEngine.messageForTransferRestriction6
RuleEngine.removeRule6
RuleEngine.detectTransferRestriction7
RuleEngine.addRule9
RuleEngine.setRules10
RuleEngine.validateTransfer10
RuleEngine.rulesCount20
RuleEngine._msgSender38
-
-
- - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/src/RuleEngine.sol.func.html b/doc/test/coverage/src/RuleEngine.sol.func.html deleted file mode 100644 index 3835b16..0000000 --- a/doc/test/coverage/src/RuleEngine.sol.func.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - LCOV - lcov.info - src/RuleEngine.sol - functions - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - src - RuleEngine.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:474995.9 %
Date:2023-11-21 13:10:43Functions:121392.3 %
Branches:182090.0 %
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Function Name Sort by function nameHit count Sort by hit count
RuleEngine._msgData0
RuleEngine._msgSender38
RuleEngine.addRule9
RuleEngine.clearRules3
RuleEngine.detectTransferRestriction7
RuleEngine.getRuleIndex3
RuleEngine.messageForTransferRestriction6
RuleEngine.removeRule6
RuleEngine.rule1
RuleEngine.rules4
RuleEngine.rulesCount20
RuleEngine.setRules10
RuleEngine.validateTransfer10
-
-
- - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/src/RuleEngine.sol.gcov.html b/doc/test/coverage/src/RuleEngine.sol.gcov.html deleted file mode 100644 index 57245ba..0000000 --- a/doc/test/coverage/src/RuleEngine.sol.gcov.html +++ /dev/null @@ -1,340 +0,0 @@ - - - - - - - LCOV - lcov.info - src/RuleEngine.sol - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - src - RuleEngine.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:474995.9 %
Date:2023-11-21 13:10:43Functions:121392.3 %
Branches:182090.0 %
-
- - - - - - - - -

-
           Branch data     Line data    Source code
-
-       1                 :            : // SPDX-License-Identifier: MPL-2.0
-       2                 :            : 
-       3                 :            : pragma solidity ^0.8.20;
-       4                 :            : 
-       5                 :            : import "CMTAT/mocks/RuleEngine/interfaces/IRule.sol";
-       6                 :            : import "CMTAT/mocks/RuleEngine/interfaces/IRuleEngine.sol";
-       7                 :            : import "./modules/MetaTxModuleStandalone.sol";
-       8                 :            : import "../lib/openzeppelin-contracts/contracts/access/AccessControl.sol";
-       9                 :            : 
-      10                 :            : /**
-      11                 :            : @title Implementation of a ruleEngine defined by the CMTAT
-      12                 :            : */
-      13                 :            : contract RuleEngine is IRuleEngine, AccessControl, MetaTxModuleStandalone {
-      14                 :            :     error RuleEngine_RuleAddressZeroNotAllowed();
-      15                 :            :     error RuleEngine_RuleAlreadyExists();
-      16                 :            :     error RuleEngine_RuleDoNotMatch();
-      17                 :            :     error RuleEngine_AdminWithAddressZeroNotAllowed();
-      18                 :            :     error RuleEngine_ArrayIsEmpty();
-      19                 :            :     /// @dev Role to manage the ruleEngine
-      20                 :            :     bytes32 public constant RULE_ENGINE_ROLE = keccak256("RULE_ENGINE_ROLE");
-      21                 :            :     /// @dev Indicate if a rule already exists
-      22                 :            :     mapping(IRule => bool) _ruleIsPresent;
-      23                 :            :     /// @dev Array of rules
-      24                 :            :     IRule[] internal _rules;
-      25                 :            :     /// @notice Generate when a rule is added
-      26                 :            :     event AddRule(IRule indexed rule);
-      27                 :            :     /// @notice Generate when a rule is removed
-      28                 :            :     event RemoveRule(IRule indexed rule);
-      29                 :            :     /// @notice Generate when all the rules are cleared
-      30                 :            :     event ClearRules(IRule[] rulesRemoved);
-      31                 :            : 
-      32                 :            :     /**
-      33                 :            :     * @param admin Address of the contract (Access Control)
-      34                 :            :     * @param forwarderIrrevocable Address of the forwarder, required for the gasless support
-      35                 :            :     */
-      36                 :            :     constructor(
-      37                 :            :         address admin,
-      38                 :            :         address forwarderIrrevocable
-      39                 :            :     ) MetaTxModuleStandalone(forwarderIrrevocable) {
-      40                 :            :         if(admin == address(0))
-      41                 :            :         {
-      42                 :            :             revert RuleEngine_AdminWithAddressZeroNotAllowed();
-      43                 :            :         }
-      44                 :            :         _grantRole(DEFAULT_ADMIN_ROLE, admin);
-      45                 :            :         _grantRole(RULE_ENGINE_ROLE, admin);
-      46                 :            :     }
-      47                 :            : 
-      48                 :            :     /**
-      49                 :            :      * @notice Set all the rules, will overwrite all the previous rules. \n
-      50                 :            :      * Revert if one rule is a zero address or if the rule is already present
-      51                 :            :      *
-      52                 :            :      */
-      53                 :            :     function setRules(
-      54                 :            :         IRule[] calldata rules_
-      55                 :            :     ) external override onlyRole(RULE_ENGINE_ROLE) {
-      56         [ +  + ]:          9 :         if(rules_.length == 0){
-      57                 :          1 :             revert RuleEngine_ArrayIsEmpty();
-      58                 :            :         }
-      59                 :          8 :         for (uint256 i = 0; i < rules_.length; ) {
-      60         [ +  + ]:         15 :             if( address(rules_[i]) == address(0x0)){
-      61                 :          1 :                 revert  RuleEngine_RuleAddressZeroNotAllowed();
-      62                 :            :             }
-      63         [ +  + ]:         14 :             if(_ruleIsPresent[rules_[i]]){
-      64                 :          1 :                 revert RuleEngine_RuleAlreadyExists();
-      65                 :            :             }
-      66                 :         13 :             _ruleIsPresent[rules_[i]] = true;
-      67                 :         13 :             emit AddRule(rules_[i]);
-      68                 :            :             unchecked {
-      69                 :         13 :                 ++i;
-      70                 :            :             }
-      71                 :            :         }
-      72                 :          6 :         _rules = rules_;
-      73                 :            :     }
-      74                 :            : 
-      75                 :            :     /**
-      76                 :            :      * @notice Clear all the rules of the array of rules
-      77                 :            :      *
-      78                 :            :      */
-      79                 :            :     function clearRules() public onlyRole(RULE_ENGINE_ROLE) {
-      80                 :          2 :         emit ClearRules(_rules);
-      81                 :          2 :         _rules = new IRule[](0);
-      82                 :            :     }
-      83                 :            : 
-      84                 :            :     /**
-      85                 :            :      * @notice Add a rule to the array of rules
-      86                 :            :      * Revert if one rule is a zero address or if the rule is already present
-      87                 :            :      *
-      88                 :            :      */
-      89                 :            :     function addRule(IRule rule_) public onlyRole(RULE_ENGINE_ROLE) {
-      90         [ +  + ]:          8 :         if( address(rule_) == address(0x0))
-      91                 :            :         {
-      92                 :          1 :             revert RuleEngine_RuleAddressZeroNotAllowed();
-      93                 :            :         }
-      94         [ +  + ]:          7 :         if( _ruleIsPresent[rule_])
-      95                 :            :         {
-      96                 :          1 :             revert RuleEngine_RuleAlreadyExists();
-      97                 :            :         }
-      98                 :          6 :         _rules.push(rule_);
-      99                 :          6 :         _ruleIsPresent[rule_] = true;
-     100                 :          6 :         emit AddRule(rule_);
-     101                 :            :     }
-     102                 :            : 
-     103                 :            :     /**
-     104                 :            :      * @notice Remove a rule from the array of rules
-     105                 :            :      * Revert if the rule found at the specified index does not match the rule in argument
-     106                 :            :      * @param rule_ address of the target rule
-     107                 :            :      * @param index the position inside the array of rule
-     108                 :            :      * @dev To reduce the array size, the last rule is moved to the location occupied
-     109                 :            :      * by the rule to remove
-     110                 :            :      *
-     111                 :            :      *
-     112                 :            :      */
-     113                 :            :     function removeRule(
-     114                 :            :         IRule rule_,
-     115                 :            :         uint256 index
-     116                 :            :     ) public onlyRole(RULE_ENGINE_ROLE) {
-     117         [ +  + ]:          5 :         if(_rules[index] != rule_)
-     118                 :            :         {
-     119                 :          1 :             revert RuleEngine_RuleDoNotMatch();
-     120                 :            :         }
-     121         [ #  + ]:          4 :         if (index != _rules.length - 1) {
-     122                 :          2 :             _rules[index] = _rules[_rules.length - 1];
-     123                 :            :         }
-     124                 :          4 :         _rules.pop();
-     125                 :          4 :         _ruleIsPresent[rule_] = false;
-     126                 :          4 :         emit RemoveRule(rule_);
-     127                 :            :     }
-     128                 :            : 
-     129                 :            :     /**
-     130                 :            :     * @return The number of rules inside the array
-     131                 :            :     */
-     132                 :            :     function rulesCount() external view override returns (uint256) {
-     133                 :         20 :         return _rules.length;
-     134                 :            :     }
-     135                 :            : 
-     136                 :            :     /**
-     137                 :            :     * @notice Get the index of a rule inside the list
-     138                 :            :     * @return index if the rule is found, _rules.length otherwise
-     139                 :            :     */
-     140                 :            :     function getRuleIndex(IRule rule_) external view returns (uint256 index) {
-     141                 :          0 :         for (index = 0; index < _rules.length; ) {
-     142         [ +  + ]:          5 :             if (_rules[index] == rule_) {
-     143                 :          5 :                 return index;
-     144                 :            :             }
-     145                 :            :             unchecked {
-     146                 :          3 :                 ++index;
-     147                 :            :             }
-     148                 :            :         }
-     149                 :          1 :         return _rules.length;
-     150                 :            :     }
-     151                 :            : 
-     152                 :            :     /**
-     153                 :            :     * @notice Get the rule at the position specified by ruleId
-     154                 :            :     * @param ruleId index of the rule
-     155                 :            :     * @return a rule address
-     156                 :            :     */
-     157                 :            :     function rule(uint256 ruleId) external view override returns (IRule) {
-     158                 :          1 :         return _rules[ruleId];
-     159                 :            :     }
-     160                 :            : 
-     161                 :            :     /**
-     162                 :            :     * @notice Get all the rules
-     163                 :            :     * @return An array of rules
-     164                 :            :     */
-     165                 :            :     function rules() external view override returns (IRule[] memory) {
-     166                 :          4 :         return _rules;
-     167                 :            :     }
-     168                 :            : 
-     169                 :            :     /** 
-     170                 :            :     * @notice Go through all the rule to know if a restriction exists on the transfer
-     171                 :            :     * @param _from the origin address
-     172                 :            :     * @param _to the destination address
-     173                 :            :     * @param _amount to transfer
-     174                 :            :     * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK
-     175                 :            :     **/
-     176                 :            :     function detectTransferRestriction(
-     177                 :            :         address _from,
-     178                 :            :         address _to,
-     179                 :            :         uint256 _amount
-     180                 :            :     ) public view override returns (uint8) {
-     181                 :         17 :         for (uint256 i = 0; i < _rules.length; ) {
-     182                 :         17 :             uint8 restriction = _rules[i].detectTransferRestriction(
-     183                 :            :                 _from,
-     184                 :            :                 _to,
-     185                 :            :                 _amount
-     186                 :            :             );
-     187         [ +  + ]:         17 :             if (restriction > 0) {
-     188                 :         12 :                 return restriction;
-     189                 :            :             }
-     190                 :            :             unchecked {
-     191                 :          5 :                 ++i;
-     192                 :            :             }
-     193                 :            :         }
-     194                 :          5 :         return uint8(REJECTED_CODE_BASE.TRANSFER_OK);
-     195                 :            :     }
-     196                 :            : 
-     197                 :            :     /** 
-     198                 :            :     * @notice Validate a transfer
-     199                 :            :     * @param _from the origin address
-     200                 :            :     * @param _to the destination address
-     201                 :            :     * @param _amount to transfer
-     202                 :            :     * @return True if the transfer is valid, false otherwise
-     203                 :            :     **/
-     204                 :            :     function validateTransfer(
-     205                 :            :         address _from,
-     206                 :            :         address _to,
-     207                 :            :         uint256 _amount
-     208                 :            :     ) public view override returns (bool) {
-     209                 :         10 :         return detectTransferRestriction(_from, _to, _amount) == uint8(REJECTED_CODE_BASE.TRANSFER_OK);
-     210                 :            :     }
-     211                 :            : 
-     212                 :            :     /** 
-     213                 :            :     * @notice Return the message corresponding to the code
-     214                 :            :     * @param _restrictionCode The target restriction code
-     215                 :            :     * @return True if the transfer is valid, false otherwise
-     216                 :            :     **/
-     217                 :            :     function messageForTransferRestriction(
-     218                 :            :         uint8 _restrictionCode
-     219                 :            :     ) external view override returns (string memory) {
-     220                 :          6 :         for (uint256 i = 0; i < _rules.length; ) {
-     221         [ #  + ]:          5 :             if (_rules[i].canReturnTransferRestrictionCode(_restrictionCode)) {
-     222                 :          4 :                 return
-     223                 :            :                     _rules[i].messageForTransferRestriction(_restrictionCode);
-     224                 :            :             }
-     225                 :            :             unchecked {
-     226                 :          1 :                 ++i;
-     227                 :            :             }
-     228                 :            :         }
-     229                 :          2 :         return "Unknown restriction code";
-     230                 :            :     }
-     231                 :            : 
-     232                 :            :     /** 
-     233                 :            :     * @dev This surcharge is not necessary if you do not use the MetaTxModule
-     234                 :            :     */
-     235                 :            :     function _msgSender()
-     236                 :            :         internal
-     237                 :            :         view
-     238                 :            :         override(MetaTxModuleStandalone, Context)
-     239                 :            :         returns (address sender)
-     240                 :            :     {
-     241                 :         38 :         return MetaTxModuleStandalone._msgSender();
-     242                 :            :     }
-     243                 :            : 
-     244                 :            :     /** 
-     245                 :            :     * @dev This surcharge is not necessary if you do not use the MetaTxModule
-     246                 :            :     */
-     247                 :            :     function _msgData()
-     248                 :            :         internal
-     249                 :            :         view
-     250                 :            :         override(MetaTxModuleStandalone, Context)
-     251                 :            :         returns (bytes calldata)
-     252                 :            :     {
-     253                 :          0 :         return MetaTxModuleStandalone._msgData();
-     254                 :            :     }
-     255                 :            : }
-
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/src/index-sort-b.html b/doc/test/coverage/src/index-sort-b.html deleted file mode 100644 index 39d6409..0000000 --- a/doc/test/coverage/src/index-sort-b.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - LCOV - lcov.info - src - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - srcHitTotalCoverage
Test:lcov.infoLines:474995.9 %
Date:2023-11-21 13:10:43Functions:121392.3 %
Branches:182090.0 %
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngine.sol -
95.9%95.9%
-
95.9 %47 / 4992.3 %12 / 1390.0 %18 / 20
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/src/index-sort-f.html b/doc/test/coverage/src/index-sort-f.html deleted file mode 100644 index 544b632..0000000 --- a/doc/test/coverage/src/index-sort-f.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - LCOV - lcov.info - src - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - srcHitTotalCoverage
Test:lcov.infoLines:474995.9 %
Date:2023-11-21 13:10:43Functions:121392.3 %
Branches:182090.0 %
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngine.sol -
95.9%95.9%
-
95.9 %47 / 4992.3 %12 / 1390.0 %18 / 20
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/src/index-sort-l.html b/doc/test/coverage/src/index-sort-l.html deleted file mode 100644 index a2f9800..0000000 --- a/doc/test/coverage/src/index-sort-l.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - LCOV - lcov.info - src - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - srcHitTotalCoverage
Test:lcov.infoLines:474995.9 %
Date:2023-11-21 13:10:43Functions:121392.3 %
Branches:182090.0 %
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngine.sol -
95.9%95.9%
-
95.9 %47 / 4992.3 %12 / 1390.0 %18 / 20
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/src/index.html b/doc/test/coverage/src/index.html deleted file mode 100644 index d452980..0000000 --- a/doc/test/coverage/src/index.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - LCOV - lcov.info - src - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - srcHitTotalCoverage
Test:lcov.infoLines:474995.9 %
Date:2023-11-21 13:10:43Functions:121392.3 %
Branches:182090.0 %
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
RuleEngine.sol -
95.9%95.9%
-
95.9 %47 / 4992.3 %12 / 1390.0 %18 / 20
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/src/rules/RuleSanctionList.sol.gcov.html b/doc/test/coverage/src/rules/RuleSanctionList.sol.gcov.html deleted file mode 100644 index 34d6650..0000000 --- a/doc/test/coverage/src/rules/RuleSanctionList.sol.gcov.html +++ /dev/null @@ -1,217 +0,0 @@ - - - - - - - LCOV - lcov.info - src/rules/RuleSanctionList.sol - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - src/rules - RuleSanctionList.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:151693.8 %
Date:2023-11-21 13:10:43Functions:6785.7 %
Branches:1010100.0 %
-
- - - - - - - - -

-
           Branch data     Line data    Source code
-
-       1                 :            : // SPDX-License-Identifier: MPL-2.0
-       2                 :            : 
-       3                 :            : pragma solidity ^0.8.20;
-       4                 :            : import "../../lib/openzeppelin-contracts/contracts/access/AccessControl.sol";
-       5                 :            : import "../../lib/CMTAT/contracts/mocks/RuleEngine/interfaces/IRule.sol";
-       6                 :            : import "../modules/MetaTxModuleStandalone.sol";
-       7                 :            : import "./abstract/RuleSanctionListInvariantStorage.sol";
-       8                 :            : interface SanctionsList {
-       9                 :            :     function isSanctioned(address addr) external view returns (bool);
-      10                 :            : }
-      11                 :            : 
-      12                 :            : contract RuleSanctionList is IRule, AccessControl, MetaTxModuleStandalone,  RuleSanctionlistInvariantStorage {
-      13                 :            :     SanctionsList  public sanctionsList;
-      14                 :            : 
-      15                 :            :     /**
-      16                 :            :     * @param admin Address of the contract (Access Control)
-      17                 :            :     * @param forwarderIrrevocable Address of the forwarder, required for the gasless support
-      18                 :            :     */
-      19                 :            :     constructor(
-      20                 :            :         address admin,
-      21                 :            :         address forwarderIrrevocable
-      22                 :            :     ) MetaTxModuleStandalone(forwarderIrrevocable) {
-      23                 :            :         if(admin == address(0)){
-      24                 :            :             revert RuleSanctionList_AdminWithAddressZeroNotAllowed();
-      25                 :            :         }
-      26                 :            :         _grantRole(DEFAULT_ADMIN_ROLE, admin);
-      27                 :            :         _grantRole(SANCTIONLIST_ROLE, admin);
-      28                 :            :     }
-      29                 :            : 
-      30                 :            :     /**
-      31                 :            :      * @notice Set the oracle contract
-      32                 :            :      * @param sanctionContractOracle_ address of your oracle contract
-      33                 :            :      * @dev zero address is authorized to authorize all transfers
-      34                 :            :      */
-      35                 :            :     function setOracle(
-      36                 :            :        address sanctionContractOracle_
-      37                 :            :     ) public onlyRole(SANCTIONLIST_ROLE) {
-      38                 :          1 :         sanctionsList = SanctionsList(sanctionContractOracle_);
-      39                 :            :     }
-      40                 :            : 
-      41                 :            :     /** 
-      42                 :            :     * @notice Validate a transfer
-      43                 :            :     * @param _from the origin address
-      44                 :            :     * @param _to the destination address
-      45                 :            :     * @param _amount to transfer
-      46                 :            :     * @return isValid => true if the transfer is valid, false otherwise
-      47                 :            :     **/
-      48                 :            :     function validateTransfer(
-      49                 :            :         address _from,
-      50                 :            :         address _to,
-      51                 :            :         uint256 _amount
-      52                 :            :     ) public view override returns (bool isValid) {
-      53                 :          4 :         return
-      54                 :            :             detectTransferRestriction(_from, _to, _amount) ==
-      55                 :            :             uint8(REJECTED_CODE_BASE.TRANSFER_OK);
-      56                 :            :     }
-      57                 :            : 
-      58                 :            :     /** 
-      59                 :            :     * @notice Check if an addres is in the whitelist or not
-      60                 :            :     * @param _from the origin address
-      61                 :            :     * @param _to the destination address
-      62                 :            :     * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK
-      63                 :            :     **/
-      64                 :            :     function detectTransferRestriction(
-      65                 :            :         address _from,
-      66                 :            :         address _to,
-      67                 :            :         uint256 /*_amount */
-      68                 :            :     ) public view override returns (uint8) {
-      69         [ +  + ]:          7 :         if(address(sanctionsList) != address(0)){
-      70         [ +  + ]:          7 :             if (sanctionsList.isSanctioned(_from)) {
-      71                 :          2 :                 return CODE_ADDRESS_FROM_IS_SANCTIONED;
-      72         [ +  + ]:          5 :             } else if (sanctionsList.isSanctioned(_to)) {
-      73                 :          2 :                 return  CODE_ADDRESS_TO_IS_SANCTIONED;
-      74                 :            :             }
-      75                 :            :         }
-      76                 :          3 :         return uint8(REJECTED_CODE_BASE.TRANSFER_OK);
-      77                 :            :     }
-      78                 :            : 
-      79                 :            :     /** 
-      80                 :            :     * @notice To know if the restriction code is valid for this rule or not.
-      81                 :            :     * @param _restrictionCode The target restriction code
-      82                 :            :     * @return true if the restriction code is known, false otherwise
-      83                 :            :     **/
-      84                 :            :     function canReturnTransferRestrictionCode(
-      85                 :            :         uint8 _restrictionCode
-      86                 :            :     ) external pure override returns (bool) {
-      87                 :          3 :         return
-      88                 :            :             _restrictionCode ==  CODE_ADDRESS_FROM_IS_SANCTIONED ||
-      89                 :            :             _restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED;
-      90                 :            :     }
-      91                 :            : 
-      92                 :            :     /** 
-      93                 :            :     * @notice Return the corresponding message
-      94                 :            :     * @param _restrictionCode The target restriction code
-      95                 :            :     * @return true if the transfer is valid, false otherwise
-      96                 :            :     **/
-      97                 :            :     function messageForTransferRestriction(
-      98                 :            :         uint8 _restrictionCode
-      99                 :            :     ) external pure override returns (string memory) {
-     100         [ +  + ]:          3 :         if (_restrictionCode == CODE_ADDRESS_FROM_IS_SANCTIONED) {
-     101                 :          1 :             return TEXT_ADDRESS_FROM_IS_SANCTIONED;
-     102         [ +  + ]:          2 :         } else if (_restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED) {
-     103                 :          1 :             return TEXT_ADDRESS_TO_IS_SANCTIONED;
-     104                 :            :         } else {
-     105                 :          1 :             return TEXT_CODE_NOT_FOUND;
-     106                 :            :         }
-     107                 :            :     }
-     108                 :            : 
-     109                 :            :         /** 
-     110                 :            :     * @dev This surcharge is not necessary if you do not use the MetaTxModule
-     111                 :            :     */
-     112                 :            :     function _msgSender()
-     113                 :            :         internal
-     114                 :            :         view
-     115                 :            :         override(MetaTxModuleStandalone, Context)
-     116                 :            :         returns (address sender)
-     117                 :            :     {
-     118                 :          2 :         return MetaTxModuleStandalone._msgSender();
-     119                 :            :     }
-     120                 :            : 
-     121                 :            :     /** 
-     122                 :            :     * @dev This surcharge is not necessary if you do not use the MetaTxModule
-     123                 :            :     */
-     124                 :            :     function _msgData()
-     125                 :            :         internal
-     126                 :            :         view
-     127                 :            :         override(MetaTxModuleStandalone, Context)
-     128                 :            :         returns (bytes calldata)
-     129                 :            :     {
-     130                 :          0 :         return MetaTxModuleStandalone._msgData();
-     131                 :            :     }
-     132                 :            : }
-
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/src/rules/RuleWhitelist.sol.gcov.html b/doc/test/coverage/src/rules/RuleWhitelist.sol.gcov.html deleted file mode 100644 index 4bfa160..0000000 --- a/doc/test/coverage/src/rules/RuleWhitelist.sol.gcov.html +++ /dev/null @@ -1,304 +0,0 @@ - - - - - - - LCOV - lcov.info - src/rules/RuleWhitelist.sol - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - src/rules - RuleWhitelist.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:373897.4 %
Date:2023-11-21 13:10:43Functions:111291.7 %
Branches:141687.5 %
-
- - - - - - - - -

-
           Branch data     Line data    Source code
-
-       1                 :            : // SPDX-License-Identifier: MPL-2.0
-       2                 :            : 
-       3                 :            : pragma solidity ^0.8.20;
-       4                 :            : 
-       5                 :            : import "../../lib/openzeppelin-contracts/contracts/access/AccessControl.sol";
-       6                 :            : import "../../lib/CMTAT/contracts/mocks/RuleEngine/interfaces/IRule.sol";
-       7                 :            : import "./../modules/MetaTxModuleStandalone.sol";
-       8                 :            : import "./abstract/RuleWhitelistInvariantStorage.sol";
-       9                 :            : /**
-      10                 :            : @title a whitelist manager
-      11                 :            : */
-      12                 :            : 
-      13                 :            : contract RuleWhitelist is IRule, AccessControl, MetaTxModuleStandalone, RuleWhitelistInvariantStorage {
-      14                 :            :      mapping(address => bool) whitelist;
-      15                 :            :     // Number of addresses in the whitelist at the moment
-      16                 :            :     uint256 private numAddressesWhitelisted;
-      17                 :            :     
-      18                 :            :     /**
-      19                 :            :     * @param admin Address of the contract (Access Control)
-      20                 :            :     * @param forwarderIrrevocable Address of the forwarder, required for the gasless support
-      21                 :            :     */
-      22                 :            :     constructor(
-      23                 :            :         address admin,
-      24                 :            :         address forwarderIrrevocable
-      25                 :            :     ) MetaTxModuleStandalone(forwarderIrrevocable) {
-      26                 :            :         if(admin == address(0)){
-      27                 :            :             revert RuleWhitelist_AdminWithAddressZeroNotAllowed();
-      28                 :            :         }
-      29                 :            :         _grantRole(DEFAULT_ADMIN_ROLE, admin);
-      30                 :            :         _grantRole(WHITELIST_ROLE, admin);
-      31                 :            :     }
-      32                 :            : 
-      33                 :            :     /**
-      34                 :            :      * @notice Add addresses to the whitelist
-      35                 :            :      * If one of addresses already exist, there is no change for this address. The transaction remains valid (no revert).
-      36                 :            :      * @param listWhitelistedAddress an array with the addresses to whitelist
-      37                 :            :      */
-      38                 :            :     function addAddressesToTheWhitelist(
-      39                 :            :         address[] calldata listWhitelistedAddress
-      40                 :            :     ) public onlyRole(WHITELIST_ROLE) {
-      41                 :         12 :         uint256 numAddressesWhitelistedLocal = numAddressesWhitelisted;
-      42                 :         12 :         for (uint256 i = 0; i < listWhitelistedAddress.length; ) {
-      43         [ #  + ]:         26 :             if (!whitelist[listWhitelistedAddress[i]]) {
-      44                 :         24 :                 whitelist[listWhitelistedAddress[i]] = true;
-      45                 :         24 :                 ++numAddressesWhitelistedLocal;
-      46                 :            :             }
-      47                 :            :             unchecked {
-      48                 :         26 :                 ++i;
-      49                 :            :             }
-      50                 :            :         }
-      51                 :         12 :         numAddressesWhitelisted = numAddressesWhitelistedLocal;
-      52                 :            :     }
-      53                 :            : 
-      54                 :            :     /**
-      55                 :            :      * @notice Remove addresses from the whitelist
-      56                 :            :      * If the address does not exist in the whitelist, there is no change for this address. 
-      57                 :            :      * The transaction remains valid (no revert).
-      58                 :            :      * @param listWhitelistedAddress an array with the addresses to remove
-      59                 :            :      */
-      60                 :            :     function removeAddressesFromTheWhitelist(
-      61                 :            :         address[] calldata listWhitelistedAddress
-      62                 :            :     ) public onlyRole(WHITELIST_ROLE) {
-      63                 :          3 :         uint256 numAddressesWhitelistedLocal = numAddressesWhitelisted;
-      64                 :          3 :         for (uint256 i = 0; i < listWhitelistedAddress.length; ) {
-      65         [ #  + ]:          7 :             if (whitelist[listWhitelistedAddress[i]]) {
-      66                 :          6 :                 whitelist[listWhitelistedAddress[i]] = false;
-      67                 :          6 :                 --numAddressesWhitelistedLocal;
-      68                 :            :             }
-      69                 :            :             unchecked {
-      70                 :          7 :                 ++i;
-      71                 :            :             }
-      72                 :            :         }
-      73                 :          3 :         numAddressesWhitelisted = numAddressesWhitelistedLocal;
-      74                 :            :     }
-      75                 :            : 
-      76                 :            :     /**
-      77                 :            :      * @notice Add one address to the whitelist
-      78                 :            :      * If the address already exists, the transaction is reverted to save gas.
-      79                 :            :      * @param _newWhitelistAddress The address to whitelist
-      80                 :            :      */
-      81                 :            :     function addAddressToTheWhitelist(
-      82                 :            :         address _newWhitelistAddress
-      83                 :            :     ) public onlyRole(WHITELIST_ROLE) {
-      84         [ +  + ]:         21 :         if(whitelist[_newWhitelistAddress])
-      85                 :            :         {
-      86                 :          1 :             revert RuleWhitelist_AddressAlreadyWhitelisted();
-      87                 :            :         }
-      88                 :         20 :         whitelist[_newWhitelistAddress] = true;
-      89                 :         20 :         ++numAddressesWhitelisted;
-      90                 :            :     }
-      91                 :            : 
-      92                 :            :     /**
-      93                 :            :      * @notice Remove one address from the whitelist
-      94                 :            :      * If the address does not exist in the whitelist, the transaction is reverted to save gas.
-      95                 :            :      * @param _removeWhitelistAddress The address to remove
-      96                 :            :      *
-      97                 :            :      */
-      98                 :            :     function removeAddressFromTheWhitelist(
-      99                 :            :         address _removeWhitelistAddress
-     100                 :            :     ) public onlyRole(WHITELIST_ROLE) {
-     101         [ +  + ]:          2 :         if(!whitelist[_removeWhitelistAddress]){
-     102                 :          1 :             revert RuleWhitelist_AddressNotPresent();
-     103                 :            :         }
-     104                 :          1 :         whitelist[_removeWhitelistAddress] = false;
-     105                 :          1 :         --numAddressesWhitelisted;
-     106                 :            :     }
-     107                 :            : 
-     108                 :            :     /**
-     109                 :            :      * @notice Get the number of whitelisted addresses
-     110                 :            :      * @return Number of whitelisted addresses
-     111                 :            :      *
-     112                 :            :      */
-     113                 :            :     function numberWhitelistedAddress() external view returns (uint256) {
-     114                 :         20 :         return numAddressesWhitelisted;
-     115                 :            :     }
-     116                 :            : 
-     117                 :            :     /**
-     118                 :            :      * @notice Know if an address is whitelisted or not
-     119                 :            :      * @param _targetAddress The concerned address
-     120                 :            :      * @return True if the address is whitelisted, false otherwise
-     121                 :            :      *
-     122                 :            :      */
-     123                 :            :     function addressIsWhitelisted(
-     124                 :            :         address _targetAddress
-     125                 :            :     ) external view returns (bool) {
-     126                 :         50 :         return whitelist[_targetAddress];
-     127                 :            :     }
-     128                 :            : 
-     129                 :            :     /** 
-     130                 :            :     * @notice Validate a transfer
-     131                 :            :     * @param _from the origin address
-     132                 :            :     * @param _to the destination address
-     133                 :            :     * @param _amount to transfer
-     134                 :            :     * @return isValid => true if the transfer is valid, false otherwise
-     135                 :            :     **/
-     136                 :            :     function validateTransfer(
-     137                 :            :         address _from,
-     138                 :            :         address _to,
-     139                 :            :         uint256 _amount
-     140                 :            :     ) public view override returns (bool isValid) {
-     141                 :          3 :         return
-     142                 :            :             detectTransferRestriction(_from, _to, _amount) ==
-     143                 :            :             uint8(REJECTED_CODE_BASE.TRANSFER_OK);
-     144                 :            :     }
-     145                 :            : 
-     146                 :            :     /** 
-     147                 :            :     * @notice Check if an addres is in the whitelist or not
-     148                 :            :     * @param _from the origin address
-     149                 :            :     * @param _to the destination address
-     150                 :            :     * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK
-     151                 :            :     **/
-     152                 :            :     function detectTransferRestriction(
-     153                 :            :         address _from,
-     154                 :            :         address _to,
-     155                 :            :         uint256 /*_amount */
-     156                 :            :     ) public view override returns (uint8) {
-     157         [ +  + ]:         23 :         if (!whitelist[_from]) {
-     158                 :         11 :             return CODE_ADDRESS_FROM_NOT_WHITELISTED;
-     159         [ +  + ]:         12 :         } else if (!whitelist[_to]) {
-     160                 :          4 :             return CODE_ADDRESS_TO_NOT_WHITELISTED;
-     161                 :            :         } else {
-     162                 :          8 :             return uint8(REJECTED_CODE_BASE.TRANSFER_OK);
-     163                 :            :         }
-     164                 :            :     }
-     165                 :            : 
-     166                 :            :     /** 
-     167                 :            :     * @notice To know if the restriction code is valid for this rule or not.
-     168                 :            :     * @param _restrictionCode The target restriction code
-     169                 :            :     * @return true if the restriction code is known, false otherwise
-     170                 :            :     **/
-     171                 :            :     function canReturnTransferRestrictionCode(
-     172                 :            :         uint8 _restrictionCode
-     173                 :            :     ) external pure override returns (bool) {
-     174                 :          8 :         return
-     175                 :            :             _restrictionCode == CODE_ADDRESS_FROM_NOT_WHITELISTED ||
-     176                 :            :             _restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED;
-     177                 :            :     }
-     178                 :            : 
-     179                 :            :     /** 
-     180                 :            :     * @notice Return the corresponding message
-     181                 :            :     * @param _restrictionCode The target restriction code
-     182                 :            :     * @return true if the transfer is valid, false otherwise
-     183                 :            :     **/
-     184                 :            :     function messageForTransferRestriction(
-     185                 :            :         uint8 _restrictionCode
-     186                 :            :     ) external pure override returns (string memory) {
-     187         [ +  + ]:          7 :         if (_restrictionCode == CODE_ADDRESS_FROM_NOT_WHITELISTED) {
-     188                 :          4 :             return TEXT_ADDRESS_FROM_NOT_WHITELISTED;
-     189         [ +  + ]:          3 :         } else if (_restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED) {
-     190                 :          2 :             return TEXT_ADDRESS_TO_NOT_WHITELISTED;
-     191                 :            :         } else {
-     192                 :          1 :             return TEXT_CODE_NOT_FOUND;
-     193                 :            :         }
-     194                 :            :     }
-     195                 :            : 
-     196                 :            :     /** 
-     197                 :            :     * @dev This surcharge is not necessary if you do not use the MetaTxModule
-     198                 :            :     */
-     199                 :            :     function _msgSender()
-     200                 :            :         internal
-     201                 :            :         view
-     202                 :            :         override(MetaTxModuleStandalone, Context)
-     203                 :            :         returns (address sender)
-     204                 :            :     {
-     205                 :         52 :         return MetaTxModuleStandalone._msgSender();
-     206                 :            :     }
-     207                 :            : 
-     208                 :            :     /** 
-     209                 :            :     * @dev This surcharge is not necessary if you do not use the MetaTxModule
-     210                 :            :     */
-     211                 :            :     function _msgData()
-     212                 :            :         internal
-     213                 :            :         view
-     214                 :            :         override(MetaTxModuleStandalone, Context)
-     215                 :            :         returns (bytes calldata)
-     216                 :            :     {
-     217                 :          0 :         return MetaTxModuleStandalone._msgData();
-     218                 :            :     }
-     219                 :            : }
-
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/test/utils/index-sort-b.html b/doc/test/coverage/test/utils/index-sort-b.html deleted file mode 100644 index 9f67453..0000000 --- a/doc/test/coverage/test/utils/index-sort-b.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - LCOV - lcov.info - test/utils - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - test/utilsHitTotalCoverage
Test:lcov.infoLines:1333.3 %
Date:2023-11-21 13:10:43Functions:1333.3 %
Branches:00-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
SanctionListOracle.sol -
33.3%33.3%
-
33.3 %1 / 333.3 %1 / 3-0 / 0
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/test/utils/index-sort-f.html b/doc/test/coverage/test/utils/index-sort-f.html deleted file mode 100644 index f68895a..0000000 --- a/doc/test/coverage/test/utils/index-sort-f.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - LCOV - lcov.info - test/utils - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - test/utilsHitTotalCoverage
Test:lcov.infoLines:1333.3 %
Date:2023-11-21 13:10:43Functions:1333.3 %
Branches:00-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
SanctionListOracle.sol -
33.3%33.3%
-
33.3 %1 / 333.3 %1 / 3-0 / 0
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/test/utils/index-sort-l.html b/doc/test/coverage/test/utils/index-sort-l.html deleted file mode 100644 index 811a542..0000000 --- a/doc/test/coverage/test/utils/index-sort-l.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - LCOV - lcov.info - test/utils - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - test/utilsHitTotalCoverage
Test:lcov.infoLines:1333.3 %
Date:2023-11-21 13:10:43Functions:1333.3 %
Branches:00-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
SanctionListOracle.sol -
33.3%33.3%
-
33.3 %1 / 333.3 %1 / 3-0 / 0
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/coverage/test/utils/index.html b/doc/test/coverage/test/utils/index.html deleted file mode 100644 index 2812067..0000000 --- a/doc/test/coverage/test/utils/index.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - LCOV - lcov.info - test/utils - - - - - - - - - - - - - - -
LCOV - code coverage report
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Current view:top level - test/utilsHitTotalCoverage
Test:lcov.infoLines:1333.3 %
Date:2023-11-21 13:10:43Functions:1333.3 %
Branches:00-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Filename Sort by nameLine Coverage Sort by line coverageFunctions Sort by function coverageBranches Sort by branch coverage
SanctionListOracle.sol -
33.3%33.3%
-
33.3 %1 / 333.3 %1 / 3-0 / 0
-
-
- - - - -
Generated by: LCOV version 1.16
-
- - - diff --git a/doc/test/lcov.info b/doc/test/lcov.info deleted file mode 100644 index 5bdcc2d..0000000 --- a/doc/test/lcov.info +++ /dev/null @@ -1,325 +0,0 @@ -TN: -SF:script/CMTATWithRuleEngineScript.s.sol -FN:14,CMTATWithRuleEngineScript.run -FNDA:0,CMTATWithRuleEngineScript.run -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:24,0 -DA:37,0 -DA:39,0 -DA:43,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:50,0 -FNF:1 -FNH:0 -LF:16 -LH:0 -BRF:0 -BRH:0 -end_of_record -TN: -SF:script/RuleEngineScript.s.sol -FN:16,RuleEngineScript.run -FNDA:0,RuleEngineScript.run -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:23,0 -DA:24,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:30,0 -DA:33,0 -BRDA:33,0,0,- -BRDA:33,0,1,- -DA:34,0 -FNF:1 -FNH:0 -LF:12 -LH:0 -BRF:2 -BRH:0 -end_of_record -TN: -SF:src/RuleEngine.sol -FN:53,RuleEngine.setRules -FNDA:10,RuleEngine.setRules -DA:56,9 -BRDA:56,0,0,1 -BRDA:56,0,1,8 -DA:57,1 -DA:59,8 -DA:60,15 -BRDA:60,1,0,1 -BRDA:60,1,1,14 -DA:61,1 -DA:63,14 -BRDA:63,2,0,1 -BRDA:63,2,1,13 -DA:64,1 -DA:66,13 -DA:67,13 -DA:69,13 -DA:72,6 -FN:79,RuleEngine.clearRules -FNDA:3,RuleEngine.clearRules -DA:80,2 -DA:81,2 -FN:89,RuleEngine.addRule -FNDA:9,RuleEngine.addRule -DA:90,8 -BRDA:90,3,0,1 -BRDA:90,3,1,7 -DA:92,1 -DA:94,7 -BRDA:94,4,0,1 -BRDA:94,4,1,6 -DA:96,1 -DA:98,6 -DA:99,6 -DA:100,6 -FN:113,RuleEngine.removeRule -FNDA:6,RuleEngine.removeRule -DA:117,5 -BRDA:117,5,0,1 -BRDA:117,5,1,4 -DA:119,1 -DA:121,4 -BRDA:121,6,0,- -BRDA:121,6,1,2 -DA:122,2 -DA:124,4 -DA:125,4 -DA:126,4 -FN:132,RuleEngine.rulesCount -FNDA:20,RuleEngine.rulesCount -DA:133,20 -FN:140,RuleEngine.getRuleIndex -FNDA:3,RuleEngine.getRuleIndex -DA:141,0 -DA:142,5 -BRDA:142,7,0,3 -BRDA:142,7,1,3 -DA:143,5 -DA:146,3 -DA:149,1 -FN:157,RuleEngine.rule -FNDA:1,RuleEngine.rule -DA:158,1 -FN:165,RuleEngine.rules -FNDA:4,RuleEngine.rules -DA:166,4 -FN:176,RuleEngine.detectTransferRestriction -FNDA:7,RuleEngine.detectTransferRestriction -DA:181,17 -DA:182,17 -DA:187,17 -BRDA:187,8,0,12 -BRDA:187,8,1,5 -DA:188,12 -DA:191,5 -DA:194,5 -FN:204,RuleEngine.validateTransfer -FNDA:10,RuleEngine.validateTransfer -DA:209,10 -FN:217,RuleEngine.messageForTransferRestriction -FNDA:6,RuleEngine.messageForTransferRestriction -DA:220,6 -DA:221,5 -BRDA:221,9,0,- -BRDA:221,9,1,4 -DA:222,4 -DA:226,1 -DA:229,2 -FN:235,RuleEngine._msgSender -FNDA:38,RuleEngine._msgSender -DA:241,38 -FN:247,RuleEngine._msgData -FNDA:0,RuleEngine._msgData -DA:253,0 -FNF:13 -FNH:12 -LF:49 -LH:47 -BRF:20 -BRH:18 -end_of_record -TN: -SF:src/modules/MetaTxModuleStandalone.sol -FN:15,MetaTxModuleStandalone._msgSender -FNDA:92,MetaTxModuleStandalone._msgSender -DA:22,92 -FN:25,MetaTxModuleStandalone._msgData -FNDA:0,MetaTxModuleStandalone._msgData -DA:32,0 -FNF:2 -FNH:1 -LF:2 -LH:1 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/rules/RuleSanctionList.sol -FN:35,RuleSanctionList.setOracle -FNDA:2,RuleSanctionList.setOracle -DA:38,1 -FN:48,RuleSanctionList.validateTransfer -FNDA:4,RuleSanctionList.validateTransfer -DA:53,4 -FN:64,RuleSanctionList.detectTransferRestriction -FNDA:3,RuleSanctionList.detectTransferRestriction -DA:69,7 -BRDA:69,0,0,2 -BRDA:69,0,1,3 -DA:70,7 -BRDA:70,1,0,2 -BRDA:70,1,1,5 -DA:71,2 -DA:72,5 -BRDA:72,2,0,2 -BRDA:72,2,1,3 -DA:73,2 -DA:76,3 -FN:84,RuleSanctionList.canReturnTransferRestrictionCode -FNDA:3,RuleSanctionList.canReturnTransferRestrictionCode -DA:87,3 -FN:97,RuleSanctionList.messageForTransferRestriction -FNDA:3,RuleSanctionList.messageForTransferRestriction -DA:100,3 -BRDA:100,3,0,1 -BRDA:100,3,1,2 -DA:101,1 -DA:102,2 -BRDA:102,4,0,1 -BRDA:102,4,1,1 -DA:103,1 -DA:105,1 -FN:112,RuleSanctionList._msgSender -FNDA:2,RuleSanctionList._msgSender -DA:118,2 -FN:124,RuleSanctionList._msgData -FNDA:0,RuleSanctionList._msgData -DA:130,0 -FNF:7 -FNH:6 -LF:16 -LH:15 -BRF:10 -BRH:10 -end_of_record -TN: -SF:src/rules/RuleWhitelist.sol -FN:38,RuleWhitelist.addAddressesToTheWhitelist -FNDA:13,RuleWhitelist.addAddressesToTheWhitelist -DA:41,12 -DA:42,12 -DA:43,26 -BRDA:43,0,0,- -BRDA:43,0,1,24 -DA:44,24 -DA:45,24 -DA:48,26 -DA:51,12 -FN:60,RuleWhitelist.removeAddressesFromTheWhitelist -FNDA:4,RuleWhitelist.removeAddressesFromTheWhitelist -DA:63,3 -DA:64,3 -DA:65,7 -BRDA:65,1,0,- -BRDA:65,1,1,6 -DA:66,6 -DA:67,6 -DA:70,7 -DA:73,3 -FN:81,RuleWhitelist.addAddressToTheWhitelist -FNDA:22,RuleWhitelist.addAddressToTheWhitelist -DA:84,21 -BRDA:84,2,0,1 -BRDA:84,2,1,20 -DA:86,1 -DA:88,20 -DA:89,20 -FN:98,RuleWhitelist.removeAddressFromTheWhitelist -FNDA:3,RuleWhitelist.removeAddressFromTheWhitelist -DA:101,2 -BRDA:101,3,0,1 -BRDA:101,3,1,1 -DA:102,1 -DA:104,1 -DA:105,1 -FN:113,RuleWhitelist.numberWhitelistedAddress -FNDA:20,RuleWhitelist.numberWhitelistedAddress -DA:114,20 -FN:123,RuleWhitelist.addressIsWhitelisted -FNDA:50,RuleWhitelist.addressIsWhitelisted -DA:126,50 -FN:136,RuleWhitelist.validateTransfer -FNDA:3,RuleWhitelist.validateTransfer -DA:141,3 -FN:152,RuleWhitelist.detectTransferRestriction -FNDA:20,RuleWhitelist.detectTransferRestriction -DA:157,23 -BRDA:157,4,0,11 -BRDA:157,4,1,12 -DA:158,11 -DA:159,12 -BRDA:159,5,0,4 -BRDA:159,5,1,8 -DA:160,4 -DA:162,8 -FN:171,RuleWhitelist.canReturnTransferRestrictionCode -FNDA:8,RuleWhitelist.canReturnTransferRestrictionCode -DA:174,8 -FN:184,RuleWhitelist.messageForTransferRestriction -FNDA:7,RuleWhitelist.messageForTransferRestriction -DA:187,7 -BRDA:187,6,0,4 -BRDA:187,6,1,3 -DA:188,4 -DA:189,3 -BRDA:189,7,0,2 -BRDA:189,7,1,1 -DA:190,2 -DA:192,1 -FN:199,RuleWhitelist._msgSender -FNDA:52,RuleWhitelist._msgSender -DA:205,52 -FN:211,RuleWhitelist._msgData -FNDA:0,RuleWhitelist._msgData -DA:217,0 -FNF:12 -FNH:11 -LF:38 -LH:37 -BRF:16 -BRH:14 -end_of_record -TN: -SF:test/utils/SanctionListOracle.sol -FN:15,SanctionListOracle.addToSanctionsList -FNDA:0,SanctionListOracle.addToSanctionsList -DA:16,0 -FN:19,SanctionListOracle.removeFromSanctionsList -FNDA:0,SanctionListOracle.removeFromSanctionsList -DA:20,0 -FN:23,SanctionListOracle.isSanctioned -FNDA:12,SanctionListOracle.isSanctioned -DA:24,12 -FNF:3 -FNH:1 -LF:3 -LH:1 -BRF:0 -BRH:0 -end_of_record diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..95e5b73 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,23 @@ +{ + "lib/CMTAT": { + "rev": "23a1e59f913d079d0c09d32fafbd95ab2d426093" + }, + "lib/RuleEngine": { + "rev": "a5516c930c3868f142dece919851ce0c94a18f5a" + }, + "lib/SnapshotEngine": { + "tag": { + "name": "v0.5.0", + "rev": "aa089353605cd1b0e555d22b62aa4fbeaae7df25" + } + }, + "lib/openzeppelin-contracts": { + "rev": "dbb6104ce834628e473d2173bbc9d47f81a9eec3" + }, + "lib/openzeppelin-contracts-upgradeable": { + "rev": "723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1" + }, + "lib/openzeppelin-foundry-upgrades": { + "rev": "7c669276d1c6a8b79f9d7591b9a994ab30abe450" + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 039b692..d0ef077 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,13 +1,22 @@ [profile.default] -solc = "0.8.22" +solc = "0.8.36" src = 'src' out = 'out' libs = ['lib'] optimizer = true optimizer_runs = 200 -evm_version = 'london' +evm_version = 'prague' #OpenZeppelin foundry upgrades build_info = true extra_output = ["storageLayout"] +ffi = true +ast = true fs_permissions = [{ access = "read", path = "./out"}] # See more config options https://github.com/foundry-rs/foundry/tree/master/config + +[invariant] +runs = 48 +depth = 64 +# The handler swallows the reverts that are legitimate (claiming outside the window, while paused or +# while frozen), so a revert reaching the runner is a real problem and should fail the run. +fail_on_revert = true diff --git a/hardhat.config.js b/hardhat.config.js index 132197f..828771f 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -1,13 +1,28 @@ /** @type import('hardhat/config').HardhatUserConfig */ require("@nomicfoundation/hardhat-foundry"); require('solidity-docgen'); + +// Only used to generate doc/solidityAPI via `npx hardhat docgen`; the build and the +// tests run under Foundry. The solc settings mirror foundry.toml so the generated +// API reflects the contracts as they are actually compiled. +// +// NOTE: `settings` must sit INSIDE `solidity`. Left at the top level it is silently +// ignored, the optimizer never runs, and docgen reports a spurious contract-size +// warning for contracts that are within the limit under Foundry. module.exports = { - solidity: "0.8.22", - settings: { - optimizer: { - enabled: true, - runs: 200 - }, - evmVersion:"london" + solidity: { + version: "0.8.36", + settings: { + optimizer: { + enabled: true, + runs: 200 + }, + evmVersion: "prague" + } + }, + docgen: { + // written straight to its committed location, so regenerating needs no manual move + outputDir: 'doc/solidityAPI', + pages: 'single' } }; diff --git a/lib/CMTAT b/lib/CMTAT index 23a1e59..658672f 160000 --- a/lib/CMTAT +++ b/lib/CMTAT @@ -1 +1 @@ -Subproject commit 23a1e59f913d079d0c09d32fafbd95ab2d426093 +Subproject commit 658672f190d56d3f61663a7d6d51962b8980df70 diff --git a/lib/RuleEngine b/lib/RuleEngine index a5516c9..ab9def2 160000 --- a/lib/RuleEngine +++ b/lib/RuleEngine @@ -1 +1 @@ -Subproject commit a5516c930c3868f142dece919851ce0c94a18f5a +Subproject commit ab9def2f19ae71af304127f42d20d9831cad1a2b diff --git a/lib/SnapshotEngine b/lib/SnapshotEngine new file mode 160000 index 0000000..aa08935 --- /dev/null +++ b/lib/SnapshotEngine @@ -0,0 +1 @@ +Subproject commit aa089353605cd1b0e555d22b62aa4fbeaae7df25 diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..620536f --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 620536fa5277db4e3fd46772d5cbc1ea0696fb43 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index dbb6104..cab1993 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 +Subproject commit cab19933c33c2ad1d4c7a84864a3601dddfd16f3 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..14f52c5 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 14f52c54d3a1eefbda3d4071efba24d3c1e07e8a diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades index 7c66927..8ddceb0 160000 --- a/lib/openzeppelin-foundry-upgrades +++ b/lib/openzeppelin-foundry-upgrades @@ -1 +1 @@ -Subproject commit 7c669276d1c6a8b79f9d7591b9a994ab30abe450 +Subproject commit 8ddceb0c0d94f7f9420fee1ee30fffbdacadf08f diff --git a/package-lock.json b/package-lock.json index f3657e6..06a9e9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,65 @@ { - "name": "debtPayment", + "name": "IncomeVault", "lockfileVersion": 2, "requires": true, "packages": { "": { "dependencies": { - "@openzeppelin/upgrades-core": "^1.32.5" + "@openzeppelin/upgrades-core": "^1.46.0" }, "devDependencies": { "@nomicfoundation/hardhat-foundry": "^1.0.1", "ethlint": "^1.2.5", "prettier-plugin-solidity": "^1.0.0-rc.1", - "sol2uml": "^2.2.6", + "sol2uml": "^2.5.26", "solidity-docgen": "^0.6.0-beta.35", - "surya": "^0.4.6" + "surya": "^0.4.13" } }, + "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/@aduh95/viz.js": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/@aduh95/viz.js/-/viz.js-3.7.0.tgz", "integrity": "sha512-20Pk2Z98fbPLkECcrZSJszKos/OgtvJJR3NcbVfgCJ6EQjDNzW2P1BKqImOz3tJ952dvO2DWEhcLhQ1Wz1e9ng==", "dev": true }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "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==", + "license": "(Apache-2.0 WITH LLVM-exception)" + }, "node_modules/@chainsafe/as-sha256": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz", @@ -66,6 +104,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", @@ -93,6 +132,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -118,6 +158,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", @@ -141,6 +182,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -164,6 +206,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0" } @@ -183,6 +226,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/properties": "^5.7.0" @@ -203,6 +247,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0", @@ -224,6 +269,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/logger": "^5.7.0" } @@ -243,6 +289,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bignumber": "^5.7.0" } @@ -262,6 +309,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/abstract-provider": "^5.7.0", @@ -290,6 +338,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/address": "^5.7.0", @@ -317,6 +366,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/basex": "^5.7.0", @@ -347,6 +397,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/address": "^5.7.0", @@ -378,6 +429,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "js-sha3": "0.8.0" @@ -397,7 +449,8 @@ "type": "individual", "url": "https://www.buymeacoffee.com/ricmoo" } - ] + ], + "peer": true }, "node_modules/@ethersproject/networks": { "version": "5.7.1", @@ -414,6 +467,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/logger": "^5.7.0" } @@ -433,6 +487,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/sha2": "^5.7.0" @@ -453,6 +508,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/logger": "^5.7.0" } @@ -472,6 +528,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0", @@ -510,6 +567,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0" @@ -530,6 +588,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0" @@ -550,6 +609,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0", @@ -571,6 +631,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0", @@ -595,6 +656,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -619,6 +681,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/constants": "^5.7.0", @@ -640,6 +703,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", @@ -667,6 +731,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/constants": "^5.7.0", @@ -688,6 +753,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0", @@ -721,6 +787,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/base64": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -744,6 +811,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/hash": "^5.7.0", @@ -769,6 +837,32 @@ "node": ">=12.0.0" } }, + "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/curves/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/hashes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", @@ -1334,6 +1428,15 @@ "node": ">=4" } }, + "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==", + "license": "MIT", + "dependencies": { + "@bytecodealliance/preview2-shim": "0.17.0" + } + }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", @@ -1527,18 +1630,22 @@ } }, "node_modules/@openzeppelin/upgrades-core": { - "version": "1.32.5", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.32.5.tgz", - "integrity": "sha512-R0wprsyJ4xWiRW05kaTfZZkRVpG2g0af3/hpjE7t2mX0Eb2n40MQLokTwqIk4LDzpp910JfLSpB0vBuZ6WNPog==", - "dependencies": { - "cbor": "^9.0.0", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.46.0.tgz", + "integrity": "sha512-UFSeO/4r8eeXj0C/HAwV+J4b72sE1HX0aALQFs5S2RBOsfXvKweyjQf35vrK32LQiyHdP6IPShAsEBVvpSEgGQ==", + "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": "^10.2.5", "minimist": "^1.2.7", "proper-lockfile": "^4.1.1", - "solidity-ast": "^0.4.51" + "solidity-ast": "^0.4.60" }, "bin": { "openzeppelin-upgrades-core": "dist/cli/cli.js" @@ -1589,6 +1696,183 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/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/@puppeteer/browsers/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/@puppeteer/browsers/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/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/@puppeteer/browsers/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/@puppeteer/browsers/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/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/@puppeteer/browsers/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/@puppeteer/browsers/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/@puppeteer/browsers/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/@puppeteer/browsers/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "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/@puppeteer/browsers/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/@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -1755,6 +2039,13 @@ "antlr4ts": "^0.5.0-alpha.4" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bn.js": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz", @@ -1786,9 +2077,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } }, "node_modules/@types/pbkdf2": { "version": "3.1.0", @@ -1818,10 +2113,11 @@ } }, "node_modules/@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" @@ -1922,7 +2218,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/agent-base": { "version": "6.0.2", @@ -2031,8 +2328,7 @@ "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, - "peer": true + "dev": true }, "node_modules/arr-diff": { "version": "2.0.0", @@ -2064,21 +2360,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-unique": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", @@ -2088,53 +2369,34 @@ "node": ">=0.10.0" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.4.tgz", - "integrity": "sha512-BMtLxpV+8BD+6ZPFIWmnUBpQoy+A+ujcg4rhp2iwCRJYA7PEh2MS4NL3lz8EiDlLrJPp2hg9qWihr5pd//jcGw==", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "tslib": "^2.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=4" } }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "node_modules/ast-types/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, - "engines": { - "node": ">=0.10.0" - } + "license": "0BSD" }, "node_modules/async-each": { "version": "1.0.3", @@ -2146,7 +2408,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/atob": { "version": "2.1.2", @@ -2160,29 +2423,17 @@ "node": ">= 4.5.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==", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "dev": true, + "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/axios-debug-log": { @@ -2198,13 +2449,113 @@ "axios": ">=1.0.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 - }, - "node_modules/base": { + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "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 + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", @@ -2269,13 +2620,25 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "peer": true + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } }, "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 + "dev": true, + "peer": true }, "node_modules/bigint-crypto-utils": { "version": "3.2.2", @@ -2287,6 +2650,15 @@ "node": ">=14.0.0" } }, + "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==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", @@ -2306,31 +2678,6 @@ "file-uri-to-path": "1.0.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/blakejs": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", @@ -2345,15 +2692,28 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2425,35 +2785,12 @@ "safe-buffer": "^5.1.2" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "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" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -2532,6 +2869,8 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "peer": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2546,6 +2885,30 @@ "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", @@ -2576,14 +2939,15 @@ } }, "node_modules/cbor": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", - "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "version": "10.0.12", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.12.tgz", + "integrity": "sha512-exQDevYd7ZQLP4moMQcZkKCVZsXLAtUSflObr3xTh4xzFIv/xBCdvCd6L259kQOUP2kcTC0jvC6PpZIf/WmRXA==", + "license": "MIT", "dependencies": { - "nofilter": "^3.1.0" + "nofilter": "^3.0.2" }, "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/chalk": { @@ -2602,21 +2966,26 @@ } }, "node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "dev": true, + "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">=20.18.1" }, "funding": { "url": "https://github.com/cheeriojs/cheerio?sponsor=1" @@ -2627,6 +2996,7 @@ "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", @@ -2639,6 +3009,16 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cheerio/node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", @@ -2659,11 +3039,19 @@ "fsevents": "^1.0.0" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } }, "node_modules/ci-info": { "version": "2.0.0", @@ -2793,13 +3181,14 @@ } }, "node_modules/cli-color": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz", - "integrity": "sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", "dev": true, + "license": "ISC", "dependencies": { "d": "^1.0.1", - "es5-ext": "^0.10.61", + "es5-ext": "^0.10.64", "es6-iterator": "^2.0.3", "memoizee": "^0.4.15", "timers-ext": "^0.1.7" @@ -2881,6 +3270,7 @@ "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" }, @@ -2919,9 +3309,9 @@ "dev": true }, "node_modules/convert-svg-core": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/convert-svg-core/-/convert-svg-core-0.6.4.tgz", - "integrity": "sha512-8mS0n7otc1lljTte4z7nDhihEakKCRq4w5ivMnIGeOZuD/OV/eDZNNEgGLV1ET3p+rMbnrZnX4lAcsf14WzD5w==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/convert-svg-core/-/convert-svg-core-0.7.1.tgz", + "integrity": "sha512-qlQlT2pHMCG0NmZsh2yuYNYO9zKbOmHoWPT+ibuvpVjvA7l9aNhHS4debQeZGuR0mA4x/0a38zOTqBkkdYoTXQ==", "dev": true, "funding": [ { @@ -2933,36 +3323,21 @@ "url": "https://www.patreon.com/neocotic" } ], + "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "cheerio": "^1.0.0-rc.11", - "commander": "^9.2.0", - "file-url": "^3.0.0", - "get-stdin": "^8.0.0", - "glob": "^8.0.1", - "lodash.omit": "^4.5.0", - "lodash.pick": "^4.4.0", - "pollock": "^0.2.0", - "puppeteer": "^13.7.0", - "tmp": "^0.2.1" + "cheerio": "^1.1.0", + "file-url": "^4.0.0", + "puppeteer-core": "^24.10.1", + "tmp": "^0.2.3" }, "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/convert-svg-core/node_modules/commander": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", - "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", - "dev": true, - "engines": { - "node": "^12.20.0 || >=14" + "node": ">=22" } }, "node_modules/convert-svg-to-png": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/convert-svg-to-png/-/convert-svg-to-png-0.6.4.tgz", - "integrity": "sha512-zHNTuVedkyuhMl+f+HMm2L7+TKDYCKFAqAmDqUr0dN7/xtgYe76PPAydjlFzeLbzEpGtEfhaA15q+ejpLaVo3g==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/convert-svg-to-png/-/convert-svg-to-png-0.7.1.tgz", + "integrity": "sha512-XgLC/EmK0/GvdaHpCpEHCHL/ty/TDeezk8+AKWmUfEgUrYiwR9Tqrih9zfVWVzQYvn8mtjLvROv9xRQ7FHBo/Q==", "dev": true, "funding": [ { @@ -2974,14 +3349,12 @@ "url": "https://www.patreon.com/neocotic" } ], + "license": "MIT", "dependencies": { - "convert-svg-core": "^0.6.4" - }, - "bin": { - "convert-svg-to-png": "bin/convert-svg-to-png" + "convert-svg-core": "^0.7.1" }, "engines": { - "node": "^12.20.0 || >=14" + "node": ">=22" } }, "node_modules/cookie": { @@ -3009,6 +3382,33 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -3047,15 +3447,6 @@ "sha.js": "^2.4.8" } }, - "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, - "dependencies": { - "node-fetch": "2.6.7" - } - }, "node_modules/cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -3084,10 +3475,11 @@ "dev": true }, "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -3100,10 +3492,11 @@ } }, "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -3112,61 +3505,27 @@ } }, "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "dev": true, + "license": "ISC", "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "es5-ext": "^0.10.64", + "type": "^2.7.2" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.12" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 14" } }, "node_modules/debug": { @@ -3207,6 +3566,8 @@ "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, + "peer": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3219,22 +3580,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -3257,11 +3602,27 @@ "node": ">=0.10.0" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "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" } @@ -3277,10 +3638,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.981744", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.981744.tgz", - "integrity": "sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==", - "dev": true + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "3.5.0", @@ -3302,6 +3664,7 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -3321,13 +3684,15 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -3339,19 +3704,35 @@ } }, "node_modules/domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" + "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "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==", + "dev": true, + "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/elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", @@ -3375,8 +3756,34 @@ "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, - "peer": true + "dev": true + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/end-of-stream": { "version": "1.4.4", @@ -3401,10 +3808,11 @@ } }, "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -3417,7 +3825,6 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, - "peer": true, "engines": { "node": ">=6" } @@ -3428,72 +3835,22 @@ "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", "dev": true }, - "node_modules/es-abstract": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz", - "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.5", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3502,14 +3859,17 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "engines": { "node": ">= 0.4" } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -3518,51 +3878,32 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "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": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "hasInstallScript": true, + "license": "ISC", "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -3574,6 +3915,7 @@ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "dev": true, + "license": "MIT", "dependencies": { "d": "1", "es5-ext": "^0.10.35", @@ -3581,13 +3923,17 @@ } }, "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "dev": true, + "license": "ISC", "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/es6-weak-map": { @@ -3595,6 +3941,7 @@ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", "dev": true, + "license": "ISC", "dependencies": { "d": "1", "es5-ext": "^0.10.46", @@ -3607,7 +3954,6 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true, - "peer": true, "engines": { "node": ">=6" } @@ -3621,6 +3967,89 @@ "node": ">=0.8.0" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/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/ethereum-cryptography": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", @@ -3724,6 +4153,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "peer": true, "dependencies": { "@ethersproject/abi": "5.7.0", "@ethersproject/abstract-provider": "5.7.0", @@ -3801,6 +4231,7 @@ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", "dev": true, + "license": "MIT", "dependencies": { "d": "1", "es5-ext": "~0.10.14" @@ -3816,6 +4247,16 @@ "node": ">=6" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -3881,16 +4322,11 @@ "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dev": true, + "license": "ISC", "dependencies": { "type": "^2.7.2" } }, - "node_modules/ext/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", - "dev": true - }, "node_modules/extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -3933,6 +4369,7 @@ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -3954,6 +4391,13 @@ "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "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", @@ -3965,6 +4409,7 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, + "license": "MIT", "dependencies": { "pend": "~1.2.0" } @@ -3977,12 +4422,16 @@ "optional": true }, "node_modules/file-url": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/file-url/-/file-url-3.0.0.tgz", - "integrity": "sha512-g872QGsHexznxkIAdK8UiZRe7SkE6kvylShU4Nsj8NvfvZag7S0QuQ4IgvPDkk75HxgjIVDwycFTDAgIiO4nDA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-4.0.0.tgz", + "integrity": "sha512-vRCdScQ6j3Ku6Kd7W1kZk9c++5SqD6Xz5Jotrjr/nkY714M14RFHy/AAVA2WQvpsqVAVgTbDrYyBpU205F0cLw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/filename-regex": { @@ -4010,19 +4459,6 @@ "node": ">=0.10.0" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -4034,9 +4470,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -4044,6 +4480,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -4053,14 +4490,6 @@ } } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -4083,14 +4512,17 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -4115,12 +4547,6 @@ "node": ">=0.10.0" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -4165,23 +4591,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4193,14 +4603,6 @@ "dev": true, "peer": true }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -4208,15 +4610,22 @@ "dev": true }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "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", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "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" @@ -4225,16 +4634,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "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==", "dev": true, - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.4" } }, "node_modules/get-stream": { @@ -4242,6 +4653,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -4252,20 +4664,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 14" } }, "node_modules/get-value": { @@ -4277,25 +4688,6 @@ "node": ">=0.10.0" } }, - "node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "dev": true, - "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/glob-base": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", @@ -4318,13 +4710,12 @@ "is-glob": "^2.0.0" } }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dependencies": { - "define-properties": "^1.1.3" - }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4332,17 +4723,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -5244,14 +5624,6 @@ "node": ">=10" } }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5264,6 +5636,8 @@ "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, + "peer": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -5271,21 +5645,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5297,6 +5662,8 @@ "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" }, @@ -5433,28 +5800,70 @@ "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==", + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==", + "node_modules/hasha/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "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==", "dependencies": { @@ -5464,9 +5873,9 @@ } }, "node_modules/htmlparser2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -5475,11 +5884,25 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "entities": "^4.3.0" + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/http-errors": { @@ -5499,6 +5922,30 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -5543,7 +5990,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "peer": true }, "node_modules/immutable": { "version": "4.3.0", @@ -5552,6 +6000,23 @@ "dev": true, "peer": true }, + "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/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -5577,19 +6042,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -5609,6 +6061,16 @@ "fp-ts": "^1.0.0" } }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", @@ -5630,31 +6092,12 @@ "node": ">=0.10.0" } }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "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": "1.0.1", @@ -5668,38 +6111,12 @@ "node": ">=0.10.0" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "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==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", @@ -5721,34 +6138,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", @@ -5843,17 +6232,6 @@ "npm": ">=3" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", @@ -5866,20 +6244,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -5933,36 +6297,8 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "dev": true, + "license": "MIT" }, "node_modules/is-stream": { "version": "1.1.0", @@ -5973,48 +6309,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "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", @@ -6028,17 +6322,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -6096,7 +6379,8 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "dev": true + "dev": true, + "peer": true }, "node_modules/js-string-escape": { "version": "1.0.1", @@ -6107,12 +6391,18 @@ "node": ">= 0.8" } }, + "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.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -6120,6 +6410,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "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.3.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", @@ -6263,17 +6560,12 @@ "ieee754": "^1.2.1" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "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, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/lodash": { "version": "4.17.21", @@ -6281,18 +6573,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", - "dev": true - }, - "node_modules/lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6318,15 +6598,13 @@ "peer": true }, "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/lru-queue": { @@ -6334,22 +6612,11 @@ "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", "dev": true, + "license": "MIT", "dependencies": { "es5-ext": "~0.10.2" } }, - "node_modules/map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -6371,10 +6638,20 @@ "node": ">=0.10.0" } }, - "node_modules/math-random": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", - "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", "dev": true }, "node_modules/mcl-wasm": { @@ -6410,19 +6687,23 @@ } }, "node_modules/memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", "dev": true, + "license": "ISC", "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", + "d": "^1.0.2", + "es5-ext": "^0.10.64", "es6-weak-map": "^2.0.3", "event-emitter": "^0.3.5", "is-promise": "^2.2.2", "lru-queue": "^0.1.0", "next-tick": "^1.1.0", "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/memory-level": { @@ -6479,6 +6760,7 @@ "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" } @@ -6488,6 +6770,7 @@ "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" }, @@ -6515,15 +6798,18 @@ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, "node_modules/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", - "dev": true, + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -6532,6 +6818,13 @@ "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", "dev": true }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, "node_modules/mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -6570,12 +6863,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, "node_modules/mnemonist": { "version": "0.38.5", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", @@ -6798,43 +7085,28 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/node-addon-api": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "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.6.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", @@ -6849,6 +7121,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "license": "MIT", "engines": { "node": ">=12.19" } @@ -6882,6 +7155,7 @@ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -6975,18 +7249,12 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -7008,23 +7276,6 @@ "node": ">=0.10.0" } }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.omit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", @@ -7099,15 +7350,6 @@ "node": ">=0.10.0" } }, - "node_modules/p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -7117,63 +7359,89 @@ "node": ">=4" } }, - "node_modules/p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "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, + "peer": true, "dependencies": { - "p-try": "^2.0.0" + "aggregate-error": "^3.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", "dev": true, + "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" }, "engines": { - "node": ">=8" + "node": ">= 14" } }, - "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==", + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "peer": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", "dependencies": { - "aggregate-error": "^3.0.0" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=10" + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 14" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "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" } @@ -7193,31 +7461,78 @@ "node": ">=0.10.0" } }, + "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/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, + "license": "MIT", "dependencies": { - "entities": "^4.4.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", - "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", "dependencies": { - "domhandler": "^5.0.2", "parse5": "^7.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -7227,15 +7542,6 @@ "node": ">=0.10.0" } }, - "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, - "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", @@ -7292,7 +7598,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "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", @@ -7307,24 +7621,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pollock": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/pollock/-/pollock-0.2.1.tgz", - "integrity": "sha512-2Xy6LImSXm0ANKv9BKSVuCa6Z4ACbK7oUrl9gtUgqLkekL7n9C0mlWsOGYYuGbCG8xT0x3Q4F31C3ZMyVQjwsg==", - "dev": true - }, "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -7334,14 +7630,6 @@ "node": ">=0.10.0" } }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/preserve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", @@ -7395,6 +7683,7 @@ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -7409,11 +7698,66 @@ "signal-exit": "^3.0.2" } }, - "node_modules/proxy-from-env": { + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/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 + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pseudomap": { "version": "1.0.2", @@ -7432,41 +7776,83 @@ } }, "node_modules/puppeteer": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-13.7.0.tgz", - "integrity": "sha512-U1uufzBjz3+PkpCxFrWzh4OrMIdIb2ztzCu0YEPfRHjHswcSwHZswnK+WdsOQJsRV8WeTg3jLhJR4D867+fjsA==", - "deprecated": "< 18.1.0 is no longer supported", + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", + "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.981744", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "pkg-dir": "4.2.0", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.5.0" + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1608973", + "puppeteer-core": "24.43.1", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" }, "engines": { - "node": ">=10.18.1" + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", + "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/puppeteer/node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "node_modules/puppeteer-core/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -7939,23 +8325,6 @@ "node": ">=0.10.0" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -8018,6 +8387,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -8042,67 +8421,10 @@ "node": ">= 4" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "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/rimraf/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, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -8150,28 +8472,6 @@ "dev": true, "peer": true }, - "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -8186,28 +8486,11 @@ "ret": "~0.1.10" } }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/scrypt-js": { "version": "3.0.1", @@ -8229,13 +8512,11 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -8263,6 +8544,8 @@ "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, + "peer": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -8275,20 +8558,6 @@ "node": ">= 0.4" } }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -8341,11 +8610,17 @@ } }, "node_modules/sha1-file": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/sha1-file/-/sha1-file-1.0.4.tgz", - "integrity": "sha512-IgcUYjTck/UAx0wdtBoTwiy4/yiIZX6do4uaqUtryJY/pBOQC1w3Cb/bZMyC2H3QYnodL5vbX0lY69xlWqeBnA==", - "deprecated": "Version 1.x or earlier is no longer supported.", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sha1-file/-/sha1-file-2.0.1.tgz", + "integrity": "sha512-L4Kum9Lp8cWqcGKycZcXxR6spUoG4idDIUzAKjPiELnIZWxiFlZ5HFVzFxVxuWuGPsrraeL0JoGk0nFZ7AGFEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasha": "^5.2.0" + }, + "engines": { + "node": ">=10" + } }, "node_modules/shebang-command": { "version": "1.2.0", @@ -8372,6 +8647,8 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "peer": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -8386,6 +8663,17 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -8538,6 +8826,46 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/sol-digger": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/sol-digger/-/sol-digger-0.0.2.tgz", @@ -8551,35 +8879,148 @@ "dev": true }, "node_modules/sol2uml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/sol2uml/-/sol2uml-2.4.2.tgz", - "integrity": "sha512-/r4kGFSiPNAEhpr7gbJ/VTOyVuoA7+aLCQdhCsGtwd1TDN6OOCoDBu2wvtNM7uUwrDIEKWkqcGBQcB7meI8TnA==", + "version": "2.5.26", + "resolved": "https://registry.npmjs.org/sol2uml/-/sol2uml-2.5.26.tgz", + "integrity": "sha512-lq7ktw4yLDcgF8em5NXaCyKJmd6qgDIxSG/onWT4CGLQeWufauBnGHuTRf/yYY1DCPmpF9PENQ5hcRPSAIqRcg==", "dev": true, + "license": "MIT", "dependencies": { "@aduh95/viz.js": "^3.7.0", - "@solidity-parser/parser": "^0.14.5", - "axios": "1.1.3", + "@solidity-parser/parser": "^0.20.1", + "axios": "^1.13.6", "axios-debug-log": "^1.0.0", - "cli-color": "^2.0.3", - "commander": "^9.4.1", - "convert-svg-to-png": "^0.6.4", - "debug": "^4.3.4", + "cli-color": "^2.0.4", + "commander": "^12.1.0", + "convert-svg-to-png": "^0.7.1", + "debug": "^4.4.1", "diff-match-patch": "^1.0.5", - "ethers": "^5.7.2", + "ethers": "^6.16.0", "js-graph-algorithms": "^1.0.18", - "klaw": "^4.0.1" + "klaw": "^4.1.0", + "puppeteer": "^24.37.5" }, "bin": { "sol2uml": "lib/sol2uml.js" } }, + "node_modules/sol2uml/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/sol2uml/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/sol2uml/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/sol2uml/node_modules/commander": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", - "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/sol2uml/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sol2uml/node_modules/ethers": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", + "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.11.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.21.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sol2uml/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/sol2uml/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/sol2uml/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.20.0 || >=14" + "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/solc": { @@ -8729,12 +9170,10 @@ } }, "node_modules/solidity-ast": { - "version": "0.4.56", - "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.56.tgz", - "integrity": "sha512-HgmsA/Gfklm/M8GFbCX/J1qkVH0spXHgALCNZ8fA8x5X+MFdn/8CP2gr5OVyXjXw6RZTPC/Sxl2RUDQOXyNMeA==", - "dependencies": { - "array.prototype.findlast": "^1.2.2" - } + "version": "0.4.62", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.62.tgz", + "integrity": "sha512-jSC7msQCkJXIzM8LlDjRZ5cif5w40g6THlXHFk3zchbL5dm3YLoBETvqPGo5KndYkftjhcs5kz1fnTu4d34lVQ==", + "license": "MIT" }, "node_modules/solidity-comments-extractor": { "version": "0.0.7", @@ -8981,6 +9420,18 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -9002,49 +9453,6 @@ "node": ">=4" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -9105,272 +9513,200 @@ } }, "node_modules/surya": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/surya/-/surya-0.4.6.tgz", - "integrity": "sha512-zaTYkRbms26cuOWu5jon5l4OsToHX7ZEflqTozXgq/XxUL3VY+tEnxT9Te2WVsA/sYgZPwcH92yQZJgljsss4g==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/surya/-/surya-0.4.13.tgz", + "integrity": "sha512-ff2YmkYu9+u9A1tUv6cEuQDhLw1N+++iI+ZenXyhYR7YmaiQ19h32p2VchBn6zy3JPcfpvBZjf/aEmLbSMW1WA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@solidity-parser/parser": "^0.14.1", + "@solidity-parser/parser": "^0.16.1", "c3-linearization": "^0.3.0", "colors": "^1.4.0", "graphviz": "0.0.9", - "sha1-file": "^1.0.4", + "sha1-file": "^2.0.0", "treeify": "^1.1.0", - "yargs": "^11.1.1" + "yargs": "^17.0.0" }, "bin": { "surya": "bin/surya" } }, - "node_modules/surya/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "node_modules/surya/node_modules/@solidity-parser/parser": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.2.tgz", + "integrity": "sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg==", "dev": true, + "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/surya/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" + "antlr4ts": "^0.5.0-alpha.4" } }, - "node_modules/surya/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "node_modules/surya/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, - "dependencies": { - "locate-path": "^2.0.0" - }, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/surya/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "node_modules/surya/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": { - "pump": "^3.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=6" - } - }, - "node_modules/surya/node_modules/invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true, - "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/surya/node_modules/lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "node_modules/surya/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, - "dependencies": { - "invert-kv": "^2.0.0" - }, + "license": "ISC", "engines": { - "node": ">=6" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/surya/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "node_modules/surya/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, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/surya/node_modules/mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "node_modules/surya/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": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6" - } - }, - "node_modules/surya/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/surya/node_modules/os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "node_modules/surya/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": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/surya/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "node_modules/surya/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": { - "p-try": "^1.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/surya/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" + "node": ">=10" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/surya/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/surya/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "node_modules/surya/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": ">=4" - } - }, - "node_modules/surya/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" + "node": ">=10" } }, "node_modules/surya/node_modules/yargs": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.1.tgz", - "integrity": "sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.1.0", + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" } }, "node_modules/surya/node_modules/yargs-parser": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", - "integrity": "sha512-CswCfdOgCr4MMsT1GzbEJ7Z2uYudWyrGX8Bgh/0eyCzj/DXWdKq6a/ADufkzI1WAOIW6jYaXJvRyLhDO0kfqBw==", + "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, - "dependencies": { - "camelcase": "^4.1.0" + "license": "ISC", + "engines": { + "node": ">=12" } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", "dev": true, + "license": "MIT", "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", "dev": true, + "license": "MIT", "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, + "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "streamx": "^2.12.5" } }, "node_modules/temp": { @@ -9382,38 +9718,44 @@ "node >=0.4.0" ] }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "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 }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "node_modules/timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", "dev": true, + "license": "ISC", "dependencies": { - "es5-ext": "~0.10.46", - "next-tick": "1" + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/to-object-path": { @@ -9478,12 +9820,6 @@ "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 - }, "node_modules/treeify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", @@ -9522,10 +9858,11 @@ "peer": true }, "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" }, "node_modules/type-fest": { "version": "0.21.3", @@ -9540,74 +9877,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", - "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" }, "node_modules/uglify-js": { "version": "3.17.4", @@ -9622,30 +9897,6 @@ "node": ">=0.8.0" } }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dev": true, - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, "node_modules/undici": { "version": "5.22.1", "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", @@ -9659,6 +9910,12 @@ "node": ">=14.0" } }, + "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==", + "license": "MIT" + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -9782,20 +10039,48 @@ "uuid": "dist/bin/uuid" } }, - "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 + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" }, - "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==", + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/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==", "dev": true, + "license": "MIT", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/which": { @@ -9810,45 +10095,12 @@ "which": "bin/which" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", "dev": true }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -9933,6 +10185,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", "dev": true, + "peer": true, "engines": { "node": ">=8.3.0" }, @@ -9955,12 +10208,6 @@ "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", "dev": true }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yargs": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz", @@ -10104,6 +10351,7 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -10121,15 +10369,53 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { + "@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 + }, "@aduh95/viz.js": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/@aduh95/viz.js/-/viz.js-3.7.0.tgz", "integrity": "sha512-20Pk2Z98fbPLkECcrZSJszKos/OgtvJJR3NcbVfgCJ6EQjDNzW2P1BKqImOz3tJ952dvO2DWEhcLhQ1Wz1e9ng==", "dev": true }, + "@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true + }, + "@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==" + }, "@chainsafe/as-sha256": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz", @@ -10164,6 +10450,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", "dev": true, + "peer": true, "requires": { "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", @@ -10181,6 +10468,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", "dev": true, + "peer": true, "requires": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -10196,6 +10484,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", "dev": true, + "peer": true, "requires": { "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", @@ -10209,6 +10498,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", "dev": true, + "peer": true, "requires": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -10222,6 +10512,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0" } @@ -10231,6 +10522,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz", "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/properties": "^5.7.0" @@ -10241,6 +10533,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0", @@ -10252,6 +10545,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", "dev": true, + "peer": true, "requires": { "@ethersproject/logger": "^5.7.0" } @@ -10261,6 +10555,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", "dev": true, + "peer": true, "requires": { "@ethersproject/bignumber": "^5.7.0" } @@ -10270,6 +10565,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz", "integrity": "sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==", "dev": true, + "peer": true, "requires": { "@ethersproject/abi": "^5.7.0", "@ethersproject/abstract-provider": "^5.7.0", @@ -10288,6 +10584,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", "dev": true, + "peer": true, "requires": { "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/address": "^5.7.0", @@ -10305,6 +10602,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz", "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==", "dev": true, + "peer": true, "requires": { "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/basex": "^5.7.0", @@ -10325,6 +10623,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz", "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==", "dev": true, + "peer": true, "requires": { "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/address": "^5.7.0", @@ -10346,6 +10645,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "js-sha3": "0.8.0" @@ -10355,13 +10655,15 @@ "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", - "dev": true + "dev": true, + "peer": true }, "@ethersproject/networks": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", "dev": true, + "peer": true, "requires": { "@ethersproject/logger": "^5.7.0" } @@ -10371,6 +10673,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz", "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/sha2": "^5.7.0" @@ -10381,6 +10684,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", "dev": true, + "peer": true, "requires": { "@ethersproject/logger": "^5.7.0" } @@ -10390,6 +10694,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", "dev": true, + "peer": true, "requires": { "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0", @@ -10418,6 +10723,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz", "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0" @@ -10428,6 +10734,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0" @@ -10438,6 +10745,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz", "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0", @@ -10449,6 +10757,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/logger": "^5.7.0", @@ -10463,6 +10772,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz", "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==", "dev": true, + "peer": true, "requires": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -10477,6 +10787,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/constants": "^5.7.0", @@ -10488,6 +10799,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", "dev": true, + "peer": true, "requires": { "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", @@ -10505,6 +10817,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz", "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==", "dev": true, + "peer": true, "requires": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/constants": "^5.7.0", @@ -10516,6 +10829,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==", "dev": true, + "peer": true, "requires": { "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0", @@ -10539,6 +10853,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", "dev": true, + "peer": true, "requires": { "@ethersproject/base64": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -10552,6 +10867,7 @@ "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz", "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==", "dev": true, + "peer": true, "requires": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/hash": "^5.7.0", @@ -10574,6 +10890,23 @@ "tweetnacl-util": "^0.15.1" } }, + "@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "requires": { + "@noble/hashes": "1.3.2" + }, + "dependencies": { + "@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 + } + } + }, "@noble/hashes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", @@ -11099,6 +11432,14 @@ } } }, + "@nomicfoundation/slang": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-0.18.3.tgz", + "integrity": "sha512-YqAWgckqbHM0/CZxi9Nlf4hjk9wUNLC9ngWCWBiqMxPIZmzsVKYuChdlrfeBPQyvQQBoOhbx+7C1005kLVQDZQ==", + "requires": { + "@bytecodealliance/preview2-shim": "0.17.0" + } + }, "@nomicfoundation/solidity-analyzer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", @@ -11199,18 +11540,21 @@ "peer": true }, "@openzeppelin/upgrades-core": { - "version": "1.32.5", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.32.5.tgz", - "integrity": "sha512-R0wprsyJ4xWiRW05kaTfZZkRVpG2g0af3/hpjE7t2mX0Eb2n40MQLokTwqIk4LDzpp910JfLSpB0vBuZ6WNPog==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.46.0.tgz", + "integrity": "sha512-UFSeO/4r8eeXj0C/HAwV+J4b72sE1HX0aALQFs5S2RBOsfXvKweyjQf35vrK32LQiyHdP6IPShAsEBVvpSEgGQ==", "requires": { - "cbor": "^9.0.0", + "@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": "^10.2.5", "minimist": "^1.2.7", "proper-lockfile": "^4.1.1", - "solidity-ast": "^0.4.51" + "solidity-ast": "^0.4.60" }, "dependencies": { "ethereum-cryptography": { @@ -11218,39 +11562,158 @@ "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz", "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", "requires": { - "@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" + "@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" + } + }, + "ethereumjs-util": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz", + "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", + "requires": { + "@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" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + } + } + }, + "@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "dev": true, + "requires": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "dependencies": { + "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 + }, + "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, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "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 + }, + "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 + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "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, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "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, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "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, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, - "ethereumjs-util": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz", - "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, "requires": { - "@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" + "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" } }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + "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 } } }, @@ -11381,6 +11844,12 @@ "antlr4ts": "^0.5.0-alpha.4" } }, + "@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, "@types/bn.js": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz", @@ -11412,9 +11881,12 @@ "dev": true }, "@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "requires": { + "undici-types": "~6.19.2" + } }, "@types/pbkdf2": { "version": "3.1.0", @@ -11444,9 +11916,9 @@ } }, "@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "optional": true, "requires": { @@ -11510,7 +11982,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", - "dev": true + "dev": true, + "peer": true }, "agent-base": { "version": "6.0.2", @@ -11595,8 +12068,7 @@ "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, - "peer": true + "dev": true }, "arr-diff": { "version": "2.0.0", @@ -11619,54 +12091,35 @@ "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", "dev": true }, - "array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "requires": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - } - }, "array-unique": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", "integrity": "sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg==", "dev": true }, - "array.prototype.findlast": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.4.tgz", - "integrity": "sha512-BMtLxpV+8BD+6ZPFIWmnUBpQoy+A+ujcg4rhp2iwCRJYA7PEh2MS4NL3lz8EiDlLrJPp2hg9qWihr5pd//jcGw==", - "requires": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - } - }, - "arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", - "requires": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - } - }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", "dev": true }, + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + }, + "dependencies": { + "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 + } + } + }, "async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", @@ -11685,23 +12138,16 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, - "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==", - "requires": { - "possible-typed-array-names": "^1.0.0" - } - }, "axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "dev": true, "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "axios-debug-log": { @@ -11714,12 +12160,65 @@ "debug": "^4.0.0" } }, + "b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "requires": {} + }, "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 }, + "bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "requires": {} + }, + "bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "dev": true, + "requires": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + } + }, + "bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true + }, + "bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "requires": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + } + }, + "bare-url": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", + "dev": true, + "requires": { + "bare-path": "^3.0.0" + } + }, "base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", @@ -11764,13 +12263,21 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "peer": true + }, + "basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", "dev": true }, "bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", - "dev": true + "dev": true, + "peer": true }, "bigint-crypto-utils": { "version": "3.2.2", @@ -11779,6 +12286,11 @@ "dev": true, "peer": true }, + "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==" + }, "binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", @@ -11795,30 +12307,6 @@ "file-uri-to-path": "1.0.0" } }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, "blakejs": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", @@ -11836,12 +12324,18 @@ "dev": true }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "requires": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "dependencies": { + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" + } } }, "braces": { @@ -11910,16 +12404,6 @@ "safe-buffer": "^5.1.2" } }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -11990,6 +12474,8 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "peer": true, "requires": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -11998,6 +12484,22 @@ "set-function-length": "^1.2.1" } }, + "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==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", @@ -12019,11 +12521,11 @@ "peer": true }, "cbor": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", - "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "version": "10.0.12", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.12.tgz", + "integrity": "sha512-exQDevYd7ZQLP4moMQcZkKCVZsXLAtUSflObr3xTh4xzFIv/xBCdvCd6L259kQOUP2kcTC0jvC6PpZIf/WmRXA==", "requires": { - "nofilter": "^3.1.0" + "nofilter": "^3.0.2" } }, "chalk": { @@ -12036,18 +12538,30 @@ } }, "cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "dev": true, "requires": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "dependencies": { + "undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true + } } }, "cheerio-select": { @@ -12081,11 +12595,15 @@ "readdirp": "^2.0.0" } }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "requires": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + } }, "ci-info": { "version": "2.0.0", @@ -12191,13 +12709,13 @@ "peer": true }, "cli-color": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz", - "integrity": "sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", "dev": true, "requires": { "d": "^1.0.1", - "es5-ext": "^0.10.61", + "es5-ext": "^0.10.64", "es6-iterator": "^2.0.3", "memoizee": "^0.4.15", "timers-ext": "^0.1.7" @@ -12295,39 +12813,24 @@ "dev": true }, "convert-svg-core": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/convert-svg-core/-/convert-svg-core-0.6.4.tgz", - "integrity": "sha512-8mS0n7otc1lljTte4z7nDhihEakKCRq4w5ivMnIGeOZuD/OV/eDZNNEgGLV1ET3p+rMbnrZnX4lAcsf14WzD5w==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/convert-svg-core/-/convert-svg-core-0.7.1.tgz", + "integrity": "sha512-qlQlT2pHMCG0NmZsh2yuYNYO9zKbOmHoWPT+ibuvpVjvA7l9aNhHS4debQeZGuR0mA4x/0a38zOTqBkkdYoTXQ==", "dev": true, "requires": { - "chalk": "^4.1.2", - "cheerio": "^1.0.0-rc.11", - "commander": "^9.2.0", - "file-url": "^3.0.0", - "get-stdin": "^8.0.0", - "glob": "^8.0.1", - "lodash.omit": "^4.5.0", - "lodash.pick": "^4.4.0", - "pollock": "^0.2.0", - "puppeteer": "^13.7.0", - "tmp": "^0.2.1" - }, - "dependencies": { - "commander": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", - "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", - "dev": true - } + "cheerio": "^1.1.0", + "file-url": "^4.0.0", + "puppeteer-core": "^24.10.1", + "tmp": "^0.2.3" } }, "convert-svg-to-png": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/convert-svg-to-png/-/convert-svg-to-png-0.6.4.tgz", - "integrity": "sha512-zHNTuVedkyuhMl+f+HMm2L7+TKDYCKFAqAmDqUr0dN7/xtgYe76PPAydjlFzeLbzEpGtEfhaA15q+ejpLaVo3g==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/convert-svg-to-png/-/convert-svg-to-png-0.7.1.tgz", + "integrity": "sha512-XgLC/EmK0/GvdaHpCpEHCHL/ty/TDeezk8+AKWmUfEgUrYiwR9Tqrih9zfVWVzQYvn8mtjLvROv9xRQ7FHBo/Q==", "dev": true, "requires": { - "convert-svg-core": "^0.6.4" + "convert-svg-core": "^0.7.1" } }, "cookie": { @@ -12349,6 +12852,18 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "requires": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + } + }, "crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -12381,15 +12896,6 @@ "sha.js": "^2.4.8" } }, - "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, - "requires": { - "node-fetch": "2.6.7" - } - }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -12420,9 +12926,9 @@ } }, "css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, "requires": { "boolbase": "^1.0.0", @@ -12433,50 +12939,26 @@ } }, "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true }, "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "dev": true, "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "requires": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - } - }, - "data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "requires": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "es5-ext": "^0.10.64", + "type": "^2.7.2" } }, - "data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "requires": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - } + "data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true }, "debug": { "version": "4.3.4", @@ -12502,22 +12984,14 @@ "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, + "peer": true, "requires": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, - "define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "requires": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -12536,6 +13010,17 @@ } } }, + "degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "requires": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -12550,9 +13035,9 @@ "peer": true }, "devtools-protocol": { - "version": "0.0.981744", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.981744.tgz", - "integrity": "sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==", + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", "dev": true }, "diff": { @@ -12594,14 +13079,25 @@ } }, "domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "requires": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" + "domhandler": "^5.0.3" + } + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" } }, "elliptic": { @@ -12629,8 +13125,28 @@ "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 + }, + "encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "dev": true, - "peer": true + "requires": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "dependencies": { + "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==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } }, "end-of-stream": { "version": "1.4.4", @@ -12652,134 +13168,74 @@ } }, "entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true }, "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, - "peer": true - }, - "eol": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", - "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", "dev": true }, - "es-abstract": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz", - "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==", - "requires": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.5", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - } + "eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true }, - "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "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, "requires": { - "get-intrinsic": "^1.2.4" + "is-arrayish": "^0.2.1" } }, + "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==", + "dev": true + }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true }, "es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, "requires": { "es-errors": "^1.3.0" } }, "es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "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, "requires": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - } - }, - "es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "requires": { - "hasown": "^2.0.0" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "hasown": "^2.0.2" } }, "es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "requires": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, @@ -12795,13 +13251,13 @@ } }, "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "dev": true, "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" + "d": "^1.0.2", + "ext": "^1.7.0" } }, "es6-weak-map": { @@ -12820,8 +13276,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "peer": true + "dev": true }, "escape-string-regexp": { "version": "1.0.5", @@ -12829,6 +13284,57 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, + "escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + }, + "dependencies": { + "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, + "optional": true + } + } + }, + "esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "requires": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + } + }, + "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 + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, "ethereum-cryptography": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", @@ -12926,6 +13432,7 @@ "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", "dev": true, + "peer": true, "requires": { "@ethersproject/abi": "5.7.0", "@ethersproject/abstract-provider": "5.7.0", @@ -13008,6 +13515,15 @@ "dev": true, "peer": true }, + "events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "requires": { + "bare-events": "^2.7.0" + } + }, "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -13065,14 +13581,6 @@ "dev": true, "requires": { "type": "^2.7.2" - }, - "dependencies": { - "type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", - "dev": true - } } }, "extend-shallow": { @@ -13123,6 +13631,12 @@ "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==", "dev": true }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "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", @@ -13146,9 +13660,9 @@ "optional": true }, "file-url": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/file-url/-/file-url-3.0.0.tgz", - "integrity": "sha512-g872QGsHexznxkIAdK8UiZRe7SkE6kvylShU4Nsj8NvfvZag7S0QuQ4IgvPDkk75HxgjIVDwycFTDAgIiO4nDA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-4.0.0.tgz", + "integrity": "sha512-vRCdScQ6j3Ku6Kd7W1kZk9c++5SqD6Xz5Jotrjr/nkY714M14RFHy/AAVA2WQvpsqVAVgTbDrYyBpU205F0cLw==", "dev": true }, "filename-regex": { @@ -13170,16 +13684,6 @@ "repeat-string": "^1.5.2" } }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, "flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -13188,19 +13692,11 @@ "peer": true }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "requires": { - "is-callable": "^1.1.3" - } - }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -13217,14 +13713,16 @@ } }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" } }, "fp-ts": { @@ -13243,12 +13741,6 @@ "map-cache": "^0.2.2" } }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, "fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -13281,18 +13773,8 @@ "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - } + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true }, "functional-red-black-tree": { "version": "1.0.1", @@ -13301,11 +13783,6 @@ "dev": true, "peer": true }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" - }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -13313,22 +13790,32 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "requires": { + "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", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, - "get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } }, "get-stream": { "version": "5.2.0", @@ -13339,14 +13826,15 @@ "pump": "^3.0.0" } }, - "get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, "requires": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" } }, "get-value": { @@ -13355,19 +13843,6 @@ "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", "dev": true }, - "glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, "glob-base": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", @@ -13387,21 +13862,11 @@ "is-glob": "^2.0.0" } }, - "globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "requires": { - "define-properties": "^1.1.3" - } - }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true }, "graceful-fs": { "version": "4.2.10", @@ -14079,11 +14544,6 @@ } } }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -14093,24 +14553,23 @@ "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, + "peer": true, "requires": { "es-define-property": "^1.0.0" } }, - "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" - }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true }, "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, "requires": { "has-symbols": "^1.0.3" } @@ -14211,10 +14670,35 @@ "minimalistic-assert": "^1.0.1" } }, + "hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "requires": { "function-bind": "^1.1.2" } @@ -14236,15 +14720,23 @@ } }, "htmlparser2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "requires": { "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "entities": "^4.3.0" + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + }, + "dependencies": { + "entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true + } } }, "http-errors": { @@ -14261,6 +14753,24 @@ "toidentifier": "1.0.1" } }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "dependencies": { + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + } + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -14285,7 +14795,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "dev": true, + "peer": true }, "immutable": { "version": "4.3.0", @@ -14294,6 +14805,16 @@ "dev": true, "peer": true }, + "import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -14316,16 +14837,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "requires": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - } - }, "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -14342,6 +14853,12 @@ "fp-ts": "^1.0.0" } }, + "ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "dev": true + }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", @@ -14359,22 +14876,11 @@ } } }, - "is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - } - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "requires": { - "has-bigints": "^1.0.1" - } + "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 }, "is-binary-path": { "version": "1.0.1", @@ -14385,26 +14891,12 @@ "binary-extensions": "^1.0.0" } }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "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==" - }, "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", @@ -14422,22 +14914,6 @@ } } }, - "is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "requires": { - "is-typed-array": "^1.1.13" - } - }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", @@ -14506,11 +14982,6 @@ "dev": true, "peer": true }, - "is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==" - }, "is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", @@ -14520,14 +14991,6 @@ "kind-of": "^3.0.2" } }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -14570,53 +15033,12 @@ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "dev": true }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "requires": { - "call-bind": "^1.0.7" - } - }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "requires": { - "which-typed-array": "^1.1.14" - } - }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -14624,14 +15046,6 @@ "dev": true, "peer": true }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" - } - }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -14676,7 +15090,8 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "dev": true + "dev": true, + "peer": true }, "js-string-escape": { "version": "1.0.1", @@ -14684,16 +15099,27 @@ "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", "dev": true }, + "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 + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "peer": true, "requires": { "argparse": "^2.0.1" } }, + "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 + }, "json-schema-traverse": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", @@ -14798,14 +15224,11 @@ } } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } + "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 }, "lodash": { "version": "4.17.21", @@ -14813,18 +15236,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", - "dev": true - }, - "lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", - "dev": true - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -14844,13 +15255,10 @@ "peer": true }, "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true }, "lru-queue": { "version": "0.1.0", @@ -14861,15 +15269,6 @@ "es5-ext": "~0.10.2" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -14885,6 +15284,12 @@ "object-visit": "^1.0.0" } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true + }, "math-random": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", @@ -14918,13 +15323,13 @@ } }, "memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", "dev": true, "requires": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", + "d": "^1.0.2", + "es5-ext": "^0.10.64", "es6-weak-map": "^2.0.3", "event-emitter": "^0.3.5", "is-promise": "^2.2.2", @@ -15005,12 +15410,11 @@ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, "minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", - "dev": true, + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.8" } }, "minimist": { @@ -15019,6 +15423,12 @@ "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", "dev": true }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -15049,12 +15459,6 @@ "minimist": "0.0.8" } }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, "mnemonist": { "version": "0.38.5", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", @@ -15238,32 +15642,23 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true + }, "next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "node-addon-api": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, "node-gyp-build": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", @@ -15369,12 +15764,9 @@ "object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "peer": true }, "object-visit": { "version": "1.0.1", @@ -15393,17 +15785,6 @@ } } }, - "object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "requires": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, "object.omit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", @@ -15465,57 +15846,74 @@ "dev": true, "peer": true }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", - "dev": true - }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "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, + "peer": true, "requires": { - "p-try": "^2.0.0" + "aggregate-error": "^3.0.0" } }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", "dev": true, "requires": { - "p-limit": "^2.2.0" + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "dependencies": { + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + } } }, - "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==", + "pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, - "peer": true, "requires": { - "aggregate-error": "^3.0.0" + "degenerator": "^5.0.0", + "netmask": "^2.0.2" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "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, + "requires": { + "callsites": "^3.0.0" + } }, "parse-glob": { "version": "3.0.4", @@ -15529,22 +15927,51 @@ "is-glob": "^2.0.0" } }, + "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, + "requires": { + "@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" + } + }, "parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "requires": { - "entities": "^4.4.0" + "entities": "^6.0.0" + }, + "dependencies": { + "entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true + } } }, "parse5-htmlparser2-tree-adapter": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", - "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "requires": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + } + }, + "parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, "requires": { - "domhandler": "^5.0.2", "parse5": "^7.0.0" } }, @@ -15554,12 +15981,6 @@ "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", "dev": true }, - "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 - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -15603,6 +16024,12 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -15610,32 +16037,12 @@ "dev": true, "peer": true }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "pollock": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/pollock/-/pollock-0.2.1.tgz", - "integrity": "sha512-2Xy6LImSXm0ANKv9BKSVuCa6Z4ACbK7oUrl9gtUgqLkekL7n9C0mlWsOGYYuGbCG8xT0x3Q4F31C3ZMyVQjwsg==", - "dev": true - }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", "dev": true }, - "possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==" - }, "preserve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", @@ -15682,10 +16089,50 @@ "signal-exit": "^3.0.2" } }, + "proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "dependencies": { + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "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 + } + } + }, "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true }, "pseudomap": { @@ -15705,29 +16152,53 @@ } }, "puppeteer": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-13.7.0.tgz", - "integrity": "sha512-U1uufzBjz3+PkpCxFrWzh4OrMIdIb2ztzCu0YEPfRHjHswcSwHZswnK+WdsOQJsRV8WeTg3jLhJR4D867+fjsA==", + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", + "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", "dev": true, "requires": { - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.981744", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "pkg-dir": "4.2.0", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.5.0" + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1608973", + "puppeteer-core": "24.43.1", + "typed-query-selector": "^2.12.2" + } + }, + "puppeteer-core": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", + "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "dev": true, + "requires": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" }, "dependencies": { + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "requires": {} } @@ -16103,17 +16574,6 @@ "safe-regex": "^1.1.0" } }, - "regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "requires": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - } - }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -16161,6 +16621,12 @@ "path-parse": "^1.0.6" } }, + "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 + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -16178,50 +16644,6 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==" }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, "ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", @@ -16256,24 +16678,6 @@ "dev": true, "peer": true }, - "safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "requires": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "dependencies": { - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - } - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -16288,22 +16692,11 @@ "ret": "~0.1.10" } }, - "safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "requires": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - } - }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "peer": true + "dev": true }, "scrypt-js": { "version": "3.0.1", @@ -16321,13 +16714,10 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true }, "serialize-javascript": { "version": "6.0.0", @@ -16349,6 +16739,8 @@ "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, + "peer": true, "requires": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -16358,17 +16750,6 @@ "has-property-descriptors": "^1.0.2" } }, - "set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "requires": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - } - }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -16414,10 +16795,13 @@ } }, "sha1-file": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/sha1-file/-/sha1-file-1.0.4.tgz", - "integrity": "sha512-IgcUYjTck/UAx0wdtBoTwiy4/yiIZX6do4uaqUtryJY/pBOQC1w3Cb/bZMyC2H3QYnodL5vbX0lY69xlWqeBnA==", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sha1-file/-/sha1-file-2.0.1.tgz", + "integrity": "sha512-L4Kum9Lp8cWqcGKycZcXxR6spUoG4idDIUzAKjPiELnIZWxiFlZ5HFVzFxVxuWuGPsrraeL0JoGk0nFZ7AGFEQ==", + "dev": true, + "requires": { + "hasha": "^5.2.0" + } }, "shebang-command": { "version": "1.2.0", @@ -16438,6 +16822,8 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "peer": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -16449,6 +16835,12 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -16571,7 +16963,36 @@ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "dev": true, "requires": { - "kind-of": "^3.2.0" + "kind-of": "^3.2.0" + } + }, + "socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "requires": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "dependencies": { + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + } } }, "sol-digger": { @@ -16587,30 +17008,92 @@ "dev": true }, "sol2uml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/sol2uml/-/sol2uml-2.4.2.tgz", - "integrity": "sha512-/r4kGFSiPNAEhpr7gbJ/VTOyVuoA7+aLCQdhCsGtwd1TDN6OOCoDBu2wvtNM7uUwrDIEKWkqcGBQcB7meI8TnA==", + "version": "2.5.26", + "resolved": "https://registry.npmjs.org/sol2uml/-/sol2uml-2.5.26.tgz", + "integrity": "sha512-lq7ktw4yLDcgF8em5NXaCyKJmd6qgDIxSG/onWT4CGLQeWufauBnGHuTRf/yYY1DCPmpF9PENQ5hcRPSAIqRcg==", "dev": true, "requires": { "@aduh95/viz.js": "^3.7.0", - "@solidity-parser/parser": "^0.14.5", - "axios": "1.1.3", + "@solidity-parser/parser": "^0.20.1", + "axios": "^1.13.6", "axios-debug-log": "^1.0.0", - "cli-color": "^2.0.3", - "commander": "^9.4.1", - "convert-svg-to-png": "^0.6.4", - "debug": "^4.3.4", + "cli-color": "^2.0.4", + "commander": "^12.1.0", + "convert-svg-to-png": "^0.7.1", + "debug": "^4.4.1", "diff-match-patch": "^1.0.5", - "ethers": "^5.7.2", + "ethers": "^6.16.0", "js-graph-algorithms": "^1.0.18", - "klaw": "^4.0.1" + "klaw": "^4.1.0", + "puppeteer": "^24.37.5" }, "dependencies": { + "@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 + }, + "@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 + }, + "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 + }, "commander": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", - "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true + }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ethers": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", + "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==", + "dev": true, + "requires": { + "@adraffy/ens-normalize": "1.11.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.21.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true + }, + "ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "requires": {} } } }, @@ -16739,12 +17222,9 @@ } }, "solidity-ast": { - "version": "0.4.56", - "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.56.tgz", - "integrity": "sha512-HgmsA/Gfklm/M8GFbCX/J1qkVH0spXHgALCNZ8fA8x5X+MFdn/8CP2gr5OVyXjXw6RZTPC/Sxl2RUDQOXyNMeA==", - "requires": { - "array.prototype.findlast": "^1.2.2" - } + "version": "0.4.62", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.62.tgz", + "integrity": "sha512-jSC7msQCkJXIzM8LlDjRZ5cif5w40g6THlXHFk3zchbL5dm3YLoBETvqPGo5KndYkftjhcs5kz1fnTu4d34lVQ==" }, "solidity-comments-extractor": { "version": "0.0.7", @@ -16947,6 +17427,17 @@ "dev": true, "peer": true }, + "streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "requires": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -16965,37 +17456,6 @@ "strip-ansi": "^4.0.0" } }, - "string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "requires": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - } - }, - "string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "requires": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - } - }, - "string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - } - }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -17037,222 +17497,149 @@ } }, "surya": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/surya/-/surya-0.4.6.tgz", - "integrity": "sha512-zaTYkRbms26cuOWu5jon5l4OsToHX7ZEflqTozXgq/XxUL3VY+tEnxT9Te2WVsA/sYgZPwcH92yQZJgljsss4g==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/surya/-/surya-0.4.13.tgz", + "integrity": "sha512-ff2YmkYu9+u9A1tUv6cEuQDhLw1N+++iI+ZenXyhYR7YmaiQ19h32p2VchBn6zy3JPcfpvBZjf/aEmLbSMW1WA==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.14.1", + "@solidity-parser/parser": "^0.16.1", "c3-linearization": "^0.3.0", "colors": "^1.4.0", "graphviz": "0.0.9", - "sha1-file": "^1.0.4", + "sha1-file": "^2.0.0", "treeify": "^1.1.0", - "yargs": "^11.1.1" + "yargs": "^17.0.0" }, "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "@solidity-parser/parser": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.2.tgz", + "integrity": "sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg==", "dev": true, "requires": { - "pump": "^3.0.0" + "antlr4ts": "^0.5.0-alpha.4" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "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 }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "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, "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" } }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } + "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 }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "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 }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "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, "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "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, "requires": { - "p-try": "^1.0.0" + "ansi-regex": "^5.0.1" } }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "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, "requires": { - "p-limit": "^1.1.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yargs": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.1.tgz", - "integrity": "sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.1.0", + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" } }, "yargs-parser": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", - "integrity": "sha512-CswCfdOgCr4MMsT1GzbEJ7Z2uYudWyrGX8Bgh/0eyCzj/DXWdKq6a/ADufkzI1WAOIW6jYaXJvRyLhDO0kfqBw==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } + "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 } } }, "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", "dev": true, "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0", "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "tar-stream": "^3.1.5" } }, "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", "dev": true, "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "requires": { + "streamx": "^2.12.5" } }, "temp": { @@ -17261,36 +17648,36 @@ "integrity": "sha512-IsFisGgDKk7qzK9erMIkQe/XwiSUdac7z3wYOsjcLkhPBy3k1SlvLoIh2dAHIlEpgA971CgguMrx9z8fFg7tSA==", "dev": true }, + "text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4" + } + }, "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 }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", "dev": true, "requires": { - "es5-ext": "~0.10.46", - "next-tick": "1" + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" } }, "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true }, "to-object-path": { "version": "0.3.0", @@ -17341,12 +17728,6 @@ "dev": true, "peer": true }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, "treeify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", @@ -17382,9 +17763,9 @@ "peer": true }, "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", "dev": true }, "type-fest": { @@ -17394,53 +17775,11 @@ "dev": true, "peer": true }, - "typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "requires": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - } - }, - "typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "requires": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - } - }, - "typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", - "requires": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - } - }, - "typed-array-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", - "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", - "requires": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - } + "typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true }, "uglify-js": { "version": "3.17.4", @@ -17449,27 +17788,6 @@ "dev": true, "optional": true }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, - "unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dev": true, - "requires": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, "undici": { "version": "5.22.1", "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", @@ -17480,6 +17798,11 @@ "busboy": "^1.6.0" } }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -17576,22 +17899,38 @@ "dev": true, "peer": true }, - "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==", + "webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", "dev": true }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "iconv-lite": "0.6.3" + }, + "dependencies": { + "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==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } } }, + "whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -17601,36 +17940,12 @@ "isexe": "^2.0.0" } }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", "dev": true }, - "which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "requires": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - } - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -17702,6 +18017,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", "dev": true, + "peer": true, "requires": {} }, "y18n": { @@ -17710,12 +18026,6 @@ "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", "dev": true }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "yargs": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz", @@ -17841,6 +18151,12 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "peer": true + }, + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true } } } diff --git a/package.json b/package.json index 9fbb044..54166b8 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,30 @@ { "scripts": { + "build": "make build", + "test": "make test", + "coverage": "make coverage", + "lint": "make lint", "lint:sol": "npx solium -d src", "lint:sol:fix": "npx solium -d src --fix", "lint:sol:test": "npx solium -d test", "lint:sol:test:fix": "npx solium -d test --fix", - "lint:sol:script": "npx solium -d script", - "lint:sol:script:fix": "npx solium -d script --fix", - "lint:sol:prettier": "npx prettier --write 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'", - "uml": "npx sol2uml class src", + "lint:sol:prettier": "npx prettier --write 'src/**/*.sol' 'test/**/*.sol'", + "uml": "npx sol2uml class src -f svg -o doc/schema/classDiagram.svg && npx sol2uml class src -f png -o doc/schema/classDiagram.png", "uml:test": "npx sol2uml class test", - "surya:report": "npx surya mdreport surya_report_IncomeVault.md src/IncomeVault.sol", - "surya:graph": "npx surya graph src/IncomeVault.sol | dot -Tpng > surya_graph_IncomeVault.png && npx surya graph src/public/IncomeVaultRestricted.sol | dot -Tpng > surya_graph_IncomeVaultRestricted.png && npx surya graph src/public/IncomeVaultOpen.sol | dot -Tpng > surya_graph_IncomeVaultOpen.png ", + "surya:graph": "cd doc/script && bash script_surya_graph.sh", + "surya:inheritance": "cd doc/script && bash script_surya_inheritance.sh", + "surya:report": "cd doc/script && bash script_surya_report.sh", "docgen": "npx hardhat docgen" }, "devDependencies": { "@nomicfoundation/hardhat-foundry": "^1.0.1", "ethlint": "^1.2.5", "prettier-plugin-solidity": "^1.0.0-rc.1", - "sol2uml": "^2.2.6", + "sol2uml": "^2.5.26", "solidity-docgen": "^0.6.0-beta.35", - "surya": "^0.4.6" + "surya": "^0.4.13" }, "dependencies": { - "@openzeppelin/upgrades-core": "^1.32.5" + "@openzeppelin/upgrades-core": "^1.46.0" } } diff --git a/remappings.txt b/remappings.txt index bac691e..88ff1cc 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,10 @@ -CMTAT/=lib/CMTAT/contracts +CMTAT/=lib/CMTAT/contracts/ RuleEngine/=lib/RuleEngine/src/ +SnapshotEngine/=lib/SnapshotEngine/contracts/ OZ/=lib/openzeppelin-contracts/contracts/ -solidity-stringutils/=lib/openzeppelin-foundry-upgrades/lib/solidity-stringutils/ \ No newline at end of file +OZUpgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/ +solidity-stringutils/=lib/openzeppelin-foundry-upgrades/lib/solidity-stringutils/ +forge-std/=lib/forge-std/src/ diff --git a/script/DeployIncomeVault.s.sol b/script/DeployIncomeVault.s.sol new file mode 100644 index 0000000..8dfad32 --- /dev/null +++ b/script/DeployIncomeVault.s.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== Foundry === */ +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +/* ==== OpenZeppelin === */ +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +/* ==== CMTAT === */ +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +/* ==== IncomeVault === */ +import {IncomeVault} from "../src/deployment/IncomeVault.sol"; +import {ISnapshotSource} from "../src/interfaces/ISnapshotSource.sol"; + +/** + * @title Deploy the role-based {IncomeVault} behind a transparent proxy + * @dev + * Run with: + * + * ```bash + * forge script script/DeployIncomeVault.s.sol --rpc-url --broadcast --ffi + * ``` + * + * `--ffi` is required: the OpenZeppelin Upgrades plugin shells out to the upgrades-core npm package + * for the upgrade-safety validation, which also needs a **full** build — run `forge clean && forge build` + * first or every run fails with "not from a full compilation". + * + * Configuration comes from the environment; {deploy} takes it explicitly so the same code path is + * exercised by `test/script/Deploy.t.sol` without any environment at all. + */ +contract DeployIncomeVault is Script { + /** + * @notice Everything the deployment needs + * @dev `forwarder` and `ruleEngine` may be the zero address — gasless support and rule checks are + * both optional. Every other member must be set. + */ + struct Config { + // owner of the ProxyAdmin, i.e. who may upgrade the implementation + address proxyAdmin; + // receives DEFAULT_ADMIN_ROLE on the vault + address admin; + // ERC-2771 trusted forwarder, or the zero address to disable gasless support + address forwarder; + // ERC-20 the dividends are paid in + IERC20 paymentToken; + // source of the holder balances + ISnapshotSource snapshotEngine; + // optional transfer-restriction engine, or the zero address + IRuleEngine ruleEngine; + // delay after the dividend time during which a claim is accepted, must be non-zero + uint256 timeLimitToWithdraw; + } + + /** + * @notice Entry point — reads the configuration from the environment and broadcasts + * @return vault the deployed proxy, typed as {IncomeVault} + */ + function run() external returns (IncomeVault vault) { + Config memory config = configFromEnv(); + vm.startBroadcast(); + vault = deploy(config); + vm.stopBroadcast(); + logDeployment(vault, config); + } + + /** + * @notice Deploy and initialize the vault + * @dev No broadcasting here, so tests can call it directly. + * @param config the deployment configuration + * @return vault the deployed proxy, typed as {IncomeVault} + */ + function deploy(Config memory config) public returns (IncomeVault vault) { + checkConfig(config); + + Options memory opts; + // the implementation's constructor takes the ERC-2771 forwarder + opts.constructorData = abi.encode(config.forwarder); + + address proxy = Upgrades.deployTransparentProxy( + "IncomeVault.sol", + config.proxyAdmin, + abi.encodeCall( + IncomeVault.initialize, + ( + config.admin, + config.paymentToken, + config.snapshotEngine, + config.ruleEngine, + config.timeLimitToWithdraw + ) + ), + opts + ); + return IncomeVault(proxy); + } + + /** + * @notice Read the configuration from environment variables + * @dev `FORWARDER` and `RULE_ENGINE` default to the zero address; everything else is required. + * @return config the configuration + */ + function configFromEnv() public view returns (Config memory config) { + config = Config({ + proxyAdmin: vm.envAddress("PROXY_ADMIN"), + admin: vm.envAddress("VAULT_ADMIN"), + forwarder: vm.envOr("FORWARDER", address(0)), + paymentToken: IERC20(vm.envAddress("PAYMENT_TOKEN")), + snapshotEngine: ISnapshotSource(vm.envAddress("SNAPSHOT_ENGINE")), + ruleEngine: IRuleEngine(vm.envOr("RULE_ENGINE", address(0))), + timeLimitToWithdraw: vm.envUint("TIME_LIMIT_TO_WITHDRAW") + }); + } + + /** + * @notice Reject a configuration that would deploy a broken vault + * @dev + * The contract validates the zero addresses itself, so this only adds what it **cannot** check: + * that the two external dependencies are actually contracts. Passing an EOA — a mistyped address, + * or a token address from the wrong chain — deploys a vault that initializes successfully and then + * reverts on the first claim. + * @param config the configuration to check + */ + function checkConfig(Config memory config) public view { + require(config.proxyAdmin != address(0), "DeployIncomeVault: PROXY_ADMIN is zero"); + require(config.admin != address(0), "DeployIncomeVault: VAULT_ADMIN is zero"); + require(config.timeLimitToWithdraw != 0, "DeployIncomeVault: TIME_LIMIT_TO_WITHDRAW is zero"); + require(address(config.paymentToken).code.length > 0, "DeployIncomeVault: PAYMENT_TOKEN is not a contract"); + require(address(config.snapshotEngine).code.length > 0, "DeployIncomeVault: SNAPSHOT_ENGINE is not a contract"); + if (address(config.ruleEngine) != address(0)) { + require( + address(config.ruleEngine).code.length > 0, + "DeployIncomeVault: RULE_ENGINE is set but is not a contract" + ); + } + } + + /** + * @notice Print what was deployed + * @param vault the deployed proxy + * @param config the configuration used + */ + function logDeployment(IncomeVault vault, Config memory config) public view { + console.log("IncomeVault (proxy): ", address(vault)); + console.log(" version: ", vault.version()); + console.log(" admin: ", config.admin); + console.log(" proxy admin: ", config.proxyAdmin); + console.log(" payment token: ", address(vault.ERC20TokenPayment())); + console.log(" snapshot source: ", address(vault.dividendSnapshotSource())); + console.log(" rule engine: ", address(vault.ruleEngine())); + console.log(" timeLimitToWithdraw: ", vault.timeLimitToWithdraw()); + } +} diff --git a/script/DeployIncomeVaultOwnable2Step.s.sol b/script/DeployIncomeVaultOwnable2Step.s.sol new file mode 100644 index 0000000..ea91c17 --- /dev/null +++ b/script/DeployIncomeVaultOwnable2Step.s.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== Foundry === */ +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +/* ==== OpenZeppelin === */ +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +/* ==== CMTAT === */ +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +/* ==== IncomeVault === */ +import {IncomeVaultOwnable2Step} from "../src/deployment/IncomeVaultOwnable2Step.sol"; +import {ISnapshotSource} from "../src/interfaces/ISnapshotSource.sol"; + +/** + * @title Deploy the role-based {IncomeVault} behind a transparent proxy + * @dev + * Run with: + * + * ```bash + * forge script script/DeployIncomeVaultOwnable2Step.s.sol --rpc-url --broadcast --ffi + * ``` + * + * `--ffi` is required: the OpenZeppelin Upgrades plugin shells out to the upgrades-core npm package + * for the upgrade-safety validation, which also needs a **full** build — run `forge clean && forge build` + * first or every run fails with "not from a full compilation". + * + * Configuration comes from the environment; {deploy} takes it explicitly so the same code path is + * exercised by `test/script/Deploy.t.sol` without any environment at all. + */ +contract DeployIncomeVaultOwnable2Step is Script { + /** + * @notice Everything the deployment needs + * @dev `forwarder` and `ruleEngine` may be the zero address — gasless support and rule checks are + * both optional. Every other member must be set. + */ + struct Config { + // owner of the ProxyAdmin, i.e. who may upgrade the implementation + address proxyAdmin; + // receives ownership of the vault (ERC-173); holds every capability + address owner; + // ERC-2771 trusted forwarder, or the zero address to disable gasless support + address forwarder; + // ERC-20 the dividends are paid in + IERC20 paymentToken; + // source of the holder balances + ISnapshotSource snapshotEngine; + // optional transfer-restriction engine, or the zero address + IRuleEngine ruleEngine; + // delay after the dividend time during which a claim is accepted, must be non-zero + uint256 timeLimitToWithdraw; + } + + /** + * @notice Entry point — reads the configuration from the environment and broadcasts + * @return vault the deployed proxy, typed as {IncomeVault} + */ + function run() external returns (IncomeVaultOwnable2Step vault) { + Config memory config = configFromEnv(); + vm.startBroadcast(); + vault = deploy(config); + vm.stopBroadcast(); + logDeployment(vault, config); + } + + /** + * @notice Deploy and initialize the vault + * @dev No broadcasting here, so tests can call it directly. + * @param config the deployment configuration + * @return vault the deployed proxy, typed as {IncomeVault} + */ + function deploy(Config memory config) public returns (IncomeVaultOwnable2Step vault) { + checkConfig(config); + + Options memory opts; + // the implementation's constructor takes the ERC-2771 forwarder + opts.constructorData = abi.encode(config.forwarder); + + address proxy = Upgrades.deployTransparentProxy( + "IncomeVaultOwnable2Step.sol", + config.proxyAdmin, + abi.encodeCall( + IncomeVaultOwnable2Step.initialize, + ( + config.owner, + config.paymentToken, + config.snapshotEngine, + config.ruleEngine, + config.timeLimitToWithdraw + ) + ), + opts + ); + return IncomeVaultOwnable2Step(proxy); + } + + /** + * @notice Read the configuration from environment variables + * @dev `FORWARDER` and `RULE_ENGINE` default to the zero address; everything else is required. + * @return config the configuration + */ + function configFromEnv() public view returns (Config memory config) { + config = Config({ + proxyAdmin: vm.envAddress("PROXY_ADMIN"), + owner: vm.envAddress("VAULT_OWNER"), + forwarder: vm.envOr("FORWARDER", address(0)), + paymentToken: IERC20(vm.envAddress("PAYMENT_TOKEN")), + snapshotEngine: ISnapshotSource(vm.envAddress("SNAPSHOT_ENGINE")), + ruleEngine: IRuleEngine(vm.envOr("RULE_ENGINE", address(0))), + timeLimitToWithdraw: vm.envUint("TIME_LIMIT_TO_WITHDRAW") + }); + } + + /** + * @notice Reject a configuration that would deploy a broken vault + * @dev + * The contract validates the zero addresses itself, so this only adds what it **cannot** check: + * that the two external dependencies are actually contracts. Passing an EOA — a mistyped address, + * or a token address from the wrong chain — deploys a vault that initializes successfully and then + * reverts on the first claim. + * @param config the configuration to check + */ + function checkConfig(Config memory config) public view { + require(config.proxyAdmin != address(0), "DeployIncomeVaultOwnable2Step: PROXY_ADMIN is zero"); + require(config.owner != address(0), "DeployIncomeVaultOwnable2Step: VAULT_OWNER is zero"); + require(config.timeLimitToWithdraw != 0, "DeployIncomeVaultOwnable2Step: TIME_LIMIT_TO_WITHDRAW is zero"); + require( + address(config.paymentToken).code.length > 0, + "DeployIncomeVaultOwnable2Step: PAYMENT_TOKEN is not a contract" + ); + require( + address(config.snapshotEngine).code.length > 0, + "DeployIncomeVaultOwnable2Step: SNAPSHOT_ENGINE is not a contract" + ); + if (address(config.ruleEngine) != address(0)) { + require( + address(config.ruleEngine).code.length > 0, + "DeployIncomeVaultOwnable2Step: RULE_ENGINE is set but is not a contract" + ); + } + } + + /** + * @notice Print what was deployed + * @param vault the deployed proxy + * @param config the configuration used + */ + function logDeployment(IncomeVaultOwnable2Step vault, Config memory config) public view { + console.log("IncomeVault (proxy): ", address(vault)); + console.log(" version: ", vault.version()); + console.log(" owner: ", vault.owner()); + console.log(" proxy admin: ", config.proxyAdmin); + console.log(" payment token: ", address(vault.ERC20TokenPayment())); + console.log(" snapshot source: ", address(vault.dividendSnapshotSource())); + console.log(" rule engine: ", address(vault.ruleEngine())); + console.log(" timeLimitToWithdraw: ", vault.timeLimitToWithdraw()); + } +} diff --git a/src/IncomeVault.sol b/src/IncomeVault.sol deleted file mode 100644 index 65de17f..0000000 --- a/src/IncomeVault.sol +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -pragma solidity ^0.8.20; - - -import "CMTAT/modules/wrapper/extensions/MetaTxModule.sol"; -import "./public/IncomeVaultRestricted.sol"; -import "./public/IncomeVaultOpen.sol"; - -/** -* @title Income Vault to distribute dividends -*/ -contract IncomeVault is Initializable, ContextUpgradeable, IncomeVaultRestricted, IncomeVaultOpen, MetaTxModule{ - - /** - * @param forwarderIrrevocable Address of the forwarder, required for the gasless support - */ - /// @custom:oz-upgrades-unsafe-allow constructor - constructor( - address forwarderIrrevocable - ) MetaTxModule(forwarderIrrevocable) { - // Disable the possibility to initialize the implementation - _disableInitializers(); - } - - /** - * @notice - * initialize the proxy contract - * The calls to this function will revert if the contract was deployed without a proxy - * @param admin Address of the contract (Access Control) - * @param ERC20TokenPayment_ ERC20 token to perform the payment - */ - function initialize( - address admin, - IERC20 ERC20TokenPayment_, - ICMTATSnapshot cmtat_token, - IRuleEngine ruleEngine_, - IAuthorizationEngine authorizationEngineIrrevocable, - uint256 timeLimitToWithdraw_ - ) public initializer { - __IncomeVault_init( - admin, - ERC20TokenPayment_, - cmtat_token, - ruleEngine_, - authorizationEngineIrrevocable, - timeLimitToWithdraw_ - ); - } - - /** - * @dev calls the different initialize functions from the different modules - */ - function __IncomeVault_init( - address admin, - IERC20 ERC20TokenPayment_, - ICMTATSnapshot cmtat_token, - IRuleEngine ruleEngine_, - IAuthorizationEngine authorizationEngineIrrevocable, - uint256 timeLimitToWithdraw_ - ) internal onlyInitializing { - if(admin == address(0)){ - revert IncomeVault_AdminWithAddressZeroNotAllowed(); - } - if(address(ERC20TokenPayment_) == address(0)){ - revert IncomeVault_TokenPaymentWithAddressZeroNotAllowed(); - } - if(address(ERC20TokenPayment_) == address(0)){ - revert IncomeVault_CMTATWithAddressZeroNotAllowed(); - } - _grantRole(DEFAULT_ADMIN_ROLE, admin); - _grantRole(INCOME_VAULT_OPERATOR_ROLE, admin); - CMTAT_TOKEN = cmtat_token; - ERC20TokenPayment = ERC20TokenPayment_; - - // Initialization - __AccessControl_init_unchained(); - __AuthorizationModule_init_unchained(admin, authorizationEngineIrrevocable); - // PauseModule_init_unchained is called before ValidationModule_init_unchained due to inheritance - __Pausable_init_unchained(); - __Validation_init_unchained(ruleEngine_); - - __IncomeVaultRestricted_init_unchained(timeLimitToWithdraw_); - } - - /** - * @dev This surcharge is not necessary if you do not use the MetaTxModule - */ - function _msgSender() - internal - view - override(ERC2771ContextUpgradeable, ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - /** - * @dev This surcharge is not necessary if you do not use the MetaTxModule - */ - function _msgData() - internal - view - override(ERC2771ContextUpgradeable, ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } - - function _contextSuffixLength() internal view - override(ERC2771ContextUpgradeable, ContextUpgradeable) - returns (uint256) { - return ERC2771ContextUpgradeable._contextSuffixLength(); - } - - // Use in case of inheritance - uint256[50] private __gap; -} diff --git a/src/IncomeVaultBase.sol b/src/IncomeVaultBase.sol new file mode 100644 index 0000000..02f3551 --- /dev/null +++ b/src/IncomeVaultBase.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== OpenZeppelin === */ +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/* ==== CMTAT === */ +/* ==== Snapshot === */ +import {ISnapshotSource} from "./interfaces/ISnapshotSource.sol"; +/* ==== IncomeVault === */ +import {IncomeVaultValidationCore} from "./modules/IncomeVaultValidationCore.sol"; +import {IncomeVaultSnapshotModule} from "./modules/IncomeVaultSnapshotModule.sol"; +import {IncomeVaultRestricted} from "./public/IncomeVaultRestricted.sol"; +import {IncomeVaultOpen} from "./public/IncomeVaultOpen.sol"; +import {VersionModule} from "./modules/VersionModule.sol"; + +/** + * @title Income Vault to distribute dividends — logic shared by every deployment variant + * @dev + * The vault is not bound to a specific token implementation: the holder balances and the total + * supply are read through the {ISnapshotSource} interface, which is implemented by the CMTA + * `SnapshotEngine` as well as by any token embedding an equivalent snapshot module. + * + * This contract holds **what** the vault does. It deliberately declares neither an access-control + * policy nor a transfer-restriction policy: the `_authorize*` hooks and + * {IncomeVaultValidationCore-_validateTransfer} are left abstract and answered by the deployment + * contract, so the same logic ships role-based ({IncomeVault}) or single-owner + * ({IncomeVaultOwnable2Step}) — and can be embedded in a host that answers them from its own modules. + * + * It also declares **no meta-transaction policy**. Gasless support is a deployment decision, exactly + * like the access-control model: {IncomeVaultBaseERC2771} adds the ERC-2771 context on top of this + * contract, and the two shipped deployments inherit that. A deployment that does not want a trusted + * forwarder inherits this contract directly and pays for none of it. Finding M-8. + */ +abstract contract IncomeVaultBase is + IncomeVaultValidationCore, + Initializable, + ContextUpgradeable, + VersionModule, + IncomeVaultSnapshotModule, + IncomeVaultRestricted, + IncomeVaultOpen +{ + /* ============ Initializer Function ============ */ + /** + * @dev calls the initialize functions of the policy-agnostic modules + * @param ERC20TokenPayment_ ERC20 token used to perform the payment + * @param snapshotSource_ contract implementing {ISnapshotSource}, source of the holder balances + * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted + */ + function __IncomeVaultBase_init_unchained( + IERC20 ERC20TokenPayment_, + ISnapshotSource snapshotSource_, + uint256 timeLimitToWithdraw_ + ) internal onlyInitializing { + _setERC20TokenPayment(ERC20TokenPayment_); + _setDividendSnapshotSource(snapshotSource_); + + // EIP-712 domain for the ERC-7741 signed operator authorisations. The version stays "1" + // across releases on purpose: bumping it would invalidate every signature already issued. + __EIP712_init_unchained("IncomeVault", "1"); + __IncomeVaultRestricted_init_unchained(timeLimitToWithdraw_); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ +} diff --git a/src/IncomeVaultBaseERC2771.sol b/src/IncomeVaultBaseERC2771.sol new file mode 100644 index 0000000..cac5177 --- /dev/null +++ b/src/IncomeVaultBaseERC2771.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC2771ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol"; + +import {ERC2771Module} from "CMTAT/modules/wrapper/options/ERC2771Module.sol"; + +import {IncomeVaultBase} from "./IncomeVaultBase.sol"; + +/** + * @title {IncomeVaultBase} plus gasless support (ERC-2771) + * @dev + * Meta-transaction support is a **deployment decision**, in the same way the access-control model and + * the transfer-restriction policy are. {IncomeVaultBase} states what the vault does and knows nothing + * about forwarders; this contract adds the ERC-2771 context and resolves the + * `ERC2771ContextUpgradeable` / `ContextUpgradeable` diamond it creates. Both shipped deployments + * inherit it, so their behaviour is unchanged. + * + * A deployment that does not want a trusted forwarder inherits {IncomeVaultBase} directly. That is the + * point of the split (finding M-8): previously the forwarder came whether it was wanted or not, and + * opting out meant passing the zero address while still carrying the code and the calldata suffix + * handling on every call. + * + * @custom:security The forwarder is set in the constructor and is **immutable** — it lives in the + * implementation's bytecode, not in proxy storage, so it survives an upgrade only if the new + * implementation is deployed with the same address. A trusted forwarder can name any `_msgSender()`, + * so it is as privileged as every role behind it. + */ +abstract contract IncomeVaultBaseERC2771 is IncomeVaultBase, ERC2771Module { + /** + * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address forwarderIrrevocable) ERC2771Module(forwarderIrrevocable) {} + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ ERC-2771 / Context disambiguation ============ */ + /** + * @dev Resolves the {ERC2771ContextUpgradeable} / {ContextUpgradeable} diamond in favour of the + * ERC-2771 answer, so a forwarded call is attributed to the original sender. + * @return sender the forwarded sender when the call came through the trusted forwarder + */ + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, ContextUpgradeable) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + /** + * @dev Resolves the same diamond for the calldata, stripping the appended sender suffix. + * @return The calldata with the ERC-2771 suffix removed + */ + function _msgData() + internal + view + virtual + override(ERC2771ContextUpgradeable, ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } + + /** + * @dev Resolves the same diamond for the length of that suffix. + * @return The number of trailing calldata bytes carrying the forwarded sender + */ + function _contextSuffixLength() + internal + view + virtual + override(ERC2771ContextUpgradeable, ContextUpgradeable) + returns (uint256) + { + return ERC2771ContextUpgradeable._contextSuffixLength(); + } +} diff --git a/src/deployment/IncomeVault.sol b/src/deployment/IncomeVault.sol new file mode 100644 index 0000000..89b58cc --- /dev/null +++ b/src/deployment/IncomeVault.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== OpenZeppelin === */ +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {IERC7741} from "../interfaces/IERC7741.sol"; +import {IIncomeVault} from "../interfaces/IIncomeVault.sol"; +/* ==== CMTAT === */ +import {AccessControlModule} from "CMTAT/modules/wrapper/security/AccessControlModule.sol"; +import {PauseModule} from "CMTAT/modules/wrapper/core/PauseModule.sol"; +import {EnforcementModule} from "CMTAT/modules/wrapper/core/EnforcementModule.sol"; +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +/* ==== Snapshot === */ +import {ISnapshotSource} from "../interfaces/ISnapshotSource.sol"; +/* ==== IncomeVault === */ +import {IncomeVaultBaseERC2771} from "../IncomeVaultBaseERC2771.sol"; +import {IncomeVaultValidationModule} from "../modules/IncomeVaultValidationModule.sol"; +import {IncomeVaultRestricted} from "../public/IncomeVaultRestricted.sol"; +import {IncomeVaultSnapshotModule} from "../modules/IncomeVaultSnapshotModule.sol"; +import {IncomeVaultValidationModule} from "../modules/IncomeVaultValidationModule.sol"; +import {IncomeVaultRolesStorage} from "../storage/IncomeVaultRolesStorage.sol"; + +/** + * @title Income Vault to distribute dividends — role-based deployment + * @dev + * Answers **who** may do what: every authorization hook of {IncomeVaultBase} is overridden with the + * role that gates it. Suited to institutional operations, where funding the vault, withdrawing from + * it and running the claim window are held by different accounts. + * + * Note the CMTAT `AccessControlModule` treats `DEFAULT_ADMIN_ROLE` as implicitly holding every role: + * the admin passes every `hasRole` check but does **not** appear in role enumerations, so an + * off-chain tool listing role holders will not see them. Role separation therefore constrains the + * operators, never the admin. + */ +contract IncomeVault is + IncomeVaultValidationModule, + IncomeVaultBaseERC2771, + AccessControlModule, + IncomeVaultRolesStorage +{ + /** + * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address forwarderIrrevocable) IncomeVaultBaseERC2771(forwarderIrrevocable) { + // Disable the possibility to initialize the implementation + _disableInitializers(); + } + + /** + * @notice + * initialize the proxy contract + * The calls to this function will revert if the contract was deployed without a proxy + * @param admin Address of the contract (Access Control) + * @param ERC20TokenPayment_ ERC20 token used to perform the payment + * @param snapshotSource_ contract implementing {ISnapshotSource}, source of the holder balances + * @param ruleEngine_ optional RuleEngine applied to the payouts, or the zero address + * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted + */ + function initialize( + address admin, + IERC20 ERC20TokenPayment_, + ISnapshotSource snapshotSource_, + IRuleEngine ruleEngine_, + uint256 timeLimitToWithdraw_ + ) public initializer { + if (admin == address(0)) { + revert IncomeVault_AdminWithAddressZeroNotAllowed(); + } + __AccessControl_init_unchained(); + __AccessControlModule_init_unchained(admin); + // the validation answer this deployment chose + __Pausable_init_unchained(); + __IncomeVaultValidation_init_unchained(ruleEngine_); + __IncomeVaultBase_init_unchained(ERC20TokenPayment_, snapshotSource_, timeLimitToWithdraw_); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ ERC-165 ============ */ + /** + * @notice ERC-165 interface detection + * @dev Adds ERC-7741, whose specification requires a contract implementing it to answer `true` + * for `0xa9e50872`. The ERC-7540 operator id is deliberately **not** advertised — this is not an + * asynchronous vault; see {IERC7540Operator}. + * @param interfaceId The interface identifier to check + * @return True if the interface is supported, false otherwise + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControlUpgradeable) + returns (bool) + { + return interfaceId == type(IIncomeVault).interfaceId || interfaceId == type(IERC7741).interfaceId + || AccessControlUpgradeable.supportsInterface(interfaceId); + } + + /* ============ ERC-2771 / Context disambiguation ============ */ + /** + * @inheritdoc IncomeVaultBaseERC2771 + */ + function _msgSender() + internal + view + virtual + override(IncomeVaultBaseERC2771, ContextUpgradeable) + returns (address sender) + { + return IncomeVaultBaseERC2771._msgSender(); + } + + /** + * @inheritdoc IncomeVaultBaseERC2771 + */ + function _msgData() + internal + view + virtual + override(IncomeVaultBaseERC2771, ContextUpgradeable) + returns (bytes calldata) + { + return IncomeVaultBaseERC2771._msgData(); + } + + /** + * @inheritdoc IncomeVaultBaseERC2771 + */ + function _contextSuffixLength() + internal + view + virtual + override(IncomeVaultBaseERC2771, ContextUpgradeable) + returns (uint256) + { + return IncomeVaultBaseERC2771._contextSuffixLength(); + } + + /* ============ Access Control ============ */ + /// @inheritdoc IncomeVaultRestricted + function _authorizeDeposit() internal view virtual override onlyRole(INCOME_VAULT_DEPOSIT_ROLE) {} + + /// @inheritdoc IncomeVaultRestricted + function _authorizeWithdraw() internal view virtual override onlyRole(INCOME_VAULT_WITHDRAW_ROLE) {} + + /// @inheritdoc IncomeVaultRestricted + function _authorizeDistribute() internal view virtual override onlyRole(INCOME_VAULT_DISTRIBUTE_ROLE) {} + + /// @inheritdoc IncomeVaultRestricted + function _authorizeOperator() internal view virtual override onlyRole(INCOME_VAULT_OPERATOR_ROLE) {} + + /// @inheritdoc IncomeVaultSnapshotModule + function _authorizeSnapshotSourceManagement() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + + /// @inheritdoc IncomeVaultValidationModule + function _authorizeRuleEngineManagement() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + + /// @inheritdoc PauseModule + function _authorizePause() internal view virtual override(PauseModule) onlyRole(PAUSER_ROLE) {} + + /// @inheritdoc PauseModule + function _authorizeDeactivate() internal view virtual override(PauseModule) onlyRole(DEFAULT_ADMIN_ROLE) {} + + /// @inheritdoc EnforcementModule + function _authorizeFreeze() internal view virtual override(EnforcementModule) onlyRole(ENFORCER_ROLE) {} +} diff --git a/src/deployment/IncomeVaultOwnable2Step.sol b/src/deployment/IncomeVaultOwnable2Step.sol new file mode 100644 index 0000000..445b53e --- /dev/null +++ b/src/deployment/IncomeVaultOwnable2Step.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== OpenZeppelin === */ +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +/* ==== CMTAT === */ +import {PauseModule} from "CMTAT/modules/wrapper/core/PauseModule.sol"; +import {EnforcementModule} from "CMTAT/modules/wrapper/core/EnforcementModule.sol"; +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +/* ==== Snapshot === */ +import {ISnapshotSource} from "../interfaces/ISnapshotSource.sol"; +/* ==== IncomeVault === */ +import {IncomeVaultBaseERC2771} from "../IncomeVaultBaseERC2771.sol"; +import {IncomeVaultValidationModule} from "../modules/IncomeVaultValidationModule.sol"; +import {IncomeVaultRestricted} from "../public/IncomeVaultRestricted.sol"; +import {IncomeVaultSnapshotModule} from "../modules/IncomeVaultSnapshotModule.sol"; +import {IncomeVaultValidationModule} from "../modules/IncomeVaultValidationModule.sol"; +import {Ownable2StepERC165Module} from "../modules/Ownable2StepERC165Module.sol"; +import {IERC7741} from "../interfaces/IERC7741.sol"; +import {IIncomeVault} from "../interfaces/IIncomeVault.sol"; + +/** + * @title Income Vault to distribute dividends — single-owner deployment + * @dev + * Answers **who** may do what with a single ERC-173 owner: every authorization hook collapses to + * `onlyOwner`. `Ownable2Step` is used rather than `Ownable` so a mistyped address cannot lose the + * contract — the handover only completes when the new owner calls `acceptOwnership`. + * + * @custom:security This variant **cannot express separated duties**. The owner deposits, withdraws, + * distributes, runs the claim window, pauses, freezes and repoints the RuleEngine. In particular the + * account that funds the vault is the same account that can empty it through `withdrawAll`. Choose + * {IncomeVault}, the role-based deployment, whenever depositing and withdrawing must be held by + * different accounts — which is the usual requirement for an issuer paying dividends. + */ +contract IncomeVaultOwnable2Step is + IncomeVaultValidationModule, + IncomeVaultBaseERC2771, + Ownable2StepUpgradeable, + Ownable2StepERC165Module +{ + /** + * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address forwarderIrrevocable) IncomeVaultBaseERC2771(forwarderIrrevocable) { + // Disable the possibility to initialize the implementation + _disableInitializers(); + } + + /** + * @notice + * initialize the proxy contract + * The calls to this function will revert if the contract was deployed without a proxy + * @param owner_ Address of the initial contract owner (ERC-173) + * @param ERC20TokenPayment_ ERC20 token used to perform the payment + * @param snapshotSource_ contract implementing {ISnapshotSource}, source of the holder balances + * @param ruleEngine_ optional RuleEngine applied to the payouts, or the zero address + * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted + */ + function initialize( + address owner_, + IERC20 ERC20TokenPayment_, + ISnapshotSource snapshotSource_, + IRuleEngine ruleEngine_, + uint256 timeLimitToWithdraw_ + ) public initializer { + if (owner_ == address(0)) { + revert IncomeVault_AdminWithAddressZeroNotAllowed(); + } + __Ownable_init_unchained(owner_); + __Ownable2Step_init_unchained(); + __ERC165_init_unchained(); + // the validation answer this deployment chose + __Pausable_init_unchained(); + __IncomeVaultValidation_init_unchained(ruleEngine_); + __IncomeVaultBase_init_unchained(ERC20TokenPayment_, snapshotSource_, timeLimitToWithdraw_); + } + + /* ============ ERC-165 ============ */ + /** + * @inheritdoc Ownable2StepERC165Module + * @dev Adds ERC-7741, whose specification requires a contract implementing it to answer `true` + * for `0xa9e50872`. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(Ownable2StepERC165Module) + returns (bool) + { + return interfaceId == type(IIncomeVault).interfaceId || interfaceId == type(IERC7741).interfaceId + || Ownable2StepERC165Module.supportsInterface(interfaceId); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ ERC-2771 / Context disambiguation ============ */ + /** + * @inheritdoc IncomeVaultBaseERC2771 + */ + function _msgSender() + internal + view + virtual + override(IncomeVaultBaseERC2771, ContextUpgradeable) + returns (address sender) + { + return IncomeVaultBaseERC2771._msgSender(); + } + + /** + * @inheritdoc IncomeVaultBaseERC2771 + */ + function _msgData() + internal + view + virtual + override(IncomeVaultBaseERC2771, ContextUpgradeable) + returns (bytes calldata) + { + return IncomeVaultBaseERC2771._msgData(); + } + + /** + * @inheritdoc IncomeVaultBaseERC2771 + */ + function _contextSuffixLength() + internal + view + virtual + override(IncomeVaultBaseERC2771, ContextUpgradeable) + returns (uint256) + { + return IncomeVaultBaseERC2771._contextSuffixLength(); + } + + /* ============ Access Control ============ */ + /// @inheritdoc IncomeVaultRestricted + function _authorizeDeposit() internal view virtual override onlyOwner {} + + /// @inheritdoc IncomeVaultRestricted + function _authorizeWithdraw() internal view virtual override onlyOwner {} + + /// @inheritdoc IncomeVaultRestricted + function _authorizeDistribute() internal view virtual override onlyOwner {} + + /// @inheritdoc IncomeVaultRestricted + function _authorizeOperator() internal view virtual override onlyOwner {} + + /// @inheritdoc IncomeVaultSnapshotModule + function _authorizeSnapshotSourceManagement() internal view virtual override onlyOwner {} + + /// @inheritdoc IncomeVaultValidationModule + function _authorizeRuleEngineManagement() internal view virtual override onlyOwner {} + + /// @inheritdoc PauseModule + function _authorizePause() internal view virtual override(PauseModule) onlyOwner {} + + /// @inheritdoc PauseModule + function _authorizeDeactivate() internal view virtual override(PauseModule) onlyOwner {} + + /// @inheritdoc EnforcementModule + function _authorizeFreeze() internal view virtual override(EnforcementModule) onlyOwner {} +} diff --git a/src/interfaces/IERC7540Operator.sol b/src/interfaces/IERC7540Operator.sol new file mode 100644 index 0000000..e38cc9f --- /dev/null +++ b/src/interfaces/IERC7540Operator.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/** + * @title IERC7540Operator + * @notice The operator subset of [ERC-7540](https://eips.ethereum.org/EIPS/eip-7540), verbatim. + * @dev + * ERC-7540 defines asynchronous ERC-4626 vaults. The {IncomeVault} is **not** one — a 4626 share + * entitles whoever holds it now, while a dividend is allocated by record date — but its claim + * delegation is exactly the operator mechanism that standard specifies, so the signatures are reused + * rather than invented. A custodian or wallet already written against ERC-7540 operators works here unchanged. + * + * ERC-7540 assigns this subset the ERC-165 identifier **`0xe3bc4e65`**, described there as + * "the operator methods that all ERC-7540 Vaults implement". Because this interface inherits nothing, + * `type(IERC7540Operator).interfaceId` is exactly the XOR of the two selectors below and equals that + * value. That equality is what pins these signatures to the standard: change either one and the id no + * longer matches what ERC-7540 assigns. + * + * @custom:security The vault does **not** answer `true` for `0xe3bc4e65` from `supportsInterface`. + * Sharing the operator methods does not make it an asynchronous vault, and a caller discovering that + * id would reasonably expect the rest of ERC-7540 — the request lifecycle, ERC-7575's `share()` — none + * of which exists here. Deliberate under-claiming. + */ +interface IERC7540Operator { + /** + * @notice The `controller` has set the `approved` status to an `operator`. + * @dev MUST be logged when the operator status is set. + * @param controller the account granting or revoking + * @param operator the account being granted or revoked + * @param approved the status that was set + */ + event OperatorSet(address indexed controller, address indexed operator, bool approved); + + /** + * @notice Grants or revokes permissions for `operator` to manage Requests on behalf of the `msg.sender`. + * @dev MUST set the operator status to the `approved` value, MUST log the {OperatorSet} event and + * MUST return true. + * @param operator the account to grant or revoke + * @param approved true to grant, false to revoke + * @return success MUST be true + */ + function setOperator(address operator, bool approved) external returns (bool success); + + /** + * @notice Returns `true` if the `operator` is approved as an operator for a `controller`. + * @param controller the account that may have granted + * @param operator the account that may have been granted + * @return status true when `operator` is approved for `controller` + */ + function isOperator(address controller, address operator) external view returns (bool status); +} diff --git a/src/interfaces/IERC7741.sol b/src/interfaces/IERC7741.sol new file mode 100644 index 0000000..d0b0c6c --- /dev/null +++ b/src/interfaces/IERC7741.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/** + * @title IERC7741 + * @notice [ERC-7741](https://eips.ethereum.org/EIPS/eip-7741) — signed operator authorisation. + * @dev + * Lets a holder grant or revoke an operator with an EIP-712 signature instead of a transaction, so a + * custodian or relayer can submit the authorisation and pay the gas. It complements + * {IERC7540Operator}, whose `setOperator` requires the holder to transact. + * + * The standard assigns this interface the ERC-165 identifier **`0xa9e50872`**. It inherits nothing, + * so `type(IERC7741).interfaceId` is the XOR of the four selectors below and equals that value. Adding + * or changing a selector here changes the id, and the vault would then advertise one the standard does + * not define. + * + * @custom:security ERC-7741 warns that "operators have significant control over users and the signed + * message can lead to undesired outcomes". Keep `deadline` as short as practical: a signature that + * leaks later is still usable until it expires or its nonce is spent through {invalidateNonce}. + */ +interface IERC7741 { + /** + * @notice Grants or revokes permissions for `operator`, authorised by an EIP-712 signature. + * @dev MUST revert if `deadline` has passed, if the nonce was already used, or if the signature + * is invalid. MUST invalidate the nonce, MUST log `OperatorSet` and MUST return true. + * @param controller the holder whose signature authorises the change + * @param operator the account being granted or revoked + * @param approved true to grant, false to revoke + * @param nonce an unordered, single-use value chosen by the signer + * @param deadline the timestamp after which the signature is no longer valid + * @param signature the EIP-712 signature, ECDSA or ERC-1271 + * @return success MUST be true + */ + function authorizeOperator( + address controller, + address operator, + bool approved, + bytes32 nonce, + uint256 deadline, + bytes memory signature + ) external returns (bool success); + + /** + * @notice Revokes the given `nonce` for `msg.sender`, so a signature using it can never be used. + * @param nonce the nonce to burn + */ + function invalidateNonce(bytes32 nonce) external; + + /** + * @notice Returns whether the given `nonce` has been used for the `controller`. + * @param controller the holder the nonce belongs to + * @param nonce the nonce to check + * @return used true when the nonce has been spent or invalidated + */ + function authorizations(address controller, bytes32 nonce) external view returns (bool used); + + /** + * @notice The EIP-712 domain separator of this contract. + * @return The domain separator, unique to this contract and chain + */ + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/src/interfaces/IIncomeVault.sol b/src/interfaces/IIncomeVault.sol new file mode 100644 index 0000000..a7a1837 --- /dev/null +++ b/src/interfaces/IIncomeVault.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title The dividend-distribution API, stated rather than inferred + * @dev + * Everything an integrator calls on a vault — or on a token that embeds the distribution logic — with + * no dependency on which of the two it is. Importing this instead of a concrete contract avoids pulling + * in CMTAT, the RuleEngine and the whole implementation graph. + * + * Scope, and what is deliberately outside it: + * + * - **Operator delegation is not redeclared here.** `setOperator`/`isOperator` belong to + * {IERC7540Operator} and the signed variant to {IERC7741}; both are implemented alongside this + * interface. Restating them would fork a standardised name. + * - **`transferDividendSelf` is absent.** It is `public` only because `try`/`catch` needs an external + * call, and it rejects every caller but the contract itself. It is not part of anyone's API. + * - **Access control is absent.** Who may call {deposit} or {withdraw} is chosen by the deployment + * contract, not by this interface. + * + * This interface is inherited by {IncomeVaultInternal}, the common base of both payout paths, so the + * compiler — not a convention — keeps it in step with the implementation. + */ +interface IIncomeVault { + /* ============ Type declarations ============ */ + /** + * @notice Why a dividend time is not claimable, or `OK` + * @dev Declared here rather than in the implementation because it is part of the stated API: + * {validateTimeCode} returns it. Both the holder-driven claims ({IncomeVaultOpen}) and the + * issuer-driven distribution ({IncomeVaultRestricted}) apply the same window through it. + */ + enum TIME_ERROR_CODE { + OK, + CLAIM_NOT_ACTIVATED, + TOO_LATE_TO_WITHDRAW, + TOO_EARLY_TO_WITHDRAW + } + + /* ============ Claiming — permissionless ============ */ + /** + * @notice Claim the caller's dividends for one distribution date + * @param time the dividend time identifying the distribution + */ + function claimDividend(uint256 time) external; + + /** + * @notice Claim `holder`'s dividends for one distribution date, as the holder or their operator + * @param holder the token holder the dividends are paid to + * @param time the dividend time identifying the distribution + */ + function claimDividendFor(address holder, uint256 time) external; + + /** + * @notice Claim the caller's dividends for several distribution dates + * @param times the dividend times to claim + */ + function claimDividendBatch(uint256[] calldata times) external; + + /** + * @notice Claim `holder`'s dividends for several dates, as the holder or their operator + * @param holder the token holder the dividends are paid to + * @param times the dividend times to claim + */ + function claimDividendBatchFor(address holder, uint256[] calldata times) external; + + /* ============ Funding — role gated ============ */ + /** + * @notice Deposit the payment token for one distribution date + * @param time the dividend time the deposit is segregated under + * @param amount the amount of payment token to deposit + */ + function deposit(uint256 time, uint256 amount) external; + + /** + * @notice Deposit the payment token for several distribution dates in one call + * @param times the dividend times to deposit for + * @param amounts the amount to deposit for each time, index for index + */ + function depositBatch(uint256[] calldata times, uint256[] calldata amounts) external; + + /** + * @notice Recover unclaimed payment token from one distribution date + * @param time the dividend time to withdraw from + * @param amount the amount of payment token to withdraw + * @param withdrawAddress the recipient of the withdrawn funds + */ + function withdraw(uint256 time, uint256 amount, address withdrawAddress) external; + + /** + * @notice Recover payment token held by the contract without naming a distribution date + * @param amount the amount of payment token to withdraw + * @param withdrawAddress the recipient of the withdrawn funds + */ + function withdrawAll(uint256 amount, address withdrawAddress) external; + + /* ============ Pushing payouts — role gated ============ */ + /** + * @notice Pay several holders their dividends for one date, reverting if any payout is refused + * @param addresses the token holders to pay + * @param time the dividend time identifying the distribution + */ + function distributeDividend(address[] calldata addresses, uint256 time) external; + + /** + * @notice Pay several holders for one date, skipping the refused payouts instead of reverting + * @param addresses the token holders to pay + * @param time the dividend time identifying the distribution + * @return paidCount how many holders were actually paid + * @return skipped the holders whose payout was refused + */ + function distributeDividendBestEffort(address[] calldata addresses, uint256 time) + external + returns (uint256 paidCount, address[] memory skipped); + + /* ============ Claim administration — role gated ============ */ + /** + * @notice Open or close claiming for one distribution date + * @param time the dividend time + * @param status true to let holders claim, false to close the period + */ + function setStatusClaim(uint256 time, bool status) external; + + /** + * @notice Set how long after a dividend time a claim is still accepted + * @param timeLimitToWithdraw_ the length of the claim window, in seconds + */ + function setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) external; + + /* ============ Claim window ============ */ + /** + * @notice Reverts unless a claim for `time` would be accepted right now + * @param time the dividend time to check + */ + function validateTime(uint256 time) external view; + + /** + * @notice Reverts unless a claim for every one of `times` would be accepted right now + * @param times the dividend times to check + */ + function validateTimeBatch(uint256[] calldata times) external view; + + /** + * @notice Why a claim for `time` would be refused, without reverting + * @param time the dividend time to check + * @return code the reason, or the no-error member when the claim would be accepted + */ + function validateTimeCode(uint256 time) external view returns (TIME_ERROR_CODE code); + + /* ============ State ============ */ + /** + * @notice The ERC-20 the dividends are paid in + * @return The payment token + */ + function ERC20TokenPayment() external view returns (IERC20); + + /** + * @notice Whether a holder has already claimed a given distribution + * @param tokenHolder the holder to look up + * @param time the dividend time + * @return True once the holder has been paid for `time` + */ + function claimedDividend(address tokenHolder, uint256 time) external view returns (bool); + + /** + * @notice The total deposited for a distribution date. This is the pro-rata denominator and is + * never reduced by a payout — see {unclaimedDividend} for what the period still holds. + * @param time the dividend time + * @return The amount deposited for `time` + */ + function segregatedDividend(uint256 time) external view returns (uint256); + + /** + * @notice Whether claiming is open for a distribution date + * @param time the dividend time + * @return True when holders may claim for `time` + */ + function segregatedClaim(uint256 time) external view returns (bool); + + /** + * @notice How much of a date's deposit has already been paid out + * @param time the dividend time + * @return The amount already paid for `time` + */ + function paidDividend(uint256 time) external view returns (uint256); + + /** + * @notice How much of a date's deposit the contract still holds + * @param time the dividend time + * @return `segregatedDividend(time) - paidDividend(time)`, saturating at zero + */ + function unclaimedDividend(uint256 time) external view returns (uint256); + + /** + * @notice How many distribution dates currently have claiming open + * @return The number of open claim periods + */ + function openClaimCount() external view returns (uint256); + + /** + * @notice How long after a dividend time a claim is still accepted + * @return The claim window length, in seconds + */ + function timeLimitToWithdraw() external view returns (uint256); +} diff --git a/src/interfaces/ISnapshotSource.sol b/src/interfaces/ISnapshotSource.sol new file mode 100644 index 0000000..9f8dafe --- /dev/null +++ b/src/interfaces/ISnapshotSource.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/** + * @title ISnapshotSource + * @notice The read surface the {IncomeVault} needs from a snapshot provider — nothing more. + * @dev + * This is the **minimum** a contract must expose to be usable as the vault's snapshot source. It is a + * strict subset of `ISnapshotState` (defined by the CMTA + * [SnapshotEngine](https://github.com/CMTA/SnapshotEngine)), which declares eight functions where the + * vault calls three; the five it does not call describe balances and supplies the vault never reads. + * + * The signatures are copied verbatim from `ISnapshotState`, so **every `ISnapshotState` + * implementation already satisfies this interface** — the `SnapshotEngine`, a token embedding the + * snapshot modules, or a custom provider. Solidity has no implicit conversion between unrelated + * interfaces, so pass one with an explicit cast: `ISnapshotSource(address(engine))`. + * + * @custom:security The vault does **not** verify this interface through ERC-165, deliberately. The + * canonical `SnapshotEngine` does not advertise an id for it, so a guard would reject the very + * implementation the vault is built for. And ERC-165 expresses shape, never semantics: a provider + * returning attacker-chosen balances satisfies this interface exactly as an honest one does. Trusting + * the snapshot source remains a configuration decision, not something a type or an interface check can + * establish. + */ +interface ISnapshotSource { + /** + * @notice Retrieve both an account's balance and the total supply at the snapshot for a given timestamp in a single call. + * @param time The timestamp identifying the snapshot to query. + * @param tokenHolder The address whose balance is being requested. + * @return tokenHolderBalance The recorded balance of the tokenHolder at the snapshot (or current balance if no snapshot). + * @return totalSupply The recorded total supply at the snapshot (or current total supply if no snapshot). + */ + function snapshotInfo(uint256 time, address tokenHolder) + external + view + returns (uint256 tokenHolderBalance, uint256 totalSupply); + + /** + * @notice Retrieve the balances of multiple accounts and the total supply at the snapshot for a given timestamp in a single call. + * @param time The timestamp identifying the snapshot to query. + * @param addresses The array of addresses to query balances for. + * @return tokenHolderBalances An array containing each address's balance at the snapshot (or current balance if no snapshot). + * @return totalSupply The recorded total supply at the snapshot (or current total supply if no snapshot). + */ + function snapshotInfoBatch(uint256 time, address[] calldata addresses) + external + view + returns (uint256[] memory tokenHolderBalances, uint256 totalSupply); + + /** + * @notice Retrieve balances of multiple accounts at multiple snapshots, as well as the total supply at each snapshot. + * @param times An array of timestamps identifying each snapshot to query. + * @param addresses The array of addresses to query balances for at each snapshot. + * @return tokenHolderBalances A 2D array where each row corresponds to the balances of all provided addresses at a given snapshot time. + * @return totalSupplies An array containing the total supply at each snapshot time (or current supply if no snapshot). + */ + function snapshotInfoBatch(uint256[] calldata times, address[] calldata addresses) + external + view + returns (uint256[][] memory tokenHolderBalances, uint256[] memory totalSupplies); +} diff --git a/src/libraries/IncomeVaultInternal.sol b/src/libraries/IncomeVaultInternal.sol deleted file mode 100644 index 7bc5b78..0000000 --- a/src/libraries/IncomeVaultInternal.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -pragma solidity ^0.8.20; - -import "OZ/token/ERC20/utils/SafeERC20.sol"; -import "./IncomeVaultInvariantStorage.sol"; -import "CMTAT/interfaces/ICMTATSnapshot.sol"; -/** -* @title Internal functions -*/ -abstract contract IncomeVaultInternal is IncomeVaultInvariantStorage { - // CMTAT token - ICMTATSnapshot public CMTAT_TOKEN; - IERC20 public ERC20TokenPayment; - mapping(address => mapping (uint256 => bool)) public claimedDividend; - mapping(uint256 => uint256) public segregatedDividend; - mapping(uint256 => bool) public segregatedClaim; - uint256 public timeLimitToWithdraw; - - // Manage transfer failure - using SafeERC20 for IERC20; - - /** - * @param time dividend time - * @param tokenHolders addresses to compute dividend - * @param tokenHoldersBalance the sender balance - * @param tokenTotalSupply the total supply - */ - function _computeDividendBatch(uint256 time, address[] calldata tokenHolders, uint256[] memory tokenHoldersBalance, uint256 tokenTotalSupply) internal view returns(uint256[] memory tokenHolderDividend){ - tokenHolderDividend = new uint256[](tokenHolders.length); - uint256 dividendTotalSupply = segregatedDividend[time]; - for(uint256 i = 0; i < tokenHolders.length; ++i){ - if(tokenHoldersBalance[i] > 0) { - tokenHolderDividend[i] = (tokenHoldersBalance[i] * dividendTotalSupply) / tokenTotalSupply; - } - } - } - - /** - * @param time dividend time - * @param senderBalance token holder balance - * @param tokenTotalSupply the total supply - */ - function _computeDividend(uint256 time, uint256 senderBalance, uint256 tokenTotalSupply) internal view returns(uint256 tokenHolderDividend){ - if (senderBalance == 0){ - revert IncomeVault_NoDividendToClaim(); - } - /** - * Example - * SenderBalance = 300 - * totalSupply = 900 - * Dividend total supply = 200 - * dividend = (300 * 200) / 900 = 60000 / 900 = 600/9 = 66.6 = 66 - */ - uint256 dividendTotalSupply = segregatedDividend[time]; - - tokenHolderDividend = (senderBalance * dividendTotalSupply) / tokenTotalSupply; - } - - /** - * @param time dividend time - * @param tokenHolder addresses to send the dividends - * @param tokenHolderDividend the computed dividends - */ - function _transferDividend(uint256 time, address tokenHolder, uint256 tokenHolderDividend) internal{ - // Before ERC-20 transfer to avoid re-entrancy attack - claimedDividend[tokenHolder][time] = true; - emit DividendClaimed(time, tokenHolder, tokenHolderDividend); - // transfer - // We don't revert if SenderBalance == 0 to record the claim - if(tokenHolderDividend != 0){ - // Will revert in case of failure - // We should put that in a try catch for the batch version ??? - ERC20TokenPayment.safeTransfer(tokenHolder, tokenHolderDividend); - } - } -} diff --git a/src/libraries/IncomeVaultInvariantStorage.sol b/src/libraries/IncomeVaultInvariantStorage.sol deleted file mode 100644 index e06a6eb..0000000 --- a/src/libraries/IncomeVaultInvariantStorage.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -pragma solidity ^0.8.20; - -abstract contract IncomeVaultInvariantStorage { - // Role - bytes32 public constant INCOME_VAULT_OPERATOR_ROLE = keccak256("INCOME_VAULT_OPERATOR_ROLE"); - bytes32 public constant INCOME_VAULT_DEPOSIT_ROLE = keccak256("INCOME_VAULT_DEPOSIT_ROLE"); - bytes32 public constant INCOME_VAULT_DISTRIBUTE_ROLE = keccak256("INCOME_VAULT_DEPOSIT_ROLE"); - bytes32 public constant INCOME_VAULT_WITHDRAW_ROLE = keccak256("INCOME_VAULT_WITHDRAW_ROLE"); - - // errors - error IncomeVault_ClaimNotActivated(); - error IncomeVault_DividendAlreadyClaimed(); - error IncomeVault_NoDividendToClaim(); - error IncomeVault_AdminWithAddressZeroNotAllowed(); - error IncomeVault_TokenPaymentWithAddressZeroNotAllowed(); - error IncomeVault_CMTATWithAddressZeroNotAllowed(); - error IncomeVault_FailApproval(); - error IncomeVault_NoAmountSend(); - error IncomeVault_NotEnoughAmount(); - error IncomeVault_TokenBalanceIsZero(); - error IncomeVault_TooLateToWithdraw(uint256 currentTime); - error IncomeVault_TooEarlyToWithdraw(uint256 currentTime); - - // event - event newDeposit(uint256 indexed time, address indexed sender, uint256 dividend); - event DividendClaimed(uint256 indexed time, address indexed sender, uint256 dividend); -} \ No newline at end of file diff --git a/src/modules/ERC7741Module.sol b/src/modules/ERC7741Module.sol new file mode 100644 index 0000000..293b9cd --- /dev/null +++ b/src/modules/ERC7741Module.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== OpenZeppelin === */ +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +/* ==== IncomeVault === */ +import {IncomeVaultInternal} from "./IncomeVaultInternal.sol"; +import {IncomeVaultOperatorModule} from "./IncomeVaultOperatorModule.sol"; +import {IERC7741} from "../interfaces/IERC7741.sol"; + +/** + * @title ERC-7741 signed operator authorisation + * @dev + * Implements [ERC-7741](https://eips.ethereum.org/EIPS/eip-7741) on top of the operator mapping owned + * by {IncomeVaultInternal}: a holder signs an EIP-712 message and anyone can submit it, so the holder + * never needs gas or even an on-chain transaction to appoint a custodian. + * + * Signatures are checked with OpenZeppelin's `SignatureChecker`, so an **ERC-1271 smart-contract + * wallet** authorises exactly as an EOA does — which matters here, because institutional holders of a + * security token are usually contracts rather than externally owned accounts. + * + * Nonces are `bytes32` and unordered, as the standard specifies, so a holder can prepare several + * independent authorisations without imposing an ordering on them. + */ +abstract contract ERC7741Module is + EIP712Upgradeable, + ContextUpgradeable, + IncomeVaultOperatorModule, + IncomeVaultInternal, + IERC7741 +{ + /* ============ State Variables ============ */ + /** + * @notice EIP-712 type hash of the authorisation message, exactly as ERC-7741 defines it + */ + bytes32 public constant AUTHORIZE_OPERATOR_TYPEHASH = keccak256( + "AuthorizeOperator(address controller,address operator,bool approved,bytes32 nonce,uint256 deadline)" + ); + + /* ============ ERC-7201 ============ */ + /** + * @dev Slot holding the ERC-7201 namespaced storage of this module, derived as + * keccak256(abi.encode(uint256(keccak256("IncomeVault.storage.ERC7741Module")) - 1)) & ~bytes32(uint256(0xff)) + * Recompute it with `SlotDerivation.erc7201Slot()` before trusting a change to it. + */ + bytes32 private constant ERC7741ModuleStorageLocation = + 0xb93ff011b98f03386917a7b9b9106f5d9f85ba058e0b4e9b3aad1f6474a96800; + + /* ==== ERC-7201 State Variables === */ + /// @custom:storage-location erc7201:IncomeVault.storage.ERC7741Module + struct ERC7741ModuleStorage { + // Nonces already spent, per holder. Set both by a successful authorisation and by + // {invalidateNonce}, so a holder can burn a signature they no longer want honoured. + mapping(address controller => mapping(bytes32 nonce => bool used)) _authorizations; + } + + /* ============ Errors ============ */ + /// @notice Thrown when the signature's deadline has passed. + error IncomeVault_AuthorizationExpired(uint256 deadline); + /// @notice Thrown when the nonce was already spent or invalidated. + error IncomeVault_AuthorizationUsed(address controller, bytes32 nonce); + /// @notice Thrown when the signature does not recover to `controller`. + error IncomeVault_InvalidAuthorization(address controller); + /// @notice Thrown when the controller is the zero address. + error IncomeVault_ControllerWithAddressZeroNotAllowed(); + + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State functions ============ */ + /// @inheritdoc IERC7741 + function authorizeOperator( + address controller, + address operator, + bool approved, + bytes32 nonce, + uint256 deadline, + bytes memory signature + ) public virtual override(IERC7741) returns (bool success) { + if (block.timestamp > deadline) { + revert IncomeVault_AuthorizationExpired(deadline); + } + if (controller == address(0)) { + revert IncomeVault_ControllerWithAddressZeroNotAllowed(); + } + ERC7741ModuleStorage storage $ = _getERC7741ModuleStorage(); + if ($._authorizations[controller][nonce]) { + revert IncomeVault_AuthorizationUsed(controller, nonce); + } + // Spend the nonce before validating, so no path can replay it. + $._authorizations[controller][nonce] = true; + + bytes32 digest = _hashTypedDataV4( + keccak256(abi.encode(AUTHORIZE_OPERATOR_TYPEHASH, controller, operator, approved, nonce, deadline)) + ); + // SignatureChecker accepts both ECDSA and ERC-1271, so contract wallets work unchanged. + if (!SignatureChecker.isValidSignatureNow(controller, digest, signature)) { + revert IncomeVault_InvalidAuthorization(controller); + } + + _setOperator(controller, operator, approved); + return true; + } + + /// @inheritdoc IERC7741 + function invalidateNonce(bytes32 nonce) public virtual override(IERC7741) { + ERC7741ModuleStorage storage $ = _getERC7741ModuleStorage(); + $._authorizations[_msgSender()][nonce] = true; + } + + /* ============ View functions ============ */ + /// @inheritdoc IERC7741 + function authorizations(address controller, bytes32 nonce) + public + view + virtual + override(IERC7741) + returns (bool used) + { + ERC7741ModuleStorage storage $ = _getERC7741ModuleStorage(); + return $._authorizations[controller][nonce]; + } + + /// @inheritdoc IERC7741 + function DOMAIN_SEPARATOR() public view virtual override(IERC7741) returns (bytes32) { + return _domainSeparatorV4(); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ ERC-7201 ============ */ + /** + * @dev Returns the ERC-7201 namespaced storage of this module + * @return $ the storage struct + */ + function _getERC7741ModuleStorage() internal pure returns (ERC7741ModuleStorage storage $) { + assembly { + $.slot := ERC7741ModuleStorageLocation + } + } +} diff --git a/src/modules/IncomeVaultInternal.sol b/src/modules/IncomeVaultInternal.sol new file mode 100644 index 0000000..e8929ab --- /dev/null +++ b/src/modules/IncomeVaultInternal.sol @@ -0,0 +1,396 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== OpenZeppelin === */ +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +/* ==== Snapshot === */ +/* ==== IncomeVault === */ +import {IncomeVaultInvariantStorage} from "../storage/IncomeVaultInvariantStorage.sol"; +import {IIncomeVault} from "../interfaces/IIncomeVault.sol"; + +/** + * @title Internal functions and ERC-7201 storage of the IncomeVault + * @dev + * Holds the dividend bookkeeping. The snapshot source is deliberately **not** here — see + * {IncomeVaultSnapshotCore} — so a host that is its own source does not inherit an unused reference. + * + * The state is held in an ERC-7201 namespaced storage struct, as OpenZeppelin Upgradeable and the + * CMTAT do. The namespace is derived from a hash, so it cannot collide with the storage of the + * inherited modules; no `__gap` is needed and new fields can be appended to the struct freely. + */ +abstract contract IncomeVaultInternal is IncomeVaultInvariantStorage, IIncomeVault { + // Manage transfer failure + using SafeERC20 for IERC20; + + /* ============ Type declarations ============ */ + /* ============ ERC-7201 ============ */ + /** + * @dev Slot holding the ERC-7201 namespaced storage of this module, derived as + * keccak256(abi.encode(uint256(keccak256("IncomeVault.storage.IncomeVaultInternal")) - 1)) & ~bytes32(uint256(0xff)) + * Recompute it with `SlotDerivation.erc7201Slot()` before trusting a change to it. + */ + bytes32 private constant IncomeVaultInternalStorageLocation = + 0xe4f8b033bcfc537db031b0e68e3c1ab0f1de86cf03893d031b6590510b0c0c00; + + /* ==== ERC-7201 State Variables === */ + /// @custom:storage-location erc7201:IncomeVault.storage.IncomeVaultInternal + struct IncomeVaultInternalStorage { + // ERC-20 token used to pay the dividends + IERC20 _ERC20TokenPayment; + // Records, per token holder and per dividend time, whether the dividends were claimed + mapping(address tokenHolder => mapping(uint256 time => bool claimed)) _claimedDividend; + // Total amount of payment token deposited for a given dividend time + mapping(uint256 time => uint256 dividend) _segregatedDividend; + // Claim status, per dividend time: true when the holders can claim + mapping(uint256 time => bool status) _segregatedClaim; + // Delay, after the dividend time, during which a claim is still accepted + uint256 _timeLimitToWithdraw; + // How many dividend times currently have their claims open. Appended after the fields above: + // ERC-7201 struct members are append-only, never reordered. + uint256 _openClaimCount; + // Total already paid out for a dividend time. `_segregatedDividend` is deliberately NOT + // reduced on a payout — it is the pro-rata denominator and must stay fixed for the period — + // so this is what makes "how much of that deposit is still here" answerable. + mapping(uint256 time => uint256 paid) _paidDividend; + } + + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ View functions ============ */ + /** + * @notice ERC-20 token used to pay the dividends + * @return The payment token + */ + function ERC20TokenPayment() public view virtual returns (IERC20) { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + return $._ERC20TokenPayment; + } + + /** + * @notice Tells whether a token holder already claimed the dividends of a given time + * @param tokenHolder the address to check + * @param time the dividend time + * @return True if the dividends were already claimed or distributed + */ + function claimedDividend(address tokenHolder, uint256 time) public view virtual returns (bool) { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + return $._claimedDividend[tokenHolder][time]; + } + + /** + * @notice Total amount of payment token deposited for a given dividend time + * @param time the dividend time + * @return The amount deposited, minus what was already withdrawn + */ + function segregatedDividend(uint256 time) public view virtual returns (uint256) { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + return $._segregatedDividend[time]; + } + + /** + * @notice Claim status of a given dividend time + * @param time the dividend time + * @return True when the token holders can claim their dividends + */ + function segregatedClaim(uint256 time) public view virtual returns (bool) { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + return $._segregatedClaim[time]; + } + + /** + * @notice Total already paid out for a dividend time + * @param time the dividend time + * @return The amount of payment token already transferred to holders for `time` + */ + function paidDividend(uint256 time) public view virtual returns (uint256) { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + return $._paidDividend[time]; + } + + /** + * @notice What is still held for a dividend time — the deposit minus what has been paid out + * @dev + * This is the amount an issuer can sweep with {IncomeVaultRestricted-withdraw}, and it is the bound + * that function enforces. `segregatedDividend` alone is **not** that amount: it is the pro-rata + * denominator and stays fixed at the deposit even after holders are paid. + * + * After the claim window closes it is exactly the rounding dust plus anything unclaimed. Before it + * closes it still includes what the remaining holders are entitled to, so sweeping early takes + * money they can no longer be paid — see the note on {IncomeVaultRestricted-withdraw}. + * @param time the dividend time + * @return The amount of payment token still attributable to `time` + */ + function unclaimedDividend(uint256 time) public view virtual returns (uint256) { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + return _unclaimed($._segregatedDividend[time], $._paidDividend[time]); + } + + /** + * @notice How many dividend times currently have their claims open + * @dev Maintained exactly by {_setStatusClaim}, the only writer of the claim status. Used by + * {IncomeVaultSnapshotModule-setDividendSnapshotSource}, which refuses to change the snapshot source while any + * period is open. + * @return The number of open claim periods + */ + function openClaimCount() public view virtual returns (uint256) { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + return $._openClaimCount; + } + + /** + * @notice Delay, after the dividend time, during which a claim is still accepted + * @return The delay in seconds + */ + function timeLimitToWithdraw() public view virtual returns (uint256) { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + return $._timeLimitToWithdraw; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State functions ============ */ + /** + * @notice Records the claim then sends the dividends to the token holder + * @param time dividend time + * @param tokenHolder addresses to send the dividends + * @param tokenHolderDividend the computed dividends + */ + function _transferDividend(uint256 time, address tokenHolder, uint256 tokenHolderDividend) internal virtual { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + // Before ERC-20 transfer to avoid re-entrancy attack + $._claimedDividend[tokenHolder][time] = true; + emit DividendClaimed(time, tokenHolder, tokenHolderDividend); + // transfer + // We don't revert if SenderBalance == 0 to record the claim + if (tokenHolderDividend != 0) { + // A payout must come out of its own period. Without this a claim made after the period + // was swept mid-window would silently be funded from another period's deposit, leaving + // that one unable to pay its holders. Unreachable in normal operation: the entitlements + // of a period always sum to at most its deposit. + uint256 paid = $._paidDividend[time]; + if (tokenHolderDividend > _unclaimed($._segregatedDividend[time], paid)) { + revert IncomeVault_NotEnoughAmount(); + } + $._paidDividend[time] = paid + tokenHolderDividend; + // Will revert in case of failure + $._ERC20TokenPayment.safeTransfer(tokenHolder, tokenHolderDividend); + } + } + + /** + * @notice Sets the ERC-20 token used to pay the dividends + * @dev reverts if `ERC20TokenPayment_` is the zero address + * @param ERC20TokenPayment_ the payment token + */ + function _setERC20TokenPayment(IERC20 ERC20TokenPayment_) internal virtual { + if (address(ERC20TokenPayment_) == address(0)) { + revert IncomeVault_TokenPaymentWithAddressZeroNotAllowed(); + } + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + $._ERC20TokenPayment = ERC20TokenPayment_; + emit ERC20TokenPaymentSet(ERC20TokenPayment_); + } + + /** + * @notice Sets the delay, after the dividend time, during which a claim is still accepted + * @dev reverts if `timeLimitToWithdraw_` is zero — see {IncomeVault_TimeLimitToWithdrawZeroNotAllowed} + * @param timeLimitToWithdraw_ the delay in seconds, must be greater than zero + */ + function _setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) internal virtual { + // Zero collapses the claim window to the single instant `block.timestamp == time`: one second + // later {_timeCode} already returns TOO_LATE_TO_WITHDRAW and the period is unclaimable. Any + // positive value is allowed — a short settlement window may be deliberate; zero never is. + if (timeLimitToWithdraw_ == 0) { + revert IncomeVault_TimeLimitToWithdrawZeroNotAllowed(); + } + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + $._timeLimitToWithdraw = timeLimitToWithdraw_; + emit TimeLimitToWithdrawSet(timeLimitToWithdraw_); + } + + /** + * @notice Records a deposit against a dividend time + * @dev + * The single writer of `_segregatedDividend`, and the only place `newDeposit` is emitted. Both + * funding paths go through it — {IncomeVaultRestricted-deposit} once, + * {IncomeVaultRestricted-depositBatch} once per element — so validating, writing and announcing a + * deposit cannot come apart. Each path carrying its own copy is what lets them diverge, so a new + * funding path must call this rather than repeat it. + * + * The ERC-20 transfer is deliberately **not** here. `depositBatch` makes a single + * `safeTransferFrom` for the whole batch, which is the reason it exists; folding the transfer in + * would turn that back into one transfer per element. + * + * Takes the storage pointer rather than fetching it, as {_timeCode} does, so a batch acquires it + * once instead of once per element. + * @param $ the ERC-7201 storage of the vault + * @param sender the account funding the deposit, reported by the event + * @param time the dividend time the deposit is segregated under + * @param amount the amount of payment token, which may not be zero + */ + function _deposit(IncomeVaultInternalStorage storage $, address sender, uint256 time, uint256 amount) + internal + virtual + { + if (amount == 0) { + revert IncomeVault_NoAmountSend(); + } + $._segregatedDividend[time] += amount; + emit newDeposit(time, sender, amount); + } + + /** + * @notice Opens or closes the claims for a dividend time + * @param time the dividend time + * @param status true when the token holders can claim + */ + function _setStatusClaim(uint256 time, bool status) internal virtual { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + // Idempotent: a call that does not change the status writes nothing, emits nothing and — the + // reason this branch exists — leaves `_openClaimCount` exact. Without it, opening an already + // open period would double-count and the counter could never return to zero. + if ($._segregatedClaim[time] == status) { + return; + } + $._segregatedClaim[time] = status; + if (status) { + ++$._openClaimCount; + } else { + --$._openClaimCount; + } + emit ClaimStatusSet(time, status); + } + + /* ============ View functions ============ */ + /** + * @dev How much of a period's deposit is still held, given the two figures that decide it. + * + * Saturating, not a plain subtraction. Withdrawing mid-period lowers the denominator, so a claim + * made afterwards is priced against the reduced figure and can push `paid` above `segregated`. That + * state means the period is over-drawn and nothing is left to sweep — this must report zero, never + * revert. + * + * One `pure` rule because both callers must agree on it: {unclaimedDividend} reports it, and + * {_transferDividend} enforces it as the bound on a payout. Were they to diverge, a payout could be + * funded from another period's deposit. + * @param segregated the amount deposited for the period, the pro-rata denominator + * @param paid the amount already paid out of that period + * @return The amount still attributable to the period, or zero when it is over-drawn + */ + function _unclaimed(uint256 segregated, uint256 paid) internal pure virtual returns (uint256) { + return segregated > paid ? segregated - paid : 0; + } + + /** + * @notice Computes the dividends owed to several token holders for a given time + * @param time dividend time + * @param tokenHolders addresses to compute dividend + * @param tokenHoldersBalance the sender balance + * @param tokenTotalSupply the total supply + * @return tokenHolderDividend the dividends owed to each address of `tokenHolders` + */ + function _computeDividendBatch( + uint256 time, + address[] calldata tokenHolders, + uint256[] memory tokenHoldersBalance, + uint256 tokenTotalSupply + ) internal view virtual returns (uint256[] memory tokenHolderDividend) { + tokenHolderDividend = new uint256[](tokenHolders.length); + uint256 dividendTotalSupply = segregatedDividend(time); + for (uint256 i = 0; i < tokenHolders.length; ++i) { + if (tokenHoldersBalance[i] > 0) { + tokenHolderDividend[i] = (tokenHoldersBalance[i] * dividendTotalSupply) / tokenTotalSupply; + } + } + } + + /** + * @notice Computes the dividends owed to a single token holder for a given time + * @param time dividend time + * @param senderBalance token holder balance + * @param tokenTotalSupply the total supply + * @return tokenHolderDividend the dividends owed to the token holder, rounded down + */ + function _computeDividend(uint256 time, uint256 senderBalance, uint256 tokenTotalSupply) + internal + view + virtual + returns (uint256 tokenHolderDividend) + { + if (senderBalance == 0) { + revert IncomeVault_NoDividendToClaim(); + } + /** + * Example + * SenderBalance = 300 + * totalSupply = 900 + * Dividend total supply = 200 + * dividend = (300 * 200) / 900 = 60000 / 900 = 600/9 = 66.6 = 66 + */ + uint256 dividendTotalSupply = segregatedDividend(time); + + tokenHolderDividend = (senderBalance * dividendTotalSupply) / tokenTotalSupply; + } + + /** + * @dev reverts with the error matching a non-OK {TIME_ERROR_CODE}. Exhaustive over the enum, and + * fails closed on an unhandled value — see the comment on the final branch. + * @param code the code returned by {_timeCode} + */ + function _revertOnInvalidTime(TIME_ERROR_CODE code) internal view virtual { + if (code == TIME_ERROR_CODE.OK) { + return; + } else if (code == TIME_ERROR_CODE.CLAIM_NOT_ACTIVATED) { + revert IncomeVault_ClaimNotActivated(); + } else if (code == TIME_ERROR_CODE.TOO_LATE_TO_WITHDRAW) { + revert IncomeVault_TooLateToWithdraw(block.timestamp); + } else { + // TOO_EARLY_TO_WITHDRAW — the only remaining value of an exhaustive enum, so an + // unconditional `else` rather than a fourth comparison. This also fails **closed**: a + // value added to TIME_ERROR_CODE without a matching arm reverts here instead of falling + // through and silently allowing the claim, which is what a trailing `else if` would do. + revert IncomeVault_TooEarlyToWithdraw(block.timestamp); + } + } + + /** + * @dev {validateTimeCode} with the caller supplying the storage pointer and the withdraw limit, + * so a batch can read the limit once instead of once per element. + * @param $ the ERC-7201 storage of the vault + * @param time the dividend time to check + * @param timeLimit the value of `timeLimitToWithdraw` + * @return code the reason the time is invalid, or `TIME_ERROR_CODE.OK` + */ + function _timeCode(IncomeVaultInternalStorage storage $, uint256 time, uint256 timeLimit) + internal + view + virtual + returns (TIME_ERROR_CODE code) + { + if (!$._segregatedClaim[time]) { + return TIME_ERROR_CODE.CLAIM_NOT_ACTIVATED; + } + if (block.timestamp > timeLimit + time) { + return TIME_ERROR_CODE.TOO_LATE_TO_WITHDRAW; + } + if (block.timestamp < time) { + return TIME_ERROR_CODE.TOO_EARLY_TO_WITHDRAW; + } + return TIME_ERROR_CODE.OK; + } + + /* ============ ERC-7201 ============ */ + /** + * @dev Returns the ERC-7201 namespaced storage of the IncomeVault + * @return $ the storage struct + */ + function _getIncomeVaultInternalStorage() internal pure returns (IncomeVaultInternalStorage storage $) { + assembly { + $.slot := IncomeVaultInternalStorageLocation + } + } +} diff --git a/src/modules/IncomeVaultOperatorModule.sol b/src/modules/IncomeVaultOperatorModule.sol new file mode 100644 index 0000000..4ba430e --- /dev/null +++ b/src/modules/IncomeVaultOperatorModule.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {IERC7540Operator} from "../interfaces/IERC7540Operator.sol"; +import {IncomeVaultInvariantStorage} from "../storage/IncomeVaultInvariantStorage.sol"; + +/** + * @title Claim delegation — one capability, one namespace + * @dev + * A holder may authorise another address to claim on their behalf. Payouts always go to the **holder**; + * the operator only pays the gas and chooses the moment. + * + * The signatures and the `OperatorSet` event are ERC-7540's, verbatim, so tooling written for that + * standard works unchanged. The vault is **not** an asynchronous vault and does not advertise + * {IERC7540Operator} through `supportsInterface`. + * + * This module owns the authorisation mapping in its own ERC-7201 namespace rather than in the + * distribution namespace, so the two capabilities can be reasoned about — and one day inherited — + * separately. {ERC7741Module} adds the signed variant on top and keeps a third namespace of its own for + * the consumed nonces. + */ +abstract contract IncomeVaultOperatorModule is ContextUpgradeable, IncomeVaultInvariantStorage, IERC7540Operator { + /* ============ ERC-7201 ============ */ + /** + * @dev Slot holding the ERC-7201 namespaced storage of this module, derived as + * keccak256(abi.encode(uint256(keccak256("IncomeVault.storage.Operator")) - 1)) & ~bytes32(uint256(0xff)) + * Recompute it with `SlotDerivation.erc7201Slot()` before trusting a change to it. + */ + bytes32 private constant OperatorStorageLocation = + 0x70af7571496f61583375b861df45fee91dcc3edadeaff09b686f7920599a5500; + + /// @custom:storage-location erc7201:IncomeVault.storage.Operator + struct OperatorStorage { + // Holders that authorised another address to claim on their behalf + mapping(address controller => mapping(address operator => bool)) _isOperator; + } + + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State functions ============ */ + /** + * @inheritdoc IERC7540Operator + * @dev Permissionless on purpose: a holder authorises their own operator, so there is no role to + * check. The authorisation only lets the operator trigger a claim; the payout still goes to the + * holder. {ERC7741Module-authorizeOperator} is the signed equivalent for a holder who cannot send + * the transaction themselves. + */ + function setOperator(address operator, bool approved) public virtual override(IERC7540Operator) returns (bool) { + _setOperator(_msgSender(), operator, approved); + return true; + } + + /* ============ View functions ============ */ + /** + * @inheritdoc IERC7540Operator + */ + function isOperator(address controller, address operator) + public + view + virtual + override(IERC7540Operator) + returns (bool) + { + OperatorStorage storage $ = _getOperatorStorage(); + return $._isOperator[controller][operator]; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State functions ============ */ + /** + * @dev Records an authorisation and emits the ERC-7540 event. The only writer of the mapping. + * @param controller the holder granting or revoking the authorisation + * @param operator the address being authorised + * @param approved true to authorise, false to revoke + */ + function _setOperator(address controller, address operator, bool approved) internal virtual { + OperatorStorage storage $ = _getOperatorStorage(); + $._isOperator[controller][operator] = approved; + emit OperatorSet(controller, operator, approved); + } + + /* ============ View functions ============ */ + /** + * @dev Reverts unless the caller is `holder` or an operator `holder` authorised + * @param holder the token holder being claimed for + */ + function _requireHolderOrOperator(address holder) internal view virtual { + address caller = _msgSender(); + if (caller != holder && !isOperator(holder, caller)) { + revert IncomeVault_UnauthorizedOperator(holder, caller); + } + } + + /* ============ ERC-7201 ============ */ + /** + * @dev Returns the ERC-7201 namespaced storage of this module + * @return $ the storage struct + */ + function _getOperatorStorage() internal pure returns (OperatorStorage storage $) { + assembly { + $.slot := OperatorStorageLocation + } + } +} diff --git a/src/modules/IncomeVaultSnapshotCore.sol b/src/modules/IncomeVaultSnapshotCore.sol new file mode 100644 index 0000000..507c5cd --- /dev/null +++ b/src/modules/IncomeVaultSnapshotCore.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/** + * @title What the dividend logic needs from a snapshot provider — and nothing more + * @dev + * The payout paths need three answers: one holder's balance at a `time`, many holders' balances at a + * `time`, and one holder's balances across many `time`s. This contract is those three questions, and + * **inherits nothing**. + * + * Declaring them as hooks rather than as calls on a stored address is what lets a host *be* its own + * snapshot source. A `CMTATStandaloneInternalSnapshot` already exposes `snapshotInfo` and both + * `snapshotInfoBatch` overloads, so it answers these from itself — no external call, no stored + * address, and no `snapshotEngine()` getter to collide with the one it already has. + * + * {IncomeVaultSnapshotModule} is the answer used by the standalone vault: an external + * {ISnapshotSource} held in storage. It is one implementation, not the only one. + */ +abstract contract IncomeVaultSnapshotCore { + /** + * @dev Balance of one holder and the total supply, at `time` + * @param time the dividend time + * @param tokenHolder the holder to look up + * @return tokenHolderBalance the holder's recorded balance + * @return totalSupply the recorded total supply + */ + function _snapshotInfo(uint256 time, address tokenHolder) + internal + view + virtual + returns (uint256 tokenHolderBalance, uint256 totalSupply); + + /** + * @dev Balances of many holders and the total supply, at one `time` + * @param time the dividend time + * @param addresses the holders to look up + * @return tokenHolderBalances one balance per address + * @return totalSupply the recorded total supply + */ + function _snapshotInfoBatch(uint256 time, address[] calldata addresses) + internal + view + virtual + returns (uint256[] memory tokenHolderBalances, uint256 totalSupply); + + /** + * @dev Balances of holders across many `time`s + * @param times the dividend times + * @param addresses the holders to look up + * @return tokenHolderBalances one row per time + * @return totalSupplies one total supply per time + */ + function _snapshotInfoBatch(uint256[] calldata times, address[] memory addresses) + internal + view + virtual + returns (uint256[][] memory tokenHolderBalances, uint256[] memory totalSupplies); +} diff --git a/src/modules/IncomeVaultSnapshotModule.sol b/src/modules/IncomeVaultSnapshotModule.sol new file mode 100644 index 0000000..431fa0d --- /dev/null +++ b/src/modules/IncomeVaultSnapshotModule.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== IncomeVault === */ +import {ISnapshotSource} from "../interfaces/ISnapshotSource.sol"; +import {IncomeVaultInternal} from "./IncomeVaultInternal.sol"; +import {IncomeVaultSnapshotCore} from "./IncomeVaultSnapshotCore.sol"; + +/** + * @title The standalone vault's answer to {IncomeVaultSnapshotCore} — an external source + * @dev + * Holds an {ISnapshotSource} and forwards the three queries to it. This is what the deployable vault + * uses; a host that is itself the snapshot source overrides the hooks instead and never inherits this + * module. + * + * The getter is deliberately **not** called `snapshotEngine()`. CMTAT's `ISnapshotEngineModule` + * declares `snapshotEngine() returns (ISnapshotEngine)`, and a same-name, same-parameter function with + * a *different return type* cannot be reconciled by any override — a contract inheriting both simply + * does not compile. Naming this after the capability rather than the generic concept removes the + * collision entirely. + */ +abstract contract IncomeVaultSnapshotModule is IncomeVaultSnapshotCore, IncomeVaultInternal { + /* ============ Modifier ============ */ + /// @dev Restricts the replacement of the snapshot source + modifier onlySnapshotSourceManager() { + _authorizeSnapshotSourceManagement(); + _; + } + + /* ============ ERC-7201 ============ */ + /** + * @dev Slot holding the ERC-7201 namespaced storage of this module, derived as + * keccak256(abi.encode(uint256(keccak256("IncomeVault.storage.SnapshotSource")) - 1)) & ~bytes32(uint256(0xff)) + * Recompute it with `SlotDerivation.erc7201Slot()` before trusting a change to it. + */ + bytes32 private constant SnapshotSourceStorageLocation = + 0x45a69a32b5b7efb4ae8ac48e2427653ef15920a29875121a072e6b49aaccac00; + + /* ==== ERC-7201 State Variables === */ + /// @custom:storage-location erc7201:IncomeVault.storage.SnapshotSource + struct SnapshotSourceStorage { + // Where the holder balances are read from + ISnapshotSource _source; + } + + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State functions ============ */ + /** + * @notice Replace the contract the vault reads the holder balances from + * @dev + * Only accepted while **no claim period is open** — `openClaimCount()` must be zero. Changing the + * source under an open period would silently re-price every unclaimed dividend of that period, + * because the amounts are computed from the source at claim time, not fixed at deposit. + * + * @custom:security The restriction narrows the hazard, it does not remove it: entitlements resolve + * against whichever source is configured *when the claim happens*, so re-opening a past `time` + * after a swap resolves it against the new source. Holders who already claimed are protected; + * holders who had not are not. + * + * @param source the new snapshot source, must implement {ISnapshotSource} and be non-zero + */ + function setDividendSnapshotSource(ISnapshotSource source) public virtual onlySnapshotSourceManager { + uint256 open = openClaimCount(); + if (open != 0) { + revert IncomeVault_ClaimPeriodOpen(open); + } + if (address(source) == address(dividendSnapshotSource())) { + revert IncomeVault_SameValue(); + } + _setDividendSnapshotSource(source); + } + + /* ============ View functions ============ */ + /** + * @notice The contract the vault reads the holder balances from + * @return The configured {ISnapshotSource} + */ + function dividendSnapshotSource() public view virtual returns (ISnapshotSource) { + SnapshotSourceStorage storage $ = _getSnapshotSourceStorage(); + return $._source; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State functions ============ */ + /** + * @notice Sets the snapshot source used to compute the dividends + * @dev reverts if `source` is the zero address + * @param source any contract implementing {ISnapshotSource} + */ + function _setDividendSnapshotSource(ISnapshotSource source) internal virtual { + if (address(source) == address(0)) { + revert IncomeVault_SnapshotSourceWithAddressZeroNotAllowed(); + } + SnapshotSourceStorage storage $ = _getSnapshotSourceStorage(); + $._source = source; + emit DividendSnapshotSourceSet(source); + } + + /* ============ View functions ============ */ + /// @inheritdoc IncomeVaultSnapshotCore + function _snapshotInfo(uint256 time, address tokenHolder) + internal + view + virtual + override + returns (uint256, uint256) + { + return dividendSnapshotSource().snapshotInfo(time, tokenHolder); + } + + /// @inheritdoc IncomeVaultSnapshotCore + function _snapshotInfoBatch(uint256 time, address[] calldata addresses) + internal + view + virtual + override + returns (uint256[] memory, uint256) + { + return dividendSnapshotSource().snapshotInfoBatch(time, addresses); + } + + /// @inheritdoc IncomeVaultSnapshotCore + function _snapshotInfoBatch(uint256[] calldata times, address[] memory addresses) + internal + view + virtual + override + returns (uint256[][] memory, uint256[] memory) + { + return dividendSnapshotSource().snapshotInfoBatch(times, addresses); + } + + /* ============ Access Control ============ */ + /** + * @dev Authorization hook invoked before {setDividendSnapshotSource}. + * Implemented by the deployment contract with the desired access-control policy. + */ + function _authorizeSnapshotSourceManagement() internal view virtual; + + /* ============ ERC-7201 ============ */ + /** + * @dev Returns the ERC-7201 namespaced storage of this module + * @return $ the storage struct + */ + function _getSnapshotSourceStorage() internal pure returns (SnapshotSourceStorage storage $) { + assembly { + $.slot := SnapshotSourceStorageLocation + } + } +} diff --git a/src/modules/IncomeVaultValidationCore.sol b/src/modules/IncomeVaultValidationCore.sol new file mode 100644 index 0000000..502c95b --- /dev/null +++ b/src/modules/IncomeVaultValidationCore.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/** + * @title What the dividend logic needs from a transfer-restriction policy — and nothing more + * @dev + * The payout paths ask one question before moving tokens: *may this payout proceed?* This contract is + * that question, and only that question. It **inherits nothing**, which is the point: a host embedding + * the dividend logic — a CMTAT that already has pause, freeze and a RuleEngine — answers from the + * modules it already owns instead of inheriting a second copy. + * + * {IncomeVaultValidationModule} is the answer used by the standalone vault, built on the CMTAT + * modules. It is one implementation, not the only one. + * + * This is the same authorization-hook pattern the project uses for access control, applied to the + * other dependency that was previously hard-wired. Before the split, {IncomeVaultOpen} and + * {IncomeVaultRestricted} each inherited the CMTAT `PauseModule` and `EnforcementModule` transitively, + * so **no CMTAT could ever embed them** — C3 linearization had no solution and the compiler rejected + * the combination with `Error (5005)`, which no override or ordering can repair. + */ +abstract contract IncomeVaultValidationCore { + /** + * @dev Reverts if the vault may not pay `value` to `to`. Implemented by the deployment — or by the + * host contract, when the dividend logic is embedded in one. + * @param from the address sending the payment, always the vault itself + * @param to the token holder receiving the dividends + * @param value the amount of payment token + */ + function _validateTransfer(address from, address to, uint256 value) internal view virtual; +} diff --git a/src/modules/IncomeVaultValidationModule.sol b/src/modules/IncomeVaultValidationModule.sol new file mode 100644 index 0000000..5d4acb6 --- /dev/null +++ b/src/modules/IncomeVaultValidationModule.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== CMTAT modules === */ +import {PauseModule} from "CMTAT/modules/wrapper/core/PauseModule.sol"; +import {EnforcementModule} from "CMTAT/modules/wrapper/core/EnforcementModule.sol"; +import {ValidationModuleRuleEngineInternal} from "CMTAT/modules/internal/ValidationModuleRuleEngineInternal.sol"; +/* ==== CMTAT engine === */ +import {IRuleEngine, IRuleEngineERC1404} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; +/* ==== IncomeVault === */ +import {IncomeVaultInvariantStorage} from "../storage/IncomeVaultInvariantStorage.sol"; +import {IncomeVaultValidationCore} from "./IncomeVaultValidationCore.sol"; + +/** + * @title The standalone vault's answer to {IncomeVaultValidationCore} + * @dev + * A dividend payout is treated as a transfer from the vault to the token holder and can be + * restricted the same way a CMTAT transfer is: + * + * - the vault can be put in the pause state ({PauseModule}), + * - an address can be frozen ({EnforcementModule}), + * - an optional {IRuleEngine} can apply arbitrary rules (allowlist, blocklist, sanction list, ...). + * + * Unlike the CMTAT, the vault is not a token bound to the RuleEngine: it only uses the *view* + * entry point {IRuleEngine-canTransfer}. `transferred()` is restricted to bound tokens by the + * RuleEngine and would revert here, and a payout is not a movement of the security token, so it + * must not update the stateful rules of the engine. + */ +abstract contract IncomeVaultValidationModule is + IncomeVaultValidationCore, + PauseModule, + EnforcementModule, + ValidationModuleRuleEngineInternal, + IncomeVaultInvariantStorage +{ + /* ============ State variables ============ */ + /** + * @dev Human-readable answers for {messageForTransferRestriction}. The strings are CMTAT's + * (`ValidationModuleERC1404`) verbatim, so an operator console written against a CMTAT reads a + * payout refusal exactly as it reads a transfer refusal. The codes are CMTAT's + * `REJECTED_CODE_BASE`, for the same reason. + */ + string internal constant TEXT_TRANSFER_OK = "NoRestriction"; + /// @dev Returned when no configured source claims the code + string internal constant TEXT_UNKNOWN_CODE = "UnknownCode"; + /// @dev The vault is paused + string internal constant TEXT_TRANSFER_REJECTED_PAUSED = "EnforcedPause"; + /// @dev The vault has been permanently deactivated + string internal constant TEXT_TRANSFER_REJECTED_DEACTIVATED = "ContractDeactivated"; + /// @dev The paying address is frozen + string internal constant TEXT_TRANSFER_REJECTED_FROM_FROZEN = "AddrFromIsFrozen"; + /// @dev The receiving token holder is frozen + string internal constant TEXT_TRANSFER_REJECTED_TO_FROZEN = "AddrToIsFrozen"; + + /* ============ Modifier ============ */ + /// @dev Restricts the management of the RuleEngine + modifier onlyRuleEngineManager() { + _authorizeRuleEngineManagement(); + _; + } + + /* ============ Initializer Function ============ */ + /** + * @notice Initializes the validation module + * @dev Writes the RuleEngine slot that CMTAT's {ValidationModuleRuleEngineInternal} owns, at its + * hardcoded ERC-7201 location. In the standalone vault that slot belongs to this contract alone. In + * a host that also inherits a CMTAT validation stack it is **shared**, so a non-zero `ruleEngine_` + * here would replace the *token's* compliance engine from the dividend initializer. Such a host must + * pass the zero address, which CMTAT's initializer treats as a no-op, and keep the engine the token + * already configured. Embedding the payout logic via {IncomeVaultValidationCore} instead avoids the + * question entirely, and is the supported route. Finding M-4. + * @param ruleEngine_ the RuleEngine applied to the payouts, or the zero address for none + */ + function __IncomeVaultValidation_init_unchained(IRuleEngine ruleEngine_) internal onlyInitializing { + ValidationModuleRuleEngineInternal.__ValidationRuleEngine_init_unchained(ruleEngine_); + } + + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State functions ============ */ + /** + * @notice Updates the RuleEngine applied to the dividend payouts. + * @param ruleEngine_ the new RuleEngine, or the zero address to disable the rule checks + */ + function setRuleEngine(IRuleEngine ruleEngine_) public virtual onlyRuleEngineManager { + if (address(ruleEngine_) == address(ruleEngine())) { + revert IncomeVault_SameValue(); + } + _setRuleEngine(ruleEngine_); + } + + /* ============ View functions ============ */ + /** + * @notice Returns true if the vault is allowed to pay `value` to `to`. + * @param from the address sending the payment, always the vault itself + * @param to the token holder receiving the dividends + * @param value the amount of payment token + * @return True if the pause, freeze and RuleEngine checks all allow the payout + */ + function canTransfer(address from, address to, uint256 value) public view virtual returns (bool) { + if (PauseModule.paused()) { + return false; + } + if (EnforcementModule.isFrozen(from) || EnforcementModule.isFrozen(to)) { + return false; + } + IRuleEngine ruleEngine_ = ruleEngine(); + if (address(ruleEngine_) != address(0)) { + return ruleEngine_.canTransfer(from, to, value); + } + return true; + } + + /** + * @notice ERC-1404 restriction code for a payout from the vault, or `0` when it would be accepted. + * @dev Answers for the **whole** payout decision, in the same order {canTransfer} evaluates it: + * deactivation, pause, either party frozen, then the RuleEngine. The codes are CMTAT's + * `REJECTED_CODE_BASE`, so a caller written against a CMTAT reads them unchanged. + * + * This returns `0` exactly when {canTransfer} returns true, and the two must not be allowed to + * drift apart: consulting only the RuleEngine here would report a paused vault or a frozen holder as + * unrestricted, and the claim would then revert. + * @param from the address sending the payment, always the vault itself + * @param to the token holder receiving the dividends + * @param value the amount of payment token + * @return The ERC-1404 restriction code, `0` when the rules allow the payout + */ + function detectTransferRestriction(address from, address to, uint256 value) public view virtual returns (uint8) { + // Deactivation implies pause, so the more specific code is tested first. + if (PauseModule.deactivated()) { + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_DEACTIVATED); + } + if (PauseModule.paused()) { + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_PAUSED); + } + if (EnforcementModule.isFrozen(from)) { + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_FROM_FROZEN); + } + if (EnforcementModule.isFrozen(to)) { + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_TO_FROZEN); + } + IRuleEngine ruleEngine_ = ruleEngine(); + if (address(ruleEngine_) == address(0)) { + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + return IRuleEngineERC1404(address(ruleEngine_)).detectTransferRestriction(from, to, value); + } + + /** + * @notice Human readable message matching a code returned by {detectTransferRestriction}. + * @param restrictionCode the ERC-1404 restriction code to translate + * @return The message associated with `restrictionCode` + */ + function messageForTransferRestriction(uint8 restrictionCode) public view virtual returns (string memory) { + if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK)) { + return TEXT_TRANSFER_OK; + } + if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_DEACTIVATED)) { + return TEXT_TRANSFER_REJECTED_DEACTIVATED; + } + if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_PAUSED)) { + return TEXT_TRANSFER_REJECTED_PAUSED; + } + if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_FROM_FROZEN)) { + return TEXT_TRANSFER_REJECTED_FROM_FROZEN; + } + if (restrictionCode == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_TO_FROZEN)) { + return TEXT_TRANSFER_REJECTED_TO_FROZEN; + } + IRuleEngine ruleEngine_ = ruleEngine(); + if (address(ruleEngine_) == address(0)) { + // The vault answers for its own codes above; anything else could only have come from a + // RuleEngine, and there is none. Saying "no restriction" here would repeat the defect + // this function's siblings were fixed for. + return TEXT_UNKNOWN_CODE; + } + return IRuleEngineERC1404(address(ruleEngine_)).messageForTransferRestriction(restrictionCode); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ Access Control ============ */ + /** + * @dev Authorization hook invoked before {setRuleEngine}. + * Implemented by the deployment contract with the desired access-control policy. + * + * @dev CMTAT's {ValidationModuleRuleEngine} declares a hook with this same name and parameters. + * That is **not** a collision to be renamed away: both this module and CMTAT's wrapper sit on the + * same {ValidationModuleRuleEngineInternal}, whose ERC-7201 slot is a hardcoded constant, so a + * contract inheriting both has exactly **one** RuleEngine. One capability, therefore one hook — and + * a single override answering both declarations is the correct resolution, not an accident. Giving + * the two hooks different names would create two doors to one slot, each able to carry a different + * policy, and the weaker one would win. See finding M-4. + */ + function _authorizeRuleEngineManagement() internal view virtual; + + /* ============ View functions ============ */ + /** + * @inheritdoc IncomeVaultValidationCore + * @dev The standalone vault's answer: its own pause state, the frozen status of both parties, and + * the RuleEngine if one is configured. + */ + function _validateTransfer(address from, address to, uint256 value) + internal + view + virtual + override(IncomeVaultValidationCore) + { + if (!canTransfer(from, to, value)) { + revert IncomeVault_InvalidTransfer(from, to, value); + } + } +} diff --git a/src/modules/Ownable2StepERC165Module.sol b/src/modules/Ownable2StepERC165Module.sol new file mode 100644 index 0000000..32d1dce --- /dev/null +++ b/src/modules/Ownable2StepERC165Module.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== OpenZeppelin === */ +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; + +/** + * @title ERC-165 advertisement of the ERC-173 / Ownable2Step access control + * @dev + * Kept in its own module so it is declared once instead of being repeated in every Ownable variant. + * The two identifiers are hardcoded because `type(I).interfaceId` XORs only the selectors declared + * directly on the interface, and OpenZeppelin ships no `IERC173` interface to compute them from. + */ +abstract contract Ownable2StepERC165Module is ERC165Upgradeable { + /** + * @notice ERC-165 interface ID of ERC-173 (contract ownership standard) + * @dev bytes4(keccak256("owner()")) ^ bytes4(keccak256("transferOwnership(address)")) + */ + bytes4 public constant IERC173_INTERFACE_ID = 0x7f5828d0; + /** + * @notice ERC-165 interface ID of the Ownable2Step-specific functions + * @dev bytes4(keccak256("acceptOwnership()")) ^ bytes4(keccak256("pendingOwner()")) + */ + bytes4 public constant IOWNABLE2STEP_INTERFACE_ID = 0x9ab669ef; + + /** + * @notice ERC-165 interface detection + * @param interfaceId The interface identifier to check + * @return True if the interface is supported, false otherwise + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165Upgradeable) returns (bool) { + return interfaceId == IERC173_INTERFACE_ID || interfaceId == IOWNABLE2STEP_INTERFACE_ID + || ERC165Upgradeable.supportsInterface(interfaceId); + } +} diff --git a/src/modules/VersionModule.sol b/src/modules/VersionModule.sol new file mode 100644 index 0000000..f2ac1b6 --- /dev/null +++ b/src/modules/VersionModule.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== CMTAT === */ +import {IERC3643Version} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; + +/** + * @title VersionModule + * @notice Exposes the IncomeVault release version through the ERC-3643 version interface. + * @dev + * Same shape as the CMTAT, RuleEngine and SnapshotEngine version modules: a single compile-time + * constant read through {IERC3643Version-version}. Bump `VERSION` together with the `CHANGELOG.md` + * entry of the release. + */ +abstract contract VersionModule is IERC3643Version { + /* ============ State Variables ============ */ + /** + * @dev + * Get the current version of the smart contract + */ + string private constant VERSION = "2.0.0"; + + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc IERC3643Version + */ + function version() public view virtual override(IERC3643Version) returns (string memory version_) { + return VERSION; + } +} diff --git a/src/public/IncomeVaultOpen.sol b/src/public/IncomeVaultOpen.sol index 833a21b..745a586 100644 --- a/src/public/IncomeVaultOpen.sol +++ b/src/public/IncomeVaultOpen.sol @@ -1,114 +1,157 @@ // SPDX-License-Identifier: MPL-2.0 -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; +/* ==== OpenZeppelin === */ +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +/* ==== IncomeVault === */ +import {IncomeVaultValidationCore} from "../modules/IncomeVaultValidationCore.sol"; +import {IncomeVaultSnapshotCore} from "../modules/IncomeVaultSnapshotCore.sol"; +import {ERC7741Module} from "../modules/ERC7741Module.sol"; -import "lib/CMTAT/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol"; -import "../libraries/IncomeVaultInternal.sol"; -import "CMTAT/modules/wrapper/controllers/ValidationModule.sol"; /** -* @title public function -*/ -abstract contract IncomeVaultOpen is ReentrancyGuardUpgradeable, ValidationModule , IncomeVaultInternal { - enum TIME_ERROR_CODE {OK, CLAIM_NOT_ACTIVATED, TOO_LATE_TO_WITHDRAW, TOO_EARLY_TO_WITHDRAW} - + * @title Permissionless functions + */ +abstract contract IncomeVaultOpen is + IncomeVaultValidationCore, + IncomeVaultSnapshotCore, + ERC7741Module, + ReentrancyGuardTransient +{ + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State functions ============ */ /** - * @notice validate if a time is valid, return 0 if valid - */ - function validateTimeCode(uint256 time) public view returns(TIME_ERROR_CODE code){ - if(!segregatedClaim[time]){ - return TIME_ERROR_CODE.CLAIM_NOT_ACTIVATED; - } - if(block.timestamp > timeLimitToWithdraw + time){ - return TIME_ERROR_CODE.TOO_LATE_TO_WITHDRAW; - } - if(block.timestamp < time){ - return TIME_ERROR_CODE.TOO_EARLY_TO_WITHDRAW; - } - return TIME_ERROR_CODE.OK; + * @notice claim your payment + * @param time provide the date where you want to receive your payment + */ + function claimDividend(uint256 time) public virtual nonReentrant { + _claimDividend(_msgSender(), time); } - + /** - * @notice validate if a time is valid, revert if invalid + * @notice Claim on behalf of a token holder + * @dev + * Callable by the holder, or by an address the holder authorised through {setOperator}. The + * dividends always go to **the holder** — an operator pays the gas and chooses the moment, it can + * never redirect the payment. Every other rule is unchanged: the claim window, the + * already-claimed check and the transfer restrictions all apply exactly as for {claimDividend}. + * @param holder the token holder to claim for + * @param time provide the date of the payment */ - function validateTime(uint256 time) public view{ - TIME_ERROR_CODE code = validateTimeCode(time); - if(code == TIME_ERROR_CODE.OK){ - return; - }else if(code == TIME_ERROR_CODE.CLAIM_NOT_ACTIVATED){ - revert IncomeVault_ClaimNotActivated(); - } - else if(code == TIME_ERROR_CODE.TOO_LATE_TO_WITHDRAW){ - revert IncomeVault_TooLateToWithdraw(block.timestamp); - }else if (code == TIME_ERROR_CODE.TOO_EARLY_TO_WITHDRAW){ - revert IncomeVault_TooEarlyToWithdraw(block.timestamp); - } + function claimDividendFor(address holder, uint256 time) public virtual nonReentrant { + _requireHolderOrOperator(holder); + _claimDividend(holder, time); + } + + /** + * @notice Batch version of {claimDividendFor} + * @param holder the token holder to claim for + * @param times provide the dates of the payments + */ + function claimDividendBatchFor(address holder, uint256[] calldata times) public virtual nonReentrant { + _requireHolderOrOperator(holder); + _claimDividendBatch(holder, times); + } + + /** + * @notice batch version of {claimDividend} + * @param times provide the dates where you want to receive your payment + * @dev Don't check if the dividends have been already claimed before external call to the snapshot source. + */ + function claimDividendBatch(uint256[] calldata times) public virtual nonReentrant { + _claimDividendBatch(_msgSender(), times); } + /* ============ View functions ============ */ /** - * @notice batch version of {validateTime} - */ - function validateTimeBatch(uint256[] memory times) public view{ - for(uint256 i = 0; i < times.length; ++i){ - validateTime(times[i]); + * @notice validate if a time is valid, return 0 if valid + * @param time the dividend time to check + * @return code the reason the time is invalid, or `TIME_ERROR_CODE.OK` + */ + function validateTimeCode(uint256 time) public view virtual returns (TIME_ERROR_CODE code) { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + return _timeCode($, time, $._timeLimitToWithdraw); + } + + /** + * @notice validate if a time is valid, revert if invalid + * @param time the dividend time to check + */ + function validateTime(uint256 time) public view virtual { + _revertOnInvalidTime(validateTimeCode(time)); + } + + /** + * @notice batch version of {validateTime} + * @param times the dividend times to check + */ + function validateTimeBatch(uint256[] calldata times) public view virtual { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + // `_timeLimitToWithdraw` is the same slot for every element: read it once + uint256 timeLimit = $._timeLimitToWithdraw; + for (uint256 i = 0; i < times.length; ++i) { + _revertOnInvalidTime(_timeCode($, times[i], timeLimit)); } } - + + /*////////////////////////////////////////////////////////////// + INTERNAL/PRIVATE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State functions ============ */ /** - * @notice claim your payment - * @param time provide the date where you want to receive your payment - */ - function claimDividend(uint256 time) public nonReentrant() { + * @dev {claimDividend} for an explicit holder + * @param sender the token holder being paid + * @param time the dividend time + */ + function _claimDividend(address sender, uint256 time) internal virtual { validateTime(time); - address sender = _msgSender(); + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); // At the beginning since no external call to do - if (claimedDividend[sender][time]){ + if ($._claimedDividend[sender][time]) { revert IncomeVault_DividendAlreadyClaimed(); } - // External call to the CMTAT to retrieve the total supply and the sender balance - (uint256 senderBalance, uint256 TokenTotalSupply) = CMTAT_TOKEN.snapshotInfo(time, sender); - if (senderBalance == 0){ + // External call to the snapshot source to retrieve the total supply and the sender balance + (uint256 senderBalance, uint256 TokenTotalSupply) = _snapshotInfo(time, sender); + if (senderBalance == 0) { revert IncomeVault_TokenBalanceIsZero(); } uint256 senderDividend = _computeDividend(time, senderBalance, TokenTotalSupply); - if (senderDividend == 0){ + if (senderDividend == 0) { revert IncomeVault_NoDividendToClaim(); } // Transfer restriction - if (!ValidationModule._operateOnTransfer(address(this), sender, senderDividend)) { - revert Errors.CMTAT_InvalidTransfer(address(this), sender, senderDividend); - } + _validateTransfer(address(this), sender, senderDividend); _transferDividend(time, sender, senderDividend); } /** - * @notice batch version of {claimDividend} - * @param times provide the dates where you want to receive your payment - * @dev Don't check if the dividends have been already claimed before external call to CMTAT. - */ - function claimDividendBatch(uint256[] memory times) public nonReentrant() { + * @dev {claimDividendBatch} for an explicit holder + * @param sender the token holder being paid + * @param times the dividend times + */ + function _claimDividendBatch(address sender, uint256[] calldata times) internal virtual { // Check if the claim is activated for each times validateTimeBatch(times); - address sender = _msgSender(); address[] memory senders = new address[](1); senders[0] = sender; - // External call to the CMTAT to retrieve the total supply and the sender balance - (uint256[][] memory senderBalances, uint256[] memory TokenTotalSupplys) = CMTAT_TOKEN.snapshotInfoBatch(times, senders); - for(uint256 i = 0; i < times.length; ++i){ - if (!claimedDividend[sender][times[i]] && (senderBalances[i][0] > 0 )){ + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + // External call to the snapshot source to retrieve the total supply and the sender balance + (uint256[][] memory senderBalances, uint256[] memory TokenTotalSupplys) = _snapshotInfoBatch(times, senders); + for (uint256 i = 0; i < times.length; ++i) { + if (!$._claimedDividend[sender][times[i]] && (senderBalances[i][0] > 0)) { uint256 senderDividend = _computeDividend(times[i], senderBalances[i][0], TokenTotalSupplys[i]); // Transfer restriction - // External Call - if (!ValidationModule._operateOnTransfer(address(this), sender, senderDividend)) { - revert Errors.CMTAT_InvalidTransfer(address(this), sender, senderDividend); - } + _validateTransfer(address(this), sender, senderDividend); // internal call performing an ERC-20 external call _transferDividend(times[i], sender, senderDividend); } } - } - uint256[50] private __gap; -} \ No newline at end of file + } + + /* ============ View functions ============ */ +} diff --git a/src/public/IncomeVaultRestricted.sol b/src/public/IncomeVaultRestricted.sol index acd2edb..1a54bc0 100644 --- a/src/public/IncomeVaultRestricted.sol +++ b/src/public/IncomeVaultRestricted.sol @@ -1,96 +1,185 @@ // SPDX-License-Identifier: MPL-2.0 -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; -import "OZ/token/ERC20/utils/SafeERC20.sol"; -import "CMTAT/modules/wrapper/controllers/ValidationModule.sol"; -import "../libraries/IncomeVaultInternal.sol"; +/* ==== OpenZeppelin === */ +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +/* ==== IncomeVault === */ +import {IncomeVaultValidationCore} from "../modules/IncomeVaultValidationCore.sol"; +import {IncomeVaultSnapshotCore} from "../modules/IncomeVaultSnapshotCore.sol"; +import {IncomeVaultInternal} from "../modules/IncomeVaultInternal.sol"; /** -* @title restricted functions -*/ -abstract contract IncomeVaultRestricted is ValidationModule, IncomeVaultInternal { - /** - * @dev calls the different initialize functions from the different modules - */ - function __IncomeVaultRestricted_init_unchained( - uint256 timeLimitToWithdraw_ - ) internal onlyInitializing { - timeLimitToWithdraw = timeLimitToWithdraw_; - } + * @title Restricted functions + */ +abstract contract IncomeVaultRestricted is + IncomeVaultValidationCore, + IncomeVaultSnapshotCore, + ContextUpgradeable, + IncomeVaultInternal, + ReentrancyGuardTransient +{ // Security using SafeERC20 for IERC20; + /* ============ Modifier ============ */ + /// @dev Restricts the deposit of dividends + modifier onlyDepositManager() { + _authorizeDeposit(); + _; + } + + /// @dev Restricts the withdrawal of the deposited funds + modifier onlyWithdrawManager() { + _authorizeWithdraw(); + _; + } + + /// @dev Restricts the issuer-driven distribution of the dividends + modifier onlyDistributeManager() { + _authorizeDistribute(); + _; + } + + /// @dev Restricts the configuration of the claim window + modifier onlyVaultOperator() { + _authorizeOperator(); + _; + } + + /* ============ Initializer Function ============ */ + /** + * @dev calls the different initialize functions from the different modules + * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted + */ + function __IncomeVaultRestricted_init_unchained(uint256 timeLimitToWithdraw_) internal onlyInitializing { + _setTimeLimitToWithdraw(timeLimitToWithdraw_); + } + + /*////////////////////////////////////////////////////////////// + PUBLIC/EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /* ============ State restricted functions ============ */ /** - * @notice deposit an amount to pay the dividends. - * @param time provide the date where you want to perform a deposit - * @param amount the amount to deposit - */ - function deposit(uint256 time, uint256 amount) public onlyRole(INCOME_VAULT_DEPOSIT_ROLE) { + * @notice deposit an amount to pay the dividends. + * @param time provide the date where you want to perform a deposit + * @param amount the amount to deposit + */ + function deposit(uint256 time, uint256 amount) public virtual onlyDepositManager { address sender = _msgSender(); - if(amount == 0) { - revert IncomeVault_NoAmountSend(); - } - segregatedDividend[time] += amount; - emit newDeposit(time, sender, amount); + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + _deposit($, sender, time, amount); // Will revert in case of failure - ERC20TokenPayment.safeTransferFrom(sender, address(this), amount); + $._ERC20TokenPayment.safeTransferFrom(sender, address(this), amount); } /** - * @notice withdraw a certain amount at a specified time. - * @param time provide the date where you want to perform a deposit - * @param amount the amount to withdraw - * @param withdrawAddress address to receive `amount`of tokens - */ - function withdraw(uint256 time, uint256 amount, address withdrawAddress) public onlyRole(INCOME_VAULT_WITHDRAW_ROLE) { - bool result = ERC20TokenPayment.approve(address(this), amount); - if(!result){ - revert IncomeVault_FailApproval(); + * @notice Deposit for several dividend times in one transaction + * @dev + * Equivalent to calling {deposit} once per entry — same accounting, same `newDeposit` event per + * entry — but the payment token is pulled **once** for the total instead of once per time. That is + * the reason the function exists; the common case is an issuer opening a year of coupon periods. + * + * Repeating a `time` is allowed and accumulates, exactly as separate calls would. + * + * @param times the dividend times to deposit for + * @param amounts the amount to deposit for each time, must be the same length and each non-zero + */ + function depositBatch(uint256[] calldata times, uint256[] calldata amounts) public virtual onlyDepositManager { + if (times.length != amounts.length) { + revert IncomeVault_InvalidLengths(times.length, amounts.length); + } + if (times.length == 0) { + revert IncomeVault_NoAmountSend(); + } + address sender = _msgSender(); + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + uint256 total; + for (uint256 i = 0; i < times.length; ++i) { + _deposit($, sender, times[i], amounts[i]); + total += amounts[i]; } - if(segregatedDividend[time] < amount) { + // One transfer for the whole batch. Will revert in case of failure. + $._ERC20TokenPayment.safeTransferFrom(sender, address(this), total); + } + + /** + * @notice withdraw a certain amount at a specified time. + * @dev + * Bounded by {unclaimedDividend}, so a sweep can never reach funds deposited for another dividend + * time. Intended for after the claim window closes, when what remains is rounding dust and + * unclaimed shares. + * + * @custom:security Withdrawing **before** the window closes is still destructive to this period: + * the amount taken is money the remaining holders are entitled to, and it also lowers + * `segregatedDividend`, which re-prices every claim that has not happened yet. The bound stops the + * damage spreading to other periods; it does not make an early sweep safe. + * + * @param time provide the date where you want to perform a deposit + * @param amount the amount to withdraw + * @param withdrawAddress address to receive `amount`of tokens + */ + function withdraw(uint256 time, uint256 amount, address withdrawAddress) public virtual onlyWithdrawManager { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + // Bound by what this period STILL holds, not by what was deposited into it. `_segregatedDividend` + // is the pro-rata denominator and is never reduced by a payout, so checking against it alone + // would let a fully-claimed period be swept again — taking another period's money. + // {unclaimedDividend} saturates at zero, so an over-drawn period simply allows nothing. + if (unclaimedDividend(time) < amount) { revert IncomeVault_NotEnoughAmount(); } - segregatedDividend[time] -= amount; + $._segregatedDividend[time] -= amount; + emit Withdraw(time, withdrawAddress, amount); // Will revert in case of failure - ERC20TokenPayment.safeTransferFrom(address(this), withdrawAddress, amount); + $._ERC20TokenPayment.safeTransfer(withdrawAddress, amount); } /** - * @notice withdraw all tokens from ERC20TokenPayment contracts deposited - * @param amount the amount to withdraw - * @param withdrawAddress address to receive `amount`of tokens - */ - function withdrawAll(uint256 amount, address withdrawAddress) public onlyRole(INCOME_VAULT_WITHDRAW_ROLE) { - bool result = ERC20TokenPayment.approve(address(this), amount); - if(!result){ - revert IncomeVault_FailApproval(); - } + * @notice withdraw all tokens from ERC20TokenPayment contracts deposited + * @param amount the amount to withdraw + * @param withdrawAddress address to receive `amount`of tokens + */ + function withdrawAll(uint256 amount, address withdrawAddress) public virtual onlyWithdrawManager { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + emit WithdrawAll(withdrawAddress, amount); // Will revert in case of failure - ERC20TokenPayment.safeTransferFrom(address(this), withdrawAddress, amount); + $._ERC20TokenPayment.safeTransfer(withdrawAddress, amount); } /** - * @notice distribute the dividends - * @param addresses compute and transfer dividend for these holders - * @param time dividend time - * @dev The dividends are distributed only if they have not yet been claimed by the token holder - */ - function distributeDividend(address[] calldata addresses, uint256 time) public onlyRole(INCOME_VAULT_DISTRIBUTE_ROLE) { - // Check if the claim is activated - if(!segregatedClaim[time]){ - revert IncomeVault_ClaimNotActivated(); - } - // Get info from the token - (uint256[] memory tokenHolderBalance, uint256 totalSupply) = CMTAT_TOKEN.snapshotInfoBatch(time, addresses); + * @notice distribute the dividends + * @param addresses compute and transfer dividend for these holders + * @param time dividend time + * @dev The dividends are distributed only if they have not yet been claimed by the token holder. + * Subject to the same claim window **and** the same transfer restrictions as + * {IncomeVaultOpen-claimDividend}: a holder the pause, freeze or RuleEngine refuses cannot be paid + * by the issuer either, and one blocked holder reverts the whole distribution. + */ + function distributeDividend(address[] calldata addresses, uint256 time) public virtual onlyDistributeManager { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + // Same window as a holder-driven claim: the claims must be open, `time` must have passed so the + // snapshot is recorded, and the withdraw limit must not have expired. Distributing before `time` + // would read the *live* balances, because {ISnapshotSource} falls back to them when no snapshot + // exists yet, and would consume the holder's claim for that period at the wrong amount. + _revertOnInvalidTime(_timeCode($, time, $._timeLimitToWithdraw)); + // Get info from the snapshot source + (uint256[] memory tokenHolderBalance, uint256 totalSupply) = _snapshotInfoBatch(time, addresses); // Compute dividend for all token holders uint256[] memory tokenHolderDividend = _computeDividendBatch(time, addresses, tokenHolderBalance, totalSupply); // transfer the dividends for all token holders - for(uint256 i = 0; i < addresses.length; ++i){ - // The dividends are distributed only if they have not yet been claimed by the token holder - if (!claimedDividend[addresses[i]][time]){ + for (uint256 i = 0; i < addresses.length; ++i) { + // The dividends are distributed only if they have not yet been claimed by the token holder + if (!$._claimedDividend[addresses[i]][time]) { // transfer dividends - if(tokenHolderDividend[i] > 0){ + if (tokenHolderDividend[i] > 0) { + // Same transfer restriction as a holder-driven claim: pause, freeze and RuleEngine. + // Reverts the whole distribution rather than skipping the holder, so a blocked + // address cannot be silently dropped from a payout the operator believes succeeded. + // The error carries the address, so it can be removed from the list and retried. + _validateTransfer(address(this), addresses[i], tokenHolderDividend[i]); _transferDividend(time, addresses[i], tokenHolderDividend[i]); } } @@ -98,21 +187,132 @@ abstract contract IncomeVaultRestricted is ValidationModule, IncomeVaultInternal } /** - * @notice set the status to open or close the claims for a given time - * @param time target time - * @param status boolean (true or false) - * - */ - function setStatusClaim(uint256 time, bool status) public onlyRole(INCOME_VAULT_OPERATOR_ROLE){ - segregatedClaim[time] = status; + * @notice Distribute the dividends, skipping any holder whose payout is refused + * @dev + * Same computation as {distributeDividend}, but a holder the ValidationModule or the payment token + * refuses is **skipped** instead of reverting the whole call. Use it when one non-compliant address + * must not block a large payout run; use {distributeDividend} when the distribution should be + * all-or-nothing. + * + * Each payout is attempted through an external self-call so it can be wrapped in `try`/`catch`, + * which gives **per-holder atomicity**: a holder is either fully paid — marked claimed *and* + * transferred — or left completely untouched and still able to claim later. A partial state where + * a holder is marked as claimed without receiving the tokens is not reachable. + * + * Every skip emits {DividendDistributionSkipped} carrying the raw revert data, so the cause can be + * decoded off-chain, and the skipped holders are returned for the caller to act on directly. + * + * @custom:security `catch` cannot distinguish a refused payout from an out-of-gas failure. The two + * contracts that can consume gas here — the payment token and the RuleEngine — are both set by the + * admin and trusted; a malicious RuleEngine could nonetheless make holders appear skipped. That is + * within the existing trust assumption for the RuleEngine, not a new one. + * + * @param addresses compute and transfer dividend for these holders + * @param time dividend time + * @return paidCount how many holders were paid + * @return skipped the holders that were not paid, trimmed to `paidCount` subtracted from the input + */ + function distributeDividendBestEffort(address[] calldata addresses, uint256 time) + public + virtual + nonReentrant + onlyDistributeManager + returns (uint256 paidCount, address[] memory skipped) + { + IncomeVaultInternalStorage storage $ = _getIncomeVaultInternalStorage(); + _revertOnInvalidTime(_timeCode($, time, $._timeLimitToWithdraw)); + + (uint256[] memory tokenHolderBalance, uint256 totalSupply) = _snapshotInfoBatch(time, addresses); + uint256[] memory tokenHolderDividend = _computeDividendBatch(time, addresses, tokenHolderBalance, totalSupply); + + address[] memory skippedBuffer = new address[](addresses.length); + uint256 skippedCount; + + for (uint256 i = 0; i < addresses.length; ++i) { + if ($._claimedDividend[addresses[i]][time] || tokenHolderDividend[i] == 0) { + continue; + } + // External self-call: `try` needs one, and it is what bounds the revert to this holder. + try this.transferDividendSelf(time, addresses[i], tokenHolderDividend[i]) { + ++paidCount; + } catch (bytes memory reason) { + skippedBuffer[skippedCount] = addresses[i]; + ++skippedCount; + emit DividendDistributionSkipped(time, addresses[i], reason); + } + } + + skipped = new address[](skippedCount); + for (uint256 i = 0; i < skippedCount; ++i) { + skipped[i] = skippedBuffer[i]; + } + } + + /** + * @notice Validate and pay one dividend — callable **only by the vault itself** + * @dev + * This exists solely so {distributeDividendBestEffort} can wrap a payout in `try`/`catch`, which + * requires an external call. It carries no access control of its own beyond the self-call check, + * so that check is what stands between it and an unauthorized payout: reverts + * {IncomeVault_OnlySelfCall} for every caller other than `address(this)`. + * + * `msg.sender` is used deliberately rather than `_msgSender()`. The check must identify the real + * caller; an ERC-2771 forwarder must never be able to present itself as the vault. + * + * @param time dividend time + * @param tokenHolder the holder to pay + * @param tokenHolderDividend the amount to pay + */ + function transferDividendSelf(uint256 time, address tokenHolder, uint256 tokenHolderDividend) public virtual { + if (msg.sender != address(this)) { + revert IncomeVault_OnlySelfCall(); + } + _validateTransfer(address(this), tokenHolder, tokenHolderDividend); + _transferDividend(time, tokenHolder, tokenHolderDividend); + } + + /** + * @notice set the status to open or close the claims for a given time + * @param time target time + * @param status boolean (true or false) + * + */ + function setStatusClaim(uint256 time, bool status) public virtual onlyVaultOperator { + _setStatusClaim(time, status); } /** - * @notice configure the time limit to withdraw - */ - function setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) public onlyRole(INCOME_VAULT_OPERATOR_ROLE){ - timeLimitToWithdraw = timeLimitToWithdraw_; + * @notice configure the time limit to withdraw + * @dev reverts if `timeLimitToWithdraw_` is zero: that would leave a one-second claim window + * @param timeLimitToWithdraw_ delay, after the dividend time, during which a claim is accepted, + * must be greater than zero + */ + function setTimeLimitToWithdraw(uint256 timeLimitToWithdraw_) public virtual onlyVaultOperator { + _setTimeLimitToWithdraw(timeLimitToWithdraw_); } - - uint256[50] private __gap; + + /* ============ Access Control ============ */ + /** + * @dev Authorization hook invoked before a deposit. + * Implemented by the deployment contract with the desired access-control policy. + */ + function _authorizeDeposit() internal view virtual; + + /** + * @dev Authorization hook invoked before {withdraw} and {withdrawAll}. + * Implemented by the deployment contract with the desired access-control policy. + */ + function _authorizeWithdraw() internal view virtual; + + /** + * @dev Authorization hook invoked before {distributeDividend}. + * Implemented by the deployment contract with the desired access-control policy. + */ + function _authorizeDistribute() internal view virtual; + + /** + * @dev Authorization hook invoked before {setStatusClaim} and {setTimeLimitToWithdraw}. + * Implemented by the deployment contract with the desired access-control policy. + */ + function _authorizeOperator() internal view virtual; } diff --git a/src/storage/IncomeVaultInvariantStorage.sol b/src/storage/IncomeVaultInvariantStorage.sol new file mode 100644 index 0000000..8f74199 --- /dev/null +++ b/src/storage/IncomeVaultInvariantStorage.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/* ==== Snapshot === */ +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ISnapshotSource} from "../interfaces/ISnapshotSource.sol"; + +/** + * @title Roles, errors and events shared by the IncomeVault modules + */ +abstract contract IncomeVaultInvariantStorage { + /* ============ Events ============ */ + /** + * @notice Emitted when an authorized address deposits dividends for a given time + * @param time the dividend time the deposit is attached to + * @param sender the address performing the deposit + * @param dividend the amount of payment token deposited + */ + event newDeposit(uint256 indexed time, address indexed sender, uint256 dividend); + /** + * @notice Emitted when the dividends of a token holder are claimed or distributed + * @param time the dividend time + * @param sender the token holder receiving the dividends + * @param dividend the amount of payment token transferred + */ + event DividendClaimed(uint256 indexed time, address indexed sender, uint256 dividend); + /** + * @notice Emitted when the ERC-20 used to pay the dividends is set + * @param newERC20TokenPayment the payment token + */ + event ERC20TokenPaymentSet(IERC20 indexed newERC20TokenPayment); + /** + * @notice Emitted when the claims are opened or closed for a dividend time + * @param time the dividend time + * @param status true when the token holders can claim + */ + event ClaimStatusSet(uint256 indexed time, bool status); + /** + * @notice Emitted when the delay during which a claim is accepted is set + * @param timeLimitToWithdraw the delay in seconds + */ + event TimeLimitToWithdrawSet(uint256 timeLimitToWithdraw); + /** + * @notice Emitted when an authorized address withdraws the funds deposited for a dividend time + * @param time the dividend time the funds were deposited for + * @param withdrawAddress the address receiving the funds + * @param amount the amount of payment token withdrawn + */ + event Withdraw(uint256 indexed time, address indexed withdrawAddress, uint256 amount); + + /** + * @notice Emitted when a best-effort distribution skips a token holder + * @dev Reported by {IncomeVaultRestricted-distributeDividendBestEffort}. The holder is left + * completely untouched — not marked as claimed — and can still claim later, or be included in a + * subsequent distribution. + * @param time the dividend time + * @param tokenHolder the holder who was not paid + * @param reason the raw revert data of the failed payout, so the cause can be decoded off-chain + */ + event DividendDistributionSkipped(uint256 indexed time, address indexed tokenHolder, bytes reason); + + /** + * @notice Emitted when an authorized address withdraws funds without a dividend time + * @dev the per-time accounting in `segregatedDividend` is left untouched, see {withdrawAll} + * @param withdrawAddress the address receiving the funds + * @param amount the amount of payment token withdrawn + */ + event WithdrawAll(address indexed withdrawAddress, uint256 amount); + /** + * @notice Emitted when the snapshot source used to compute the dividends is set. + * @param newSource The contract queried for historical balances and total supply. + */ + event DividendSnapshotSourceSet(ISnapshotSource indexed newSource); + + /* ============ Errors ============ */ + error IncomeVault_ClaimNotActivated(); + error IncomeVault_DividendAlreadyClaimed(); + error IncomeVault_NoDividendToClaim(); + error IncomeVault_AdminWithAddressZeroNotAllowed(); + error IncomeVault_TokenPaymentWithAddressZeroNotAllowed(); + error IncomeVault_SnapshotSourceWithAddressZeroNotAllowed(); + /** + * @notice Thrown when the withdraw time limit is set to zero. + * @dev A limit of zero collapses the claim window `[time, time + limit]` to the single instant + * `block.timestamp == time`, making the period effectively unclaimable. + */ + error IncomeVault_TimeLimitToWithdrawZeroNotAllowed(); + /** + * @notice Thrown when the snapshot source is changed while at least one claim period is open. + * @param openClaimCount how many dividend times currently have their claims open + */ + error IncomeVault_ClaimPeriodOpen(uint256 openClaimCount); + /** + * @notice Thrown when {IncomeVaultRestricted-transferDividendSelf} is called by anyone but the vault. + * @dev That function exists only so the best-effort distribution can wrap a payout in try/catch, + * which requires an external call. It must never be reachable from outside. + */ + error IncomeVault_OnlySelfCall(); + /** + * @notice Thrown when {IncomeVaultRestricted-depositBatch} is given arrays of different lengths. + * @param timesLength the number of dividend times supplied + * @param amountsLength the number of amounts supplied + */ + error IncomeVault_InvalidLengths(uint256 timesLength, uint256 amountsLength); + /** + * @notice Thrown when a caller claims for a holder without being that holder or their operator. + * @param holder the token holder whose dividends were targeted + * @param caller the address that attempted the claim + */ + error IncomeVault_UnauthorizedOperator(address holder, address caller); + error IncomeVault_NoAmountSend(); + error IncomeVault_NotEnoughAmount(); + error IncomeVault_TokenBalanceIsZero(); + error IncomeVault_TooLateToWithdraw(uint256 currentTime); + error IncomeVault_TooEarlyToWithdraw(uint256 currentTime); + /** + * @notice Thrown when the ValidationModule (pause, freeze or RuleEngine) forbids the payout. + */ + error IncomeVault_InvalidTransfer(address from, address to, uint256 value); + error IncomeVault_SameValue(); +} diff --git a/src/storage/IncomeVaultRolesStorage.sol b/src/storage/IncomeVaultRolesStorage.sol new file mode 100644 index 0000000..a9c5855 --- /dev/null +++ b/src/storage/IncomeVaultRolesStorage.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.24; + +/** + * @title Role identifiers of the role-based IncomeVault deployment + * @dev + * These constants are inherited **only** by the deployment that actually enforces them + * ({IncomeVault}). They are deliberately kept out of {IncomeVaultInvariantStorage}: a variant using + * another access-control policy — {IncomeVaultOwnable2Step} — would otherwise publish roles it never + * checks, and granting one would confer no privilege with no on-chain signal that it had no effect. + */ +abstract contract IncomeVaultRolesStorage { + /** + * @notice Role allowed to open/close the claims and to configure the withdraw time limit + */ + bytes32 public constant INCOME_VAULT_OPERATOR_ROLE = keccak256("INCOME_VAULT_OPERATOR_ROLE"); + /** + * @notice Role allowed to deposit the payment token in the vault + */ + bytes32 public constant INCOME_VAULT_DEPOSIT_ROLE = keccak256("INCOME_VAULT_DEPOSIT_ROLE"); + /** + * @notice Role allowed to push the dividends to a list of token holders + */ + bytes32 public constant INCOME_VAULT_DISTRIBUTE_ROLE = keccak256("INCOME_VAULT_DISTRIBUTE_ROLE"); + /** + * @notice Role allowed to withdraw the payment token from the vault + */ + bytes32 public constant INCOME_VAULT_WITHDRAW_ROLE = keccak256("INCOME_VAULT_WITHDRAW_ROLE"); +} diff --git a/test/AccessControlHooks.t.sol b/test/AccessControlHooks.t.sol new file mode 100644 index 0000000..80bc78d --- /dev/null +++ b/test/AccessControlHooks.t.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; + +/** + * @title Access-control hooks — both deployment variants + * @dev + * The logic contracts declare the capabilities, the deployment contracts declare the policy. + * These tests pin the policy of each variant: who is accepted, who is rejected, and that the + * role-based variant really separates duties while the single-owner variant really does not. + */ +contract AccessControlHooksTest is HelperContract { + address constant NEW_OWNER = address(12); + address constant DEPOSITOR = address(13); + address constant WITHDRAWER = address(14); + + function setUp() public { + _deployContracts(); + + _deployOwnableVault(); + tokenPayment.mint(OWNER, tokenBalance); + } + + /* ============ Role variant: every hook accepts its intended holder ============ */ + function testDepositRoleHolderCanDeposit() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.grantRole(INCOME_VAULT_DEPOSIT_ROLE, DEPOSITOR); + tokenPayment.mint(DEPOSITOR, defaultDepositAmount); + + vm.prank(DEPOSITOR); + tokenPayment.approve(address(incomeVault), defaultDepositAmount); + vm.prank(DEPOSITOR); + incomeVault.deposit(defaultSnapshotTime, defaultDepositAmount); + + assertEq(incomeVault.segregatedDividend(defaultSnapshotTime), defaultDepositAmount); + } + + function testWithdrawRoleHolderCanWithdraw() public { + _performOnlyDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.grantRole(INCOME_VAULT_WITHDRAW_ROLE, WITHDRAWER); + + vm.prank(WITHDRAWER); + incomeVault.withdraw(defaultSnapshotTime, defaultDepositAmount, ADDRESS2); + assertEq(tokenPayment.balanceOf(ADDRESS2), defaultDepositAmount); + } + + /* ============ Role variant: the separation of duties is real ============ */ + /** + * @notice A depositor cannot drain the vault — the capability that motivates the role variant + */ + function testDepositRoleCannotWithdraw() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.grantRole(INCOME_VAULT_DEPOSIT_ROLE, DEPOSITOR); + _performOnlyDeposit(); + + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, DEPOSITOR, INCOME_VAULT_WITHDRAW_ROLE) + ); + vm.prank(DEPOSITOR); + incomeVault.withdraw(defaultSnapshotTime, defaultDepositAmount, DEPOSITOR); + } + + function testWithdrawRoleCannotDeposit() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.grantRole(INCOME_VAULT_WITHDRAW_ROLE, WITHDRAWER); + + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, WITHDRAWER, INCOME_VAULT_DEPOSIT_ROLE) + ); + vm.prank(WITHDRAWER); + incomeVault.deposit(defaultSnapshotTime, defaultDepositAmount); + } + + function testDepositRoleCannotOperateTheClaimWindow() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.grantRole(INCOME_VAULT_DEPOSIT_ROLE, DEPOSITOR); + + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, DEPOSITOR, INCOME_VAULT_OPERATOR_ROLE) + ); + vm.prank(DEPOSITOR); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + } + + function testDepositRoleCannotSetTheRuleEngine() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.grantRole(INCOME_VAULT_DEPOSIT_ROLE, DEPOSITOR); + + vm.expectRevert(abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, DEPOSITOR, bytes32(0))); + vm.prank(DEPOSITOR); + incomeVault.setRuleEngine(IRuleEngine(ADDRESS3)); + } + + function testDepositRoleCannotFreeze() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.grantRole(INCOME_VAULT_DEPOSIT_ROLE, DEPOSITOR); + + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, DEPOSITOR, incomeVault.ENFORCER_ROLE()) + ); + vm.prank(DEPOSITOR); + incomeVault.setAddressFrozen(ADDRESS1, true, ""); + } + + /* ============ Ownable variant: the owner holds every capability ============ */ + function testOwnerCanUseEveryCapability() public { + vm.prank(OWNER); + tokenPayment.approve(address(ownableVault), defaultDepositAmount); + vm.prank(OWNER); + ownableVault.deposit(defaultSnapshotTime, defaultDepositAmount); + assertEq(ownableVault.segregatedDividend(defaultSnapshotTime), defaultDepositAmount); + + vm.prank(OWNER); + ownableVault.setStatusClaim(defaultSnapshotTime, true); + assertEq(ownableVault.segregatedClaim(defaultSnapshotTime), true); + + vm.prank(OWNER); + ownableVault.setTimeLimitToWithdraw(1 days); + assertEq(ownableVault.timeLimitToWithdraw(), 1 days); + + vm.prank(OWNER); + ownableVault.setAddressFrozen(ADDRESS1, true, ""); + assertEq(ownableVault.isFrozen(ADDRESS1), true); + + vm.prank(OWNER); + ownableVault.pause(); + assertEq(ownableVault.paused(), true); + + vm.prank(OWNER); + ownableVault.unpause(); + + vm.prank(OWNER); + ownableVault.withdraw(defaultSnapshotTime, defaultDepositAmount, ADDRESS2); + assertEq(tokenPayment.balanceOf(ADDRESS2), defaultDepositAmount); + } + + /** + * @notice The documented limitation: the funder of the vault is also the account that can empty it + */ + function testOwnableVariantCannotSeparateDepositFromWithdraw() public { + vm.prank(OWNER); + tokenPayment.approve(address(ownableVault), defaultDepositAmount); + vm.prank(OWNER); + ownableVault.deposit(defaultSnapshotTime, defaultDepositAmount); + + // the very same account drains it, with no role to withhold + vm.prank(OWNER); + ownableVault.withdrawAll(defaultDepositAmount, OWNER); + assertEq(tokenPayment.balanceOf(OWNER), tokenBalance); + } + + /* ============ Ownable variant: every hook rejects a non-owner ============ */ + function testAttackerCannotDeposit() public { + _expectNotOwner(); + vm.prank(ATTACKER); + ownableVault.deposit(defaultSnapshotTime, defaultDepositAmount); + } + + function testAttackerCannotWithdraw() public { + _expectNotOwner(); + vm.prank(ATTACKER); + ownableVault.withdraw(defaultSnapshotTime, 1, ATTACKER); + } + + function testAttackerCannotWithdrawAll() public { + _expectNotOwner(); + vm.prank(ATTACKER); + ownableVault.withdrawAll(1, ATTACKER); + } + + function testAttackerCannotDistribute() public { + address[] memory addresses = new address[](0); + _expectNotOwner(); + vm.prank(ATTACKER); + ownableVault.distributeDividend(addresses, defaultSnapshotTime); + } + + function testAttackerCannotOperate() public { + _expectNotOwner(); + vm.prank(ATTACKER); + ownableVault.setStatusClaim(defaultSnapshotTime, true); + } + + function testAttackerCannotSetTimeLimit() public { + _expectNotOwner(); + vm.prank(ATTACKER); + ownableVault.setTimeLimitToWithdraw(1 days); + } + + function testAttackerCannotSetRuleEngine() public { + _expectNotOwner(); + vm.prank(ATTACKER); + ownableVault.setRuleEngine(IRuleEngine(ADDRESS3)); + } + + function testAttackerCannotPause() public { + _expectNotOwner(); + vm.prank(ATTACKER); + ownableVault.pause(); + } + + function testAttackerCannotFreeze() public { + _expectNotOwner(); + vm.prank(ATTACKER); + ownableVault.setAddressFrozen(ADDRESS1, true, ""); + } + + /* ============ Ownable2Step handover ============ */ + function testTransferOwnershipAloneDoesNotMoveControl() public { + vm.prank(OWNER); + ownableVault.transferOwnership(NEW_OWNER); + + assertEq(ownableVault.owner(), OWNER); + assertEq(ownableVault.pendingOwner(), NEW_OWNER); + + // the pending owner has no capability yet + _expectNotOwner(NEW_OWNER); + vm.prank(NEW_OWNER); + ownableVault.setStatusClaim(defaultSnapshotTime, true); + } + + function testAcceptOwnershipMovesControl() public { + vm.prank(OWNER); + ownableVault.transferOwnership(NEW_OWNER); + vm.prank(NEW_OWNER); + ownableVault.acceptOwnership(); + + assertEq(ownableVault.owner(), NEW_OWNER); + + vm.prank(NEW_OWNER); + ownableVault.setStatusClaim(defaultSnapshotTime, true); + assertEq(ownableVault.segregatedClaim(defaultSnapshotTime), true); + + // and the previous owner has lost it + _expectNotOwner(OWNER); + vm.prank(OWNER); + ownableVault.setStatusClaim(defaultSnapshotTime, false); + } + + /* ============ ERC-165 ============ */ + function testRoleVariantAdvertisesAccessControl() public view { + assertEq(incomeVault.supportsInterface(type(IAccessControl).interfaceId), true); + } + + function testOwnableVariantAdvertisesErc173AndOwnable2Step() public view { + assertEq(ownableVault.supportsInterface(ownableVault.IERC173_INTERFACE_ID()), true); + assertEq(ownableVault.supportsInterface(ownableVault.IOWNABLE2STEP_INTERFACE_ID()), true); + assertEq(ownableVault.supportsInterface(0xffffffff), false); + } + + /* ============ helpers ============ */ + function _expectNotOwner() internal { + _expectNotOwner(ATTACKER); + } + + function _expectNotOwner(address caller) internal { + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", caller)); + } +} diff --git a/test/CodeQuality.t.sol b/test/CodeQuality.t.sol new file mode 100644 index 0000000..c21f40a --- /dev/null +++ b/test/CodeQuality.t.sol @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/** + * @title Regression tests for the findings of CLAUDE_ANALYSIS.md + */ +contract CodeQualityTest is HelperContract { + function setUp() public { + _deployContracts(); + } + + /* ============ C-1 — the claim switch is evented ============ */ + function testSetStatusClaimEmits() public { + vm.expectEmit(true, false, false, true); + emit ClaimStatusSet(defaultSnapshotTime, true); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + } + + /* ============ C-2 — the payment token is evented like its sibling ============ */ + function testInitializeEmitsBothEngineEvents() public { + Options memory opts; + opts.constructorData = abi.encode(ZERO_ADDRESS); + + vm.recordLogs(); + Upgrades.deployTransparentProxy( + "IncomeVault.sol", + DEFAULT_ADMIN_ADDRESS, + abi.encodeCall( + IncomeVault.initialize, + ( + DEFAULT_ADMIN_ADDRESS, + IERC20(address(tokenPayment)), + ISnapshotSource(address(snapshotEngine)), + IRuleEngine(ZERO_ADDRESS), + TIME_LIMIT_TO_WITHDRAW + ) + ), + opts + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + bool payment; + bool snapshot; + bool timeLimit; + for (uint256 i = 0; i < logs.length; ++i) { + bytes32 t = logs[i].topics[0]; + if (t == ERC20TokenPaymentSet.selector) payment = true; + if (t == DividendSnapshotSourceSet.selector) snapshot = true; + if (t == TimeLimitToWithdrawSet.selector) timeLimit = true; + } + assertTrue(payment, "ERC20TokenPaymentSet not emitted at initialize"); + assertTrue(snapshot, "DividendSnapshotSourceSet not emitted at initialize"); + assertTrue(timeLimit, "TimeLimitToWithdrawSet not emitted at initialize"); + } + + /* ============ C-3 — funds leaving the vault are evented ============ */ + function testWithdrawEmits() public { + _performOnlyDeposit(); + vm.expectEmit(true, true, false, true); + emit Withdraw(defaultSnapshotTime, ADDRESS2, defaultDepositAmount); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.withdraw(defaultSnapshotTime, defaultDepositAmount, ADDRESS2); + } + + function testWithdrawAllEmits() public { + _performOnlyDeposit(); + vm.expectEmit(true, false, false, true); + emit WithdrawAll(ADDRESS2, defaultDepositAmount); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.withdrawAll(defaultDepositAmount, ADDRESS2); + } + + function testSetTimeLimitToWithdrawEmits() public { + vm.expectEmit(false, false, false, true); + emit TimeLimitToWithdrawSet(1 days); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setTimeLimitToWithdraw(1 days); + } + + /* ============ A-1 — the batch path still behaves identically ============ */ + function testValidateTimeBatchStillRejectsEachCode() public { + uint256[] memory times = new uint256[](1); + times[0] = defaultSnapshotTime; + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_ClaimNotActivated.selector)); + incomeVault.validateTimeBatch(times); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); + incomeVault.validateTimeBatch(times); + + vm.warp(defaultSnapshotTime + TIME_LIMIT_TO_WITHDRAW + 1); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); + incomeVault.validateTimeBatch(times); + } + + /** + * @notice The hoisted read must not change what a single-element batch reports + */ + function testValidateTimeBatchMatchesValidateTime() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 10); + + uint256[] memory times = new uint256[](1); + times[0] = defaultSnapshotTime; + incomeVault.validateTimeBatch(times); // does not revert + incomeVault.validateTime(defaultSnapshotTime); // same verdict + assertEq(uint256(incomeVault.validateTimeCode(defaultSnapshotTime)), 0); + } + + /* ============ H-1 — the push path applies the same claim window as the pull path ============ */ + /** + * @notice `distributeDividend` must refuse a distribution before `time` + * @dev + * Before `time` the snapshot has not been recorded, and {ISnapshotSource} falls back to the **live** + * balance — so a distribution would pay from current balances and permanently consume the holder's + * claim for that period at the wrong amount. This is finding H-1. + */ + function testCannotDistributeBeforeTheDividendTime() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + + assertLt(block.timestamp, defaultSnapshotTime); + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + + // nothing was paid and the claim is still available to the holder + assertEq(tokenPayment.balanceOf(ADDRESS1), 0); + assertEq(incomeVault.claimedDividend(ADDRESS1, defaultSnapshotTime), false); + } + + /** + * @notice `distributeDividend` must refuse a distribution after the withdraw limit + */ + function testCannotDistributeAfterTheWithdrawLimit() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + TIME_LIMIT_TO_WITHDRAW + 1); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + } + + /** + * @notice The claim-not-activated case still reverts with its own error + */ + function testCannotDistributeWhenTheClaimIsNotActivated() public { + _performDeposit(); + vm.warp(defaultSnapshotTime + 50); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.expectRevert(abi.encodeWithSelector(IncomeVault_ClaimNotActivated.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + } + + /** + * @notice Inside the window the distribution still works, on the recorded snapshot balances + */ + function testDistributeInsideTheWindowStillWorks() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount); + assertEq(incomeVault.claimedDividend(ADDRESS1, defaultSnapshotTime), true); + } + + /** + * @notice The push path and the pull path now agree on when a payout is allowed + */ + function testPushAndPullAgreeOnTheWindow() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + + // too early: both refuse, with the same error + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); + vm.prank(ADDRESS1); + incomeVault.claimDividend(defaultSnapshotTime); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + + // too late: both refuse, with the same error + vm.warp(defaultSnapshotTime + TIME_LIMIT_TO_WITHDRAW + 1); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); + vm.prank(ADDRESS1); + incomeVault.claimDividend(defaultSnapshotTime); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + } + + /* ============ H-2 — the push path enforces the same restrictions as the pull path ============ */ + /** + * @notice A frozen holder cannot be paid by the issuer either + */ + function testCannotDistributeToAFrozenHolder() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setAddressFrozen(ADDRESS1, true, ""); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.expectRevert( + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + + assertEq(tokenPayment.balanceOf(ADDRESS1), 0); + assertEq(incomeVault.claimedDividend(ADDRESS1, defaultSnapshotTime), false); + } + + /** + * @notice Pausing the vault stops the issuer-driven distribution too + */ + function testCannotDistributeWhilePaused() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.pause(); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.expectRevert( + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + } + + /** + * @notice One blocked holder reverts the whole batch, and the error names that holder + * @dev Same semantics as {claimDividendBatch}: the vault fails closed rather than paying a + * partial set silently. `IncomeVault_InvalidTransfer` carries the address so the operator can + * remove it from the list and retry. + */ + function testOneBlockedHolderRevertsTheWholeDistribution() public { + _performOnlyDeposit(); + vm.prank(CMTAT_ADMIN); + snapshotEngine.scheduleSnapshot(defaultSnapshotTime); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS2, ADDRESS1_INITIAL_AMOUNT); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + // only the second holder is frozen + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setAddressFrozen(ADDRESS2, true, ""); + + address[] memory addresses = new address[](2); + addresses[0] = ADDRESS1; + addresses[1] = ADDRESS2; + vm.expectRevert( + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS2, defaultDepositAmount / 2 + ) + ); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + + // the whole batch is rolled back, including the holder who was allowed + assertEq(tokenPayment.balanceOf(ADDRESS1), 0); + assertEq(incomeVault.claimedDividend(ADDRESS1, defaultSnapshotTime), false); + } + + /** + * @notice With every holder allowed the distribution is unchanged + */ + function testDistributeStillWorksWhenEveryHolderIsAllowed() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount); + } + + /* ============ A-1 — a zero withdraw limit cannot be configured ============ */ + /** + * @notice `setTimeLimitToWithdraw(0)` is refused + * @dev + * With `timeLimitToWithdraw == 0` the claim window `[time, time + limit]` collapses to the single + * instant `block.timestamp == time`: one second later `_timeCode` already returns + * `TOO_LATE_TO_WITHDRAW`. The period becomes effectively unclaimable, and nothing signalled it — + * the transaction succeeded and the event fired. Finding A-1 of `CLAUDE_IMPROVEMENT.md`. + */ + function testCannotSetAZeroTimeLimitToWithdraw() public { + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TimeLimitToWithdrawZeroNotAllowed.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setTimeLimitToWithdraw(0); + + // the previous value is untouched + assertEq(incomeVault.timeLimitToWithdraw(), TIME_LIMIT_TO_WITHDRAW); + } + + /** + * @notice The guard lives in the internal setter, so `initialize` is covered too + * @dev This is the point of validating in `_setTimeLimitToWithdraw` rather than at the call site: + * a vault cannot be *deployed* into the bricked state either. + */ + function testCannotInitializeWithAZeroTimeLimitToWithdraw() public { + IncomeVault implementation = new IncomeVault(ZERO_ADDRESS); + bytes memory data = abi.encodeCall( + IncomeVault.initialize, + ( + DEFAULT_ADMIN_ADDRESS, + IERC20(address(tokenPayment)), + ISnapshotSource(address(snapshotEngine)), + IRuleEngine(ZERO_ADDRESS), + 0 + ) + ); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TimeLimitToWithdrawZeroNotAllowed.selector)); + new TransparentUpgradeableProxy(address(implementation), DEFAULT_ADMIN_ADDRESS, data); + } + + /** + * @notice Any positive value is still accepted — only zero is refused + * @dev A short window may be a deliberate settlement policy; zero is the only value that is + * broken by definition, so it is the only one rejected. + */ + function testAOneSecondTimeLimitIsStillAccepted() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setTimeLimitToWithdraw(1); + assertEq(incomeVault.timeLimitToWithdraw(), 1); + } +} diff --git a/test/Deactivate.t.sol b/test/Deactivate.t.sol new file mode 100644 index 0000000..46551a8 --- /dev/null +++ b/test/Deactivate.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; + +/** + * @title `deactivateContract` — finding B-1 + * @dev + * The only irreversible action in the system: once deactivated the vault can never be unpaused, so + * with a proxy the sole way back is a new implementation. It was previously untested in both variants. + */ +contract DeactivateTest is HelperContract { + function setUp() public { + _deployContracts(); + + _deployOwnableVault(); + } + + /* ============ role-based variant ============ */ + function testAdminCanDeactivateAPausedVault() public { + assertEq(incomeVault.deactivated(), false); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.pause(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deactivateContract(); + + assertEq(incomeVault.deactivated(), true); + assertEq(incomeVault.paused(), true); + } + + /** + * @notice The vault must be paused first — deactivation is not a shortcut around the pause + */ + function testCannotDeactivateWithoutPausingFirst() public { + vm.expectRevert(abi.encodeWithSignature("ExpectedPause()")); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deactivateContract(); + assertEq(incomeVault.deactivated(), false); + } + + /** + * @notice Deactivation is irreversible: the vault can never be unpaused again + */ + function testADeactivatedVaultCanNeverBeUnpaused() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.pause(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deactivateContract(); + + vm.expectRevert(abi.encodeWithSignature("CMTAT_PauseModule_ContractIsDeactivated()")); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.unpause(); + } + + function testCannotDeactivateTwice() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.pause(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deactivateContract(); + + vm.expectRevert(abi.encodeWithSignature("AlreadyDeactivated()")); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deactivateContract(); + } + + /** + * @notice A deactivated vault pays nobody — the practical consequence + */ + function testADeactivatedVaultRefusesEveryPayout() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.pause(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deactivateContract(); + + vm.expectRevert( + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); + vm.prank(ADDRESS1); + incomeVault.claimDividend(defaultSnapshotTime); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.expectRevert( + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + } + + function testAttackerCannotDeactivate() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.pause(); + + vm.expectRevert(abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, bytes32(0))); + vm.prank(ATTACKER); + incomeVault.deactivateContract(); + assertEq(incomeVault.deactivated(), false); + } + + /** + * @notice `PAUSER_ROLE` alone is not enough — deactivation needs the admin + */ + function testPauserRoleAloneCannotDeactivate() public { + address pauser = address(21); + // read the role first: a call inside the argument list would consume the prank + bytes32 pauserRole = incomeVault.PAUSER_ROLE(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.grantRole(pauserRole, pauser); + + vm.prank(pauser); + incomeVault.pause(); + + vm.expectRevert(abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, pauser, bytes32(0))); + vm.prank(pauser); + incomeVault.deactivateContract(); + } + + /* ============ single-owner variant ============ */ + function testOwnerCanDeactivate() public { + vm.prank(OWNER); + ownableVault.pause(); + vm.prank(OWNER); + ownableVault.deactivateContract(); + assertEq(ownableVault.deactivated(), true); + } + + function testAttackerCannotDeactivateOwnableVariant() public { + vm.prank(OWNER); + ownableVault.pause(); + + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", ATTACKER)); + vm.prank(ATTACKER); + ownableVault.deactivateContract(); + assertEq(ownableVault.deactivated(), false); + } +} diff --git a/test/DepositBatch.t.sol b/test/DepositBatch.t.sol new file mode 100644 index 0000000..0651e45 --- /dev/null +++ b/test/DepositBatch.t.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; + +/** + * @title Batch deposit — finding E-2 + */ +contract DepositBatchTest is HelperContract { + uint256[] times; + uint256[] amounts; + + function setUp() public { + _deployContracts(); + times = [uint256(1_000), 2_000, 3_000]; + amounts = [uint256(100), 200, 300]; + tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, 10_000); + } + + function _approve(uint256 amount) internal { + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenPayment.approve(address(incomeVault), amount); + } + + /* ============ behaviour ============ */ + function testDepositBatchCreditsEveryTime() public { + _approve(600); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.depositBatch(times, amounts); + + assertEq(incomeVault.segregatedDividend(1_000), 100); + assertEq(incomeVault.segregatedDividend(2_000), 200); + assertEq(incomeVault.segregatedDividend(3_000), 300); + assertEq(tokenPayment.balanceOf(address(incomeVault)), 600); + } + + /** + * @notice One transfer for the batch, one event per entry + */ + function testDepositBatchPullsTheTokenOnceAndEventsEachEntry() public { + _approve(600); + vm.recordLogs(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.depositBatch(times, amounts); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + uint256 deposits; + uint256 transfers; + for (uint256 i = 0; i < logs.length; ++i) { + if (logs[i].topics[0] == newDeposit.selector) ++deposits; + if (logs[i].topics[0] == keccak256("Transfer(address,address,uint256)")) ++transfers; + } + assertEq(deposits, 3, "one newDeposit per entry"); + assertEq(transfers, 1, "the payment token is pulled once for the total"); + } + + /** + * @notice Identical outcome to calling `deposit` once per entry + */ + function testDepositBatchMatchesSeparateDeposits() public { + _approve(600); + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deposit(times[0], amounts[0]); + incomeVault.deposit(times[1], amounts[1]); + incomeVault.deposit(times[2], amounts[2]); + vm.stopPrank(); + + uint256[3] memory separate = [ + incomeVault.segregatedDividend(times[0]), + incomeVault.segregatedDividend(times[1]), + incomeVault.segregatedDividend(times[2]) + ]; + uint256 separateBalance = tokenPayment.balanceOf(address(incomeVault)); + + // same again on a fresh vault, in one call + _deployContracts(); + tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, 10_000); + _approve(600); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.depositBatch(times, amounts); + + assertEq(incomeVault.segregatedDividend(times[0]), separate[0]); + assertEq(incomeVault.segregatedDividend(times[1]), separate[1]); + assertEq(incomeVault.segregatedDividend(times[2]), separate[2]); + assertEq(tokenPayment.balanceOf(address(incomeVault)), separateBalance); + } + + /** + * @notice A repeated time accumulates, exactly as two separate deposits would + */ + function testRepeatedTimeAccumulates() public { + uint256[] memory t = new uint256[](2); + uint256[] memory a = new uint256[](2); + t[0] = 500; + t[1] = 500; + a[0] = 40; + a[1] = 60; + + _approve(100); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.depositBatch(t, a); + assertEq(incomeVault.segregatedDividend(500), 100); + } + + /* ============ guards ============ */ + function testCannotDepositBatchWithMismatchedLengths() public { + uint256[] memory a = new uint256[](2); + a[0] = 1; + a[1] = 2; + _approve(600); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_InvalidLengths.selector, 3, 2)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.depositBatch(times, a); + } + + function testCannotDepositBatchWithAZeroAmount() public { + uint256[] memory a = new uint256[](3); + a[0] = 100; + a[1] = 0; + a[2] = 300; + _approve(600); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_NoAmountSend.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.depositBatch(times, a); + + // nothing was credited: the whole batch rolled back + assertEq(incomeVault.segregatedDividend(times[0]), 0); + } + + function testCannotDepositAnEmptyBatch() public { + uint256[] memory t = new uint256[](0); + uint256[] memory a = new uint256[](0); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_NoAmountSend.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.depositBatch(t, a); + } + + function testCannotDepositBatchWithoutEnoughAllowance() public { + _approve(599); + vm.expectRevert(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.depositBatch(times, amounts); + } + + function testAttackerCannotDepositBatch() public { + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_DEPOSIT_ROLE) + ); + vm.prank(ATTACKER); + incomeVault.depositBatch(times, amounts); + } + + /* ============ what it actually saves ============ */ + /** + * @notice The batch is cheaper per *transaction*, not per call + * @dev + * Measured, and the naive framing is misleading: **inside a single transaction the batch costs + * more** than the separate calls, because decoding two dynamic `calldata` arrays outweighs what is + * saved by pulling the payment token once. + * + * The win is the intrinsic transaction cost. An issuer opening N periods sends **one** transaction + * instead of N, paying 21,000 gas of base cost once rather than N times. This test compares the + * totals a caller really pays. + */ + /** + * @dev Only the per-transaction total is asserted. The in-call figures are printed, not checked: + * the two sides are measured across a different number of external calls — one for the batch, + * three for the separate deposits — and Foundry's instrumentation charges per call, so under + * `--gas-report` (what `make gas` runs) the overhead lands three times as heavily on the second + * measurement and reverses the comparison on bytecode that has not changed. Asserting a + * direction that the measurement mode decides would fail the suite for the wrong reason. + * + * Gas is read with `vm.lastCallGas()`, the EVM's own accounting, rather than from `gasleft()` + * deltas around the calls, which also count what the harness spends between them. + */ + function testBatchIsCheaperPerTransaction() public { + uint256 intrinsic = 21_000; + + _approve(600); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.depositBatch(times, amounts); + uint256 batchCall = vm.lastCallGas().gasTotalUsed; + + _deployContracts(); + tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, 10_000); + _approve(600); + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deposit(times[0], amounts[0]); + uint256 separateCalls = vm.lastCallGas().gasTotalUsed; + incomeVault.deposit(times[1], amounts[1]); + separateCalls += vm.lastCallGas().gasTotalUsed; + incomeVault.deposit(times[2], amounts[2]); + separateCalls += vm.lastCallGas().gasTotalUsed; + vm.stopPrank(); + + uint256 batchTotal = batchCall + intrinsic; + uint256 separateTotal = separateCalls + (3 * intrinsic); + + console.log("in-call depositBatch(3):", batchCall); + console.log("in-call 3 x deposit: ", separateCalls); + console.log("total depositBatch(3):", batchTotal); + console.log("total 3 x deposit: ", separateTotal); + + // a caller sending one transaction instead of three comes out ahead, which is the point of + // the batch: the saving is the intrinsic cost paid once, not a cheaper deposit + assertLt(batchTotal, separateTotal, "batch should win once the per-transaction cost is counted"); + } +} diff --git a/test/DistributeBestEffort.t.sol b/test/DistributeBestEffort.t.sol new file mode 100644 index 0000000..b8b9825 --- /dev/null +++ b/test/DistributeBestEffort.t.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {Vm} from "forge-std/Vm.sol"; + +/** + * @title Best-effort distribution — finding A-4 + */ +contract DistributeBestEffortTest is HelperContract { + function setUp() public { + _deployContracts(); + } + + /// @dev two holders with equal balances, claims open, inside the window + function _twoHolders() internal returns (address[] memory addresses) { + _performOnlyDeposit(); + vm.prank(CMTAT_ADMIN); + snapshotEngine.scheduleSnapshot(defaultSnapshotTime); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS2, ADDRESS1_INITIAL_AMOUNT); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + addresses = new address[](2); + addresses[0] = ADDRESS1; + addresses[1] = ADDRESS2; + } + + /* ============ the point of the function ============ */ + function testABlockedHolderIsSkippedAndTheRestArePaid() public { + address[] memory addresses = _twoHolders(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setAddressFrozen(ADDRESS2, true, ""); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + (uint256 paidCount, address[] memory skipped) = + incomeVault.distributeDividendBestEffort(addresses, defaultSnapshotTime); + + assertEq(paidCount, 1); + assertEq(skipped.length, 1); + assertEq(skipped[0], ADDRESS2); + + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount / 2); + assertEq(tokenPayment.balanceOf(ADDRESS2), 0); + } + + /** + * @notice A skipped holder is left completely untouched and can still claim + * @dev Per-holder atomicity: the self-call rolls back `claimedDividend` along with the transfer, + * so a holder is never marked as paid without receiving the tokens. + */ + function testASkippedHolderIsUntouchedAndCanClaimLater() public { + address[] memory addresses = _twoHolders(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setAddressFrozen(ADDRESS2, true, ""); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividendBestEffort(addresses, defaultSnapshotTime); + assertEq(incomeVault.claimedDividend(ADDRESS2, defaultSnapshotTime), false); + + // unfreeze, and the holder claims for themselves + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setAddressFrozen(ADDRESS2, false, ""); + vm.prank(ADDRESS2); + incomeVault.claimDividend(defaultSnapshotTime); + assertEq(tokenPayment.balanceOf(ADDRESS2), defaultDepositAmount / 2); + } + + /** + * @notice The skip is reported with decodable revert data + */ + function testSkipEmitsTheReason() public { + address[] memory addresses = _twoHolders(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setAddressFrozen(ADDRESS2, true, ""); + + vm.recordLogs(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividendBestEffort(addresses, defaultSnapshotTime); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bool found; + for (uint256 i = 0; i < logs.length; ++i) { + if (logs[i].topics[0] == DividendDistributionSkipped.selector) { + found = true; + assertEq(uint256(logs[i].topics[1]), defaultSnapshotTime); + assertEq(address(uint160(uint256(logs[i].topics[2]))), ADDRESS2); + bytes memory reason = abi.decode(logs[i].data, (bytes)); + assertEq( + bytes4(reason), + IncomeVault_InvalidTransfer.selector, + "reason should decode to IncomeVault_InvalidTransfer" + ); + } + } + assertTrue(found, "DividendDistributionSkipped not emitted"); + } + + function testEveryHolderPaidWhenNoneIsBlocked() public { + address[] memory addresses = _twoHolders(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + (uint256 paidCount, address[] memory skipped) = + incomeVault.distributeDividendBestEffort(addresses, defaultSnapshotTime); + + assertEq(paidCount, 2); + assertEq(skipped.length, 0); + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount / 2); + assertEq(tokenPayment.balanceOf(ADDRESS2), defaultDepositAmount / 2); + } + + /* ============ the strict variant is unchanged ============ */ + function testTheStrictVariantStillRevertsOnTheSameInput() public { + address[] memory addresses = _twoHolders(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setAddressFrozen(ADDRESS2, true, ""); + + vm.expectRevert( + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS2, defaultDepositAmount / 2 + ) + ); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + } + + /* ============ the claim window still applies ============ */ + function testBestEffortStillEnforcesTheClaimWindow() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividendBestEffort(addresses, defaultSnapshotTime); + } + + /* ============ access control ============ */ + function testAttackerCannotRunTheBestEffortDistribution() public { + address[] memory addresses = _twoHolders(); + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_DISTRIBUTE_ROLE) + ); + vm.prank(ATTACKER); + incomeVault.distributeDividendBestEffort(addresses, defaultSnapshotTime); + } + + /** + * @notice The self-call helper is unreachable from outside — the critical guard + * @dev It carries no role check of its own, so without this the payout path would be open to + * anyone. Neither an attacker nor the privileged admin may call it directly. + */ + function testNobodyCanCallTheSelfHelperDirectly() public { + _twoHolders(); + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_OnlySelfCall.selector)); + vm.prank(ATTACKER); + incomeVault.transferDividendSelf(defaultSnapshotTime, ATTACKER, defaultDepositAmount); + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_OnlySelfCall.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.transferDividendSelf(defaultSnapshotTime, ADDRESS1, defaultDepositAmount); + + assertEq(tokenPayment.balanceOf(ATTACKER), 0); + } +} diff --git a/test/EdgeCases.t.sol b/test/EdgeCases.t.sol new file mode 100644 index 0000000..a891c94 --- /dev/null +++ b/test/EdgeCases.t.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {IncomeVaultOwnable2Step} from "../src/deployment/IncomeVaultOwnable2Step.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/** + * @title Branch coverage of the guards and the unconfigured paths — finding B-2 + * @dev + * These are the branches the behavioural suites never reach: the constructor guards, and the + * "no RuleEngine configured" answers. Each asserts the observable consequence, not just that the + * line executed. + */ +contract EdgeCasesTest is HelperContract { + function setUp() public { + _deployContracts(); + } + + /* ============ initializer guards ============ */ + function testCannotInitializeWithAZeroAdmin() public { + IncomeVault implementation = new IncomeVault(ZERO_ADDRESS); + bytes memory data = abi.encodeCall( + IncomeVault.initialize, + ( + ZERO_ADDRESS, + IERC20(address(tokenPayment)), + ISnapshotSource(address(snapshotEngine)), + IRuleEngine(ZERO_ADDRESS), + TIME_LIMIT_TO_WITHDRAW + ) + ); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_AdminWithAddressZeroNotAllowed.selector)); + new TransparentUpgradeableProxy(address(implementation), DEFAULT_ADMIN_ADDRESS, data); + } + + function testCannotInitializeTheOwnableVariantWithAZeroOwner() public { + IncomeVaultOwnable2Step implementation = new IncomeVaultOwnable2Step(ZERO_ADDRESS); + bytes memory data = abi.encodeCall( + IncomeVaultOwnable2Step.initialize, + ( + ZERO_ADDRESS, + IERC20(address(tokenPayment)), + ISnapshotSource(address(snapshotEngine)), + IRuleEngine(ZERO_ADDRESS), + TIME_LIMIT_TO_WITHDRAW + ) + ); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_AdminWithAddressZeroNotAllowed.selector)); + new TransparentUpgradeableProxy(address(implementation), DEFAULT_ADMIN_ADDRESS, data); + } + + function testCannotInitializeWithAZeroPaymentToken() public { + IncomeVault implementation = new IncomeVault(ZERO_ADDRESS); + bytes memory data = abi.encodeCall( + IncomeVault.initialize, + ( + DEFAULT_ADMIN_ADDRESS, + IERC20(ZERO_ADDRESS), + ISnapshotSource(address(snapshotEngine)), + IRuleEngine(ZERO_ADDRESS), + TIME_LIMIT_TO_WITHDRAW + ) + ); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TokenPaymentWithAddressZeroNotAllowed.selector)); + new TransparentUpgradeableProxy(address(implementation), DEFAULT_ADMIN_ADDRESS, data); + } + + /* ============ a holder with no tokens at the snapshot ============ */ + /** + * @notice Claiming with a zero snapshot balance is refused before any dividend is computed + * @dev Distinct from `IncomeVault_NoDividendToClaim`, which means "you held tokens but the + * computed share rounds to zero". + */ + function testHolderWithNoTokensAtTheSnapshotCannotClaim() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + // ADDRESS3 never received any security token + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TokenBalanceIsZero.selector)); + vm.prank(ADDRESS3); + incomeVault.claimDividend(defaultSnapshotTime); + } + + /* ============ no RuleEngine configured ============ */ + /** + * @notice With no RuleEngine, the ERC-1404 views answer "no restriction" rather than reverting + * @dev `_deployContracts()` wires no engine, so these are the unconfigured answers an integrator + * gets from a vault that relies on pause and freeze alone. + */ + function testErc1404ViewsWithoutARuleEngine() public view { + assertEq(address(incomeVault.ruleEngine()), ZERO_ADDRESS); + assertEq(incomeVault.detectTransferRestriction(address(incomeVault), ADDRESS1, 100), 0); + assertEq(incomeVault.messageForTransferRestriction(0), "NoRestriction"); + // A code the vault never issues, with no RuleEngine to ask. This used to answer + // "No restriction" for *every* code, including ones that mean something (H-1). + assertEq(incomeVault.messageForTransferRestriction(42), "UnknownCode"); + // and the payout is allowed + assertEq(incomeVault.canTransfer(address(incomeVault), ADDRESS1, 100), true); + } + + /* ============ every TIME_ERROR_CODE arm ============ */ + /** + * @notice `validateTime` maps each code to its own error, and `validateTimeCode` reports it + */ + function testEveryTimeErrorCodeArm() public { + // 1. claims not activated + assertEq(uint256(incomeVault.validateTimeCode(defaultSnapshotTime)), 1); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_ClaimNotActivated.selector)); + incomeVault.validateTime(defaultSnapshotTime); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + + // 3. too early + assertEq(uint256(incomeVault.validateTimeCode(defaultSnapshotTime)), 3); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); + incomeVault.validateTime(defaultSnapshotTime); + + // 0. OK — inside the window, no revert + vm.warp(defaultSnapshotTime + 50); + assertEq(uint256(incomeVault.validateTimeCode(defaultSnapshotTime)), 0); + incomeVault.validateTime(defaultSnapshotTime); + + // 2. too late + vm.warp(defaultSnapshotTime + TIME_LIMIT_TO_WITHDRAW + 1); + assertEq(uint256(incomeVault.validateTimeCode(defaultSnapshotTime)), 2); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); + incomeVault.validateTime(defaultSnapshotTime); + } +} diff --git a/test/HelperContract.sol b/test/HelperContract.sol index 312126d..7fd8434 100644 --- a/test/HelperContract.sol +++ b/test/HelperContract.sol @@ -1,23 +1,39 @@ //SPDX-License-Identifier: MPL-2.0 -pragma solidity ^0.8.20; - -import "forge-std/Test.sol"; -import "CMTAT/CMTAT_STANDALONE.sol"; -import "../src/IncomeVault.sol"; -import "RuleEngine/RuleEngine.sol"; -import "RuleEngine/rules/validation/RuleWhitelist.sol"; -import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +/* ==== CMTAT === */ +import {CMTATStandaloneSnapshot} from "CMTAT/deployment/snapshot/CMTATStandaloneSnapshot.sol"; +import {ICMTATConstructor} from "CMTAT/interfaces/technical/ICMTATConstructor.sol"; +import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {ISnapshotEngine} from "CMTAT/interfaces/engine/ISnapshotEngine.sol"; +/* ==== SnapshotEngine === */ +import {SnapshotEngine} from "SnapshotEngine/deployment/SnapshotEngine.sol"; +import {IERC20SnapshotCompatible} from "SnapshotEngine/interface/IERC20SnapshotCompatible.sol"; +import {ISnapshotState} from "SnapshotEngine/interface/ISnapshotState.sol"; +import {ISnapshotSource} from "../src/interfaces/ISnapshotSource.sol"; +/* ==== OpenZeppelin === */ +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +/* ==== IncomeVault === */ +import {IncomeVault} from "../src/deployment/IncomeVault.sol"; +import {IncomeVaultOwnable2Step} from "../src/deployment/IncomeVaultOwnable2Step.sol"; +import {IncomeVaultInvariantStorage} from "../src/storage/IncomeVaultInvariantStorage.sol"; +import {IncomeVaultRolesStorage} from "../src/storage/IncomeVaultRolesStorage.sol"; +import {ERC20PaymentMock} from "./mocks/ERC20PaymentMock.sol"; + /** -* @title Constants used by the tests -*/ -abstract contract HelperContract is IncomeVaultInvariantStorage { + * @title Constants and shared deployment used by the tests + */ +abstract contract HelperContract is Test, IncomeVaultInvariantStorage, IncomeVaultRolesStorage { // EOA to perform tests address constant ZERO_ADDRESS = address(0); address constant DEFAULT_ADMIN_ADDRESS = address(1); // Operator - address constant DEBT_VAULT_OPERATOR_ADDRESS = address(2); - address constant DEBT_VAULT_DEPOSIT_OPERATOR_ADDRESS = address(3); - address constant DEBT_VAULT_WITHDRAW_OPERATOR_ADDRESS = address(8); + address constant INCOME_VAULT_OPERATOR_ADDRESS = address(2); + address constant INCOME_VAULT_DEPOSIT_OPERATOR_ADDRESS = address(3); + address constant INCOME_VAULT_WITHDRAW_OPERATOR_ADDRESS = address(8); // Other address constant ATTACKER = address(4); address constant ADDRESS1 = address(5); @@ -25,42 +41,147 @@ abstract contract HelperContract is IncomeVaultInvariantStorage { address constant ADDRESS3 = address(7); address constant TOKEN_PAYMENT_ADMIN = address(8); address constant CMTAT_ADMIN = address(9); - // role string - string constant RULE_ENGINE_ROLE_HASH = - "0x774b3c5f4a8b37a7da21d72b7f2429e4a6d49c4de0ac5f2b831a1a539d0f0fd2"; - string constant WHITELIST_ROLE_HASH = - "0xdc72ed553f2544c34465af23b847953efeb813428162d767f9ba5f4013be6760"; - string constant DEFAULT_ADMIN_ROLE_HASH = - "0x0000000000000000000000000000000000000000000000000000000000000000"; - - // contract - CMTAT_STANDALONE CMTAT_CONTRACT; - - //bytes32 public constant RULE_ENGINE_ROLE = keccak256("RULE_ENGINE_ROLE"); + /// @dev owner of the {IncomeVaultOwnable2Step} deployment + address constant OWNER = address(11); + + string constant DEFAULT_ADMIN_ROLE_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000"; uint8 constant NO_ERROR = 0; // Forwarder - string ERC2771ForwarderDomain = 'ERC2771ForwarderDomain'; - - uint256 TIME_LIMIT_TO_WITHDRAW = 365 days; + string ERC2771ForwarderDomain = "ERC2771ForwarderDomain"; + uint256 constant TIME_LIMIT_TO_WITHDRAW = 365 days; // Contracts - CMTAT_STANDALONE tokenPayment; - IncomeVault debtVault; + /// @dev security token, source of the holder balances + CMTATStandaloneSnapshot CMTAT_CONTRACT; + /// @dev external snapshot engine bound to `CMTAT_CONTRACT`, implements {ISnapshotState} + SnapshotEngine snapshotEngine; + /// @dev ERC-20 used to pay the dividends + ERC20PaymentMock tokenPayment; + IncomeVault incomeVault; + /// @dev the single-owner deployment variant, only built by {_deployOwnableVault} + IncomeVaultOwnable2Step ownableVault; + // CMTAT value - uint256 FLAG = 5; - uint8 DECIMALS = 0; - uint256 ADDRESS1_INITIAL_AMOUNT = 5000; - uint256 CMTAT_ADMIN_INITIAL_AMOUNT = 5000; + uint8 constant DECIMALS = 0; + uint256 constant ADDRESS1_INITIAL_AMOUNT = 5000; - uint256 defaultSnapshotTime = block.timestamp + 50; - uint256 defaultDepositAmount = 2000; + uint256 constant defaultDepositAmount = 2000; + // Payment token minted to the deposit account + uint256 constant tokenBalance = 5000; - - // Custom error openZeppelin + // Custom error OpenZeppelin error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); - constructor() {} + + /** + * @dev Deploys the CMTAT, the external SnapshotEngine bound to it, the payment token and the + * IncomeVault behind a transparent proxy. The vault reads the balances through {ISnapshotSource}, + * so the snapshot engine — not the token — is what it is wired to. + */ + function _deployContracts(IRuleEngine ruleEngine_) internal { + // Security token + CMTAT_CONTRACT = new CMTATStandaloneSnapshot( + ZERO_ADDRESS, + CMTAT_ADMIN, + ICMTATConstructor.ERC20Attributes("CMTA Token", "CMTAT", DECIMALS), + ICMTATConstructor.ExtraInformationAttributes( + "CMTAT_ISIN", IERC1643CMTAT.DocumentInfo("", "", 0x00), "CMTAT_info" + ), + ICMTATConstructor.Engine(IRuleEngine(ZERO_ADDRESS)) + ); + + // Snapshot engine bound to the security token + snapshotEngine = new SnapshotEngine(IERC20SnapshotCompatible(address(CMTAT_CONTRACT)), CMTAT_ADMIN); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.setSnapshotEngine(ISnapshotEngine(address(snapshotEngine))); + + // Payment token + tokenPayment = new ERC20PaymentMock("Payment Token", "PAY"); + + // IncomeVault, deployed behind a transparent proxy + Options memory opts; + opts.constructorData = abi.encode(ZERO_ADDRESS); + address proxy = Upgrades.deployTransparentProxy( + "IncomeVault.sol", + DEFAULT_ADMIN_ADDRESS, + abi.encodeCall( + IncomeVault.initialize, + ( + DEFAULT_ADMIN_ADDRESS, + IERC20(address(tokenPayment)), + ISnapshotSource(address(snapshotEngine)), + ruleEngine_, + TIME_LIMIT_TO_WITHDRAW + ) + ), + opts + ); + incomeVault = IncomeVault(proxy); + + tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, tokenBalance); + } + + function _deployContracts() internal { + _deployContracts(IRuleEngine(ZERO_ADDRESS)); + } + + /** + * @dev Deploys the single-owner variant behind its own proxy, against the payment token and + * snapshot engine already built by {_deployContracts}. Call it after `_deployContracts()`. + * Kept here rather than repeated per suite — five test files used to carry a copy. + */ + function _deployOwnableVault() internal { + _deployOwnableVault(IRuleEngine(ZERO_ADDRESS)); + } + + /// @dev {_deployOwnableVault} with an explicit RuleEngine + function _deployOwnableVault(IRuleEngine ruleEngine_) internal { + Options memory opts; + opts.constructorData = abi.encode(ZERO_ADDRESS); + address proxy = Upgrades.deployTransparentProxy( + "IncomeVaultOwnable2Step.sol", + DEFAULT_ADMIN_ADDRESS, + abi.encodeCall( + IncomeVaultOwnable2Step.initialize, + ( + OWNER, + IERC20(address(tokenPayment)), + ISnapshotSource(address(snapshotEngine)), + ruleEngine_, + TIME_LIMIT_TO_WITHDRAW + ) + ), + opts + ); + ownableVault = IncomeVaultOwnable2Step(proxy); + } + + /* ============ Shared arrange helpers ============ */ + function _performOnlyDeposit() internal { + _performOnlyDeposit(defaultSnapshotTime, defaultDepositAmount); + } + + function _performOnlyDeposit(uint256 time, uint256 amount) internal { + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenPayment.approve(address(incomeVault), amount); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deposit(time, amount); + } + + /// @dev schedule the snapshot on the engine, then mint the security token + function _mintCMTATTokens() internal { + vm.prank(CMTAT_ADMIN); + snapshotEngine.scheduleSnapshot(defaultSnapshotTime); + + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); + } + + function _performDeposit() internal { + _performOnlyDeposit(); + _mintCMTATTokens(); + } } diff --git a/test/IncomeVault.t.sol b/test/IncomeVault.t.sol index 24a2f2f..562dd40 100644 --- a/test/IncomeVault.t.sol +++ b/test/IncomeVault.t.sol @@ -1,164 +1,53 @@ // SPDX-License-Identifier: MPL-2.0 -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; -import "forge-std/Test.sol"; import "./HelperContract.sol"; -import "CMTAT/interfaces/engine/IRuleEngine.sol"; -import "CMTAT/interfaces/engine/IAuthorizationEngine.sol"; -import {IncomeVault} from "../src/IncomeVault.sol"; -//import {Upgrades,} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; /** -* @title Test for IncomeVault -*/ -contract IncomeVaultTest is Test, HelperContract { + * @title Test for IncomeVault + */ +contract IncomeVaultTest is HelperContract { uint256 resUint256; - uint8 resUint8; bool resBool; - bool resCallBool; - string resString; - uint8 CODE_NONEXISTENT = 255; - - // ADMIN balance payment - uint256 tokenBalance = 5000; // Arrange function setUp() public { - // Deploy CMTAT - CMTAT_CONTRACT = new CMTAT_STANDALONE( - ZERO_ADDRESS, - CMTAT_ADMIN, - IAuthorizationEngine(address(0)), - "CMTA Token", - "CMTAT", - DECIMALS, - "CMTAT_ISIN", - "https://cmta.ch", - IRuleEngine(address(0)), - "CMTAT_info", - FLAG - ); - - // Token payment - tokenPayment = new CMTAT_STANDALONE( - ZERO_ADDRESS, - TOKEN_PAYMENT_ADMIN, - IAuthorizationEngine(address(0)), - "CMTA Token", - "CMTAT", - DECIMALS, - "CMTAT_ISIN", - "https://cmta.ch", - IRuleEngine(address(0)), - "CMTAT_info", - FLAG - ); - Options memory opts; - opts.constructorData = abi.encode(ZERO_ADDRESS); - address proxy = Upgrades.deployTransparentProxy( - "IncomeVault.sol", - DEFAULT_ADMIN_ADDRESS, - abi.encodeCall(IncomeVault.initialize, ( DEFAULT_ADMIN_ADDRESS, - tokenPayment, - ICMTATSnapshot(address(CMTAT_CONTRACT)), - IRuleEngine(ZERO_ADDRESS), - IAuthorizationEngine(ZERO_ADDRESS), - TIME_LIMIT_TO_WITHDRAW)), - opts - ); - debtVault = IncomeVault(proxy); - // Deploy DebtVault - /*debtVault = new DebtVault( - ZERO_ADDRESS - );*/ - /*debtVault.initialize( - DEFAULT_ADMIN_ADDRESS, - tokenPayment, - ICMTATSnapshot(address(CMTAT_CONTRACT)), - IRuleEngine(ZERO_ADDRESS), - IAuthorizationEngine(ZERO_ADDRESS) - );*/ - /** - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.mint(DEFAULT_ADMIN_ADDRESS, ADDRESS1_INITIAL_AMOUNT); - */ - vm.prank(TOKEN_PAYMENT_ADMIN); - tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, tokenBalance); - + _deployContracts(); } function testCannotClaimWithZeroDeposit() public { // Arrange - // Configure snapshot - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.scheduleSnapshot(defaultSnapshotTime); - - // Mint token for Address 1 - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); + _mintCMTATTokens(); // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + 50); + // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); - + incomeVault.setStatusClaim(defaultSnapshotTime, true); + // Claim deposit - vm.expectRevert( - abi.encodeWithSelector(IncomeVault_NoDividendToClaim.selector)); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_NoDividendToClaim.selector)); vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); // Check balance resUint256 = tokenPayment.balanceOf(ADDRESS1); - assertEq(resUint256, 0); - } - - function _performOnlyDeposit() internal { - // Allowance - vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount); - // Act - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(defaultSnapshotTime, defaultDepositAmount); - } - - function _mintCMTATTokens() internal { - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.scheduleSnapshot(defaultSnapshotTime); - - // Mint token for Address 1 - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); - } - - function _performDeposit() internal { - _performOnlyDeposit(); - // Configure snapshot - _mintCMTATTokens(); + assertEq(resUint256, 0); } function testHolderCannotClaimIfClaimNotOpened() public { // Arrange - // Configure snapshot - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.scheduleSnapshot(defaultSnapshotTime); - - // Mint token for Address 1 - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); + _mintCMTATTokens(); // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + 50); + // Claim deposit - vm.expectRevert( - abi.encodeWithSelector(IncomeVault_ClaimNotActivated.selector)); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_ClaimNotActivated.selector)); vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); } function testHolderCanClaimWithDepositAndOneHolder() public { @@ -166,20 +55,34 @@ contract IncomeVaultTest is Test, HelperContract { _performDeposit(); // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + 50); + // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); - + incomeVault.setStatusClaim(defaultSnapshotTime, true); + // Claim deposit vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); // Check balance resUint256 = tokenPayment.balanceOf(ADDRESS1); - assertEq(resUint256, defaultDepositAmount); + assertEq(resUint256, defaultDepositAmount); + } + + function testHolderCannotClaimTwice() public { + // Arrange + _performDeposit(); + vm.warp(defaultSnapshotTime + 50); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.prank(ADDRESS1); + incomeVault.claimDividend(defaultSnapshotTime); + + // Act & Assert + vm.expectRevert(abi.encodeWithSelector(IncomeVault_DividendAlreadyClaimed.selector)); + vm.prank(ADDRESS1); + incomeVault.claimDividend(defaultSnapshotTime); } function testHolderCannotClaimIfPaused() public { @@ -187,23 +90,25 @@ contract IncomeVaultTest is Test, HelperContract { _performDeposit(); // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + 50); + // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); + incomeVault.setStatusClaim(defaultSnapshotTime, true); // Contract pause vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.pause(); - + incomeVault.pause(); + // Act // Claim deposit vm.expectRevert( - abi.encodeWithSelector(Errors.CMTAT_InvalidTransfer.selector, address(debtVault), ADDRESS1, defaultDepositAmount)); + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); } function testHolderCannotClaimIfHolderAddressIsFrozen() public { @@ -211,37 +116,35 @@ contract IncomeVaultTest is Test, HelperContract { _performDeposit(); // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + 50); + // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); + incomeVault.setStatusClaim(defaultSnapshotTime, true); - // Contract pause + // Freeze the holder vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.freeze(ADDRESS1, "Blacklist"); - + incomeVault.setAddressFrozen(ADDRESS1, true, "Blacklist"); + // Act // Claim deposit vm.expectRevert( - abi.encodeWithSelector(Errors.CMTAT_InvalidTransfer.selector, address(debtVault), ADDRESS1, defaultDepositAmount)); + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); } function testHolderCanClaimWithDepositAndTwoHolders() public { - // Allowance - vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount); - // Act - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(defaultSnapshotTime, defaultDepositAmount); - // Configure snapshot + // Arrange + _performOnlyDeposit(); + // Configure snapshot vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.scheduleSnapshot(defaultSnapshotTime); - + snapshotEngine.scheduleSnapshot(defaultSnapshotTime); + // Mint token for Address 1 vm.prank(CMTAT_ADMIN); CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); @@ -250,30 +153,29 @@ contract IncomeVaultTest is Test, HelperContract { CMTAT_CONTRACT.mint(ADDRESS2, ADDRESS1_INITIAL_AMOUNT); // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + 50); + // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); - + incomeVault.setStatusClaim(defaultSnapshotTime, true); + // Claim deposit Address 1 vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); // Check balance resUint256 = tokenPayment.balanceOf(ADDRESS1); // Dividends are shared between the two token holders - assertEq(resUint256, defaultDepositAmount / 2); + assertEq(resUint256, defaultDepositAmount / 2); // Claim deposit Address 2 vm.prank(ADDRESS2); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); // Check balance resUint256 = tokenPayment.balanceOf(ADDRESS2); // Dividends are shared between the two token holders - assertEq(resUint256, defaultDepositAmount / 2); + assertEq(resUint256, defaultDepositAmount / 2); } function testCannotHolderClaimIfItIsTooLateToWithdraw() public { @@ -281,40 +183,60 @@ contract IncomeVaultTest is Test, HelperContract { _performDeposit(); // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + 50); + // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); + incomeVault.setStatusClaim(defaultSnapshotTime, true); // Timeout - timeout = defaultSnapshotTime + TIME_LIMIT_TO_WITHDRAW + 1 seconds; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + TIME_LIMIT_TO_WITHDRAW + 1 seconds); + // Act // Claim deposit - vm.expectRevert( - abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); } + function testCannotHolderClaimWithDepositAndOneHolderIfTooEarly() public { // Arrange _performDeposit(); // Timeout // No timeout - + // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); - + incomeVault.setStatusClaim(defaultSnapshotTime, true); + // Claim deposit - vm.expectRevert( - abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); } + /* ============ Snapshot source ============ */ + /** + * @dev the vault is wired to the snapshot engine through {ISnapshotSource}, not to the token + */ + function testSnapshotEngineIsTheConfiguredSource() public view { + assertEq(address(incomeVault.dividendSnapshotSource()), address(snapshotEngine)); + } + + function testCannotDeployWithSnapshotEngineAddressZero() public { + IncomeVault implementation = new IncomeVault(ZERO_ADDRESS); + bytes memory data = abi.encodeCall( + IncomeVault.initialize, + ( + DEFAULT_ADMIN_ADDRESS, + IERC20(address(tokenPayment)), + ISnapshotSource(ZERO_ADDRESS), + IRuleEngine(ZERO_ADDRESS), + TIME_LIMIT_TO_WITHDRAW + ) + ); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_SnapshotSourceWithAddressZeroNotAllowed.selector)); + new TransparentUpgradeableProxy(address(implementation), DEFAULT_ADMIN_ADDRESS, data); + } } diff --git a/test/IncomeVaultBatch.t.sol b/test/IncomeVaultBatch.t.sol index 442ba45..923545f 100644 --- a/test/IncomeVaultBatch.t.sol +++ b/test/IncomeVaultBatch.t.sol @@ -1,99 +1,17 @@ // SPDX-License-Identifier: MPL-2.0 -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; -import "forge-std/Test.sol"; import "./HelperContract.sol"; -import "CMTAT/interfaces/engine/IRuleEngine.sol"; -import "CMTAT/interfaces/engine/IAuthorizationEngine.sol"; -import {IncomeVault} from "../src/IncomeVault.sol"; -//import {Upgrades,} from "openzeppelin-foundry-upgrades/Upgrades.sol"; /** -* @title Test for IncomeVault -*/ -contract IncomeVaultBatchTest is Test, HelperContract { + * @title Test for the batch functions of IncomeVault + */ +contract IncomeVaultBatchTest is HelperContract { uint256 resUint256; - uint8 resUint8; - bool resBool; - bool resCallBool; - string resString; - uint8 CODE_NONEXISTENT = 255; - - // ADMIN balance payment - uint256 tokenBalance = 5000; // Arrange function setUp() public { - // Deploy CMTAT - CMTAT_CONTRACT = new CMTAT_STANDALONE( - ZERO_ADDRESS, - CMTAT_ADMIN, - IAuthorizationEngine(address(0)), - "CMTA Token", - "CMTAT", - DECIMALS, - "CMTAT_ISIN", - "https://cmta.ch", - IRuleEngine(address(0)), - "CMTAT_info", - FLAG - ); - - // Token payment - tokenPayment = new CMTAT_STANDALONE( - ZERO_ADDRESS, - TOKEN_PAYMENT_ADMIN, - IAuthorizationEngine(address(0)), - "CMTA Token", - "CMTAT", - DECIMALS, - "CMTAT_ISIN", - "https://cmta.ch", - IRuleEngine(address(0)), - "CMTAT_info", - FLAG - ); - Options memory opts; - opts.constructorData = abi.encode(ZERO_ADDRESS); - address proxy = Upgrades.deployTransparentProxy( - "IncomeVault.sol", - DEFAULT_ADMIN_ADDRESS, - abi.encodeCall(IncomeVault.initialize, ( DEFAULT_ADMIN_ADDRESS, - tokenPayment, - ICMTATSnapshot(address(CMTAT_CONTRACT)), - IRuleEngine(ZERO_ADDRESS), - IAuthorizationEngine(ZERO_ADDRESS), - TIME_LIMIT_TO_WITHDRAW)), - opts - ); - debtVault = IncomeVault(proxy); - vm.prank(TOKEN_PAYMENT_ADMIN); - tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, tokenBalance); - - } - - function _performOnlyDeposit() internal { - // Allowance - vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount); - // Act - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(defaultSnapshotTime, defaultDepositAmount); - } - - function _performDeposit() internal { - _performOnlyDeposit(); - // Configure snapshot - _mintCMTATTokens(); - } - - function _mintCMTATTokens() internal { - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.scheduleSnapshot(defaultSnapshotTime); - - // Mint token for Address 1 - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); + _deployContracts(); } function testHolderCanBatchClaimWithDepositAndOneHolder() public { @@ -106,33 +24,26 @@ contract IncomeVaultBatchTest is Test, HelperContract { uint256[] memory times = new uint256[](2); times[0] = defaultSnapshotTime; times[1] = newTime; - // Set the new approval - vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount * 2); - - // Deposit - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(newTime, defaultDepositAmount); + _performOnlyDeposit(newTime, defaultDepositAmount); // Timeout - uint256 timeout = newTime + 50; - vm.warp(timeout); - + vm.warp(newTime + 50); + // Open claim first deposit vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); + incomeVault.setStatusClaim(defaultSnapshotTime, true); // Open claim second deposit vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(newTime, true); - + incomeVault.setStatusClaim(newTime, true); + // Claim deposit vm.prank(ADDRESS1); - debtVault.claimDividendBatch(times); + incomeVault.claimDividendBatch(times); // Check balance resUint256 = tokenPayment.balanceOf(ADDRESS1); - assertEq(resUint256, defaultDepositAmount * 2); + assertEq(resUint256, defaultDepositAmount * 2); } function testHolderCanBatchClaimWithZeroDepositAndOneHolder() public { @@ -147,25 +58,25 @@ contract IncomeVaultBatchTest is Test, HelperContract { times[1] = newTime; // Timeout - uint256 timeout = newTime + 50; - vm.warp(timeout); - + vm.warp(newTime + 50); + // Open claim first deposit vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); + incomeVault.setStatusClaim(defaultSnapshotTime, true); // Open claim second deposit vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(newTime, true); - + incomeVault.setStatusClaim(newTime, true); + // Claim deposit vm.prank(ADDRESS1); - debtVault.claimDividendBatch(times); + incomeVault.claimDividendBatch(times); // Check balance resUint256 = tokenPayment.balanceOf(ADDRESS1); - assertEq(resUint256, 0); + assertEq(resUint256, 0); } + function testCannotHolderBatchClaimWithDepositAndOneHolderIfClaimNotOpen() public { // Arrange // First deposit @@ -176,27 +87,19 @@ contract IncomeVaultBatchTest is Test, HelperContract { uint256[] memory times = new uint256[](2); times[0] = defaultSnapshotTime; times[1] = newTime; - // Set the new approval - vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount * 2); - - // Deposit - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(newTime, defaultDepositAmount); + _performOnlyDeposit(newTime, defaultDepositAmount); // Timeout - uint256 timeout = newTime + 50; - vm.warp(timeout); - - // Open claim first deposit + vm.warp(newTime + 50); + + // Open claim first deposit only vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); - + incomeVault.setStatusClaim(defaultSnapshotTime, true); + // Claim deposit - vm.expectRevert( - abi.encodeWithSelector(IncomeVault_ClaimNotActivated.selector)); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_ClaimNotActivated.selector)); vm.prank(ADDRESS1); - debtVault.claimDividendBatch(times); + incomeVault.claimDividendBatch(times); } function testCannotHolderBatchClaimIfTooLate() public { @@ -209,36 +112,23 @@ contract IncomeVaultBatchTest is Test, HelperContract { uint256[] memory times = new uint256[](2); times[0] = defaultSnapshotTime; times[1] = newTime; - // Set the new approval - vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount * 2); - - // Deposit - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(newTime, defaultDepositAmount); + _performOnlyDeposit(newTime, defaultDepositAmount); // Timeout - uint256 timeout = newTime + 50; - vm.warp(timeout); + vm.warp(defaultSnapshotTime + TIME_LIMIT_TO_WITHDRAW + 1 seconds); - // Timeout - timeout = defaultSnapshotTime + TIME_LIMIT_TO_WITHDRAW + 1 seconds; - vm.warp(timeout); - - // Open claim first deposit vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); + incomeVault.setStatusClaim(defaultSnapshotTime, true); // Open claim second deposit vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(newTime, true); - + incomeVault.setStatusClaim(newTime, true); + // Claim deposit - vm.expectRevert( - abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); vm.prank(ADDRESS1); - debtVault.claimDividendBatch(times); + incomeVault.claimDividendBatch(times); } function testCannotHolderBatchClaimIfTooEarly() public { @@ -251,31 +141,22 @@ contract IncomeVaultBatchTest is Test, HelperContract { uint256[] memory times = new uint256[](2); times[0] = defaultSnapshotTime; times[1] = newTime; - // Set the new approval - vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount * 2); - - // Deposit - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(newTime, defaultDepositAmount); + _performOnlyDeposit(newTime, defaultDepositAmount); // Timeout vm.warp(defaultSnapshotTime); - - + // Open claim first deposit vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); + incomeVault.setStatusClaim(defaultSnapshotTime, true); // Open claim second deposit vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(newTime, true); - + incomeVault.setStatusClaim(newTime, true); + // Claim deposit - vm.expectRevert( - abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooEarlyToWithdraw.selector, block.timestamp)); vm.prank(ADDRESS1); - debtVault.claimDividendBatch(times); + incomeVault.claimDividendBatch(times); } - } diff --git a/test/IncomeVaultInterface.t.sol b/test/IncomeVaultInterface.t.sol new file mode 100644 index 0000000..c9c209c --- /dev/null +++ b/test/IncomeVaultInterface.t.sol @@ -0,0 +1,125 @@ +//SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {IIncomeVault} from "../src/interfaces/IIncomeVault.sol"; +import {IERC7741} from "../src/interfaces/IERC7741.sol"; +import {IERC7540Operator} from "../src/interfaces/IERC7540Operator.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {CMTATDividendHostMock} from "./mocks/CMTATDividendHostMock.sol"; +import {EmbeddedDividendHostMock} from "./mocks/EmbeddedDividendHostMock.sol"; + +/** + * @title The vault's own API is a stated interface, not a by-product — finding M-7 + * @dev + * {IIncomeVault} is inherited by {IncomeVaultInternal}, the common base of both payout paths, so the + * compiler already proves every deployable implements it. These tests cover what the compiler cannot: + * that the interface is reachable through a proxy, that it is advertised through ERC-165, and that a + * host embedding the distribution logic satisfies the same interface as the standalone vault. + */ +contract IncomeVaultInterfaceTest is HelperContract { + function setUp() public { + _deployContracts(); + } + + /** + * @notice An integrator can drive a real deployment through the interface alone + * @dev The point of the finding: no import of the concrete contract, and therefore none of CMTAT, + * the RuleEngine or the upgrade plumbing behind it. + */ + function testTheVaultIsUsableThroughTheInterfaceAlone() public { + IIncomeVault vault = IIncomeVault(address(incomeVault)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.setTimeLimitToWithdraw(2 days); + assertEq(vault.timeLimitToWithdraw(), 2 days); + + uint256 time = block.timestamp + 1 days; + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenPayment.approve(address(incomeVault), 100); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.deposit(time, 100); + + assertEq(vault.segregatedDividend(time), 100); + assertEq(vault.unclaimedDividend(time), 100); + assertEq(vault.paidDividend(time), 0); + assertEq(address(vault.ERC20TokenPayment()), address(tokenPayment)); + assertFalse(vault.segregatedClaim(time)); + assertFalse(vault.claimedDividend(ADDRESS1, time)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.setStatusClaim(time, true); + assertTrue(vault.segregatedClaim(time)); + assertEq(vault.openClaimCount(), 1); + } + + /** + * @notice The claim-window view returns the interface's own enum + * @dev `TIME_ERROR_CODE` is declared on {IIncomeVault} rather than on the implementation, so an + * integrator holding only the interface can interpret the answer. + */ + function testTheClaimWindowCodeIsPartOfTheStatedApi() public { + IIncomeVault vault = IIncomeVault(address(incomeVault)); + uint256 time = block.timestamp + 1 days; + + assertEq(uint256(vault.validateTimeCode(time)), uint256(IIncomeVault.TIME_ERROR_CODE.CLAIM_NOT_ACTIVATED)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenPayment.approve(address(incomeVault), 100); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.deposit(time, 100); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.setStatusClaim(time, true); + + assertEq(uint256(vault.validateTimeCode(time)), uint256(IIncomeVault.TIME_ERROR_CODE.TOO_EARLY_TO_WITHDRAW)); + vm.warp(time + 1); + assertEq(uint256(vault.validateTimeCode(time)), uint256(IIncomeVault.TIME_ERROR_CODE.OK)); + } + + /** + * @notice Both deployment variants advertise the interface through ERC-165 + */ + function testBothVariantsAdvertiseTheInterface() public { + _deployOwnableVault(); + bytes4 id = type(IIncomeVault).interfaceId; + + assertTrue(IERC165(address(incomeVault)).supportsInterface(id)); + assertTrue(IERC165(address(ownableVault)).supportsInterface(id)); + } + + /** + * @notice The id is distinct from the other interfaces the vault advertises + * @dev Guards against a copy-paste that advertises one id twice. + */ + function testTheInterfaceIdIsDistinct() public pure { + bytes4 id = type(IIncomeVault).interfaceId; + + assertTrue(id != type(IERC7741).interfaceId); + assertTrue(id != type(IERC7540Operator).interfaceId); + assertTrue(id != type(IERC165).interfaceId); + assertTrue(id != bytes4(0xffffffff)); + } + + /** + * @notice ERC-7540's operator id stays unadvertised + * @dev The vault is not an asynchronous vault and must not claim to be one. Restated here because + * adding {IIncomeVault} to `supportsInterface` is exactly the edit that invites adding this one too. + */ + function testTheAsyncVaultIdIsStillNotAdvertised() public view { + assertFalse(IERC165(address(incomeVault)).supportsInterface(type(IERC7540Operator).interfaceId)); + } + + /** + * @notice A host embedding the distribution logic satisfies the same interface as the vault + * @dev The casts compile only because both mocks inherit {IncomeVaultInternal}, hence + * {IIncomeVault}. This is the M-7 counterpart to the M-1/M-2 compile guards: the embedded and the + * standalone deployments present one API, not two. + */ + function testTheEmbeddedHostsPresentTheSameInterface() public { + IIncomeVault embedded = IIncomeVault(address(new EmbeddedDividendHostMock())); + IIncomeVault cmtatHost = IIncomeVault(address(new CMTATDividendHostMock())); + + assertTrue(address(embedded) != address(0)); + assertTrue(address(cmtatHost) != address(0)); + } +} diff --git a/test/IncomeVaultRestricted.t.sol b/test/IncomeVaultRestricted.t.sol index d6b0684..935ccc5 100644 --- a/test/IncomeVaultRestricted.t.sol +++ b/test/IncomeVaultRestricted.t.sol @@ -1,140 +1,62 @@ // SPDX-License-Identifier: MPL-2.0 -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; -import "forge-std/Test.sol"; import "./HelperContract.sol"; -import "CMTAT/interfaces/engine/IRuleEngine.sol"; -import "CMTAT/interfaces/engine/IAuthorizationEngine.sol"; -import {IncomeVault} from "../src/IncomeVault.sol"; -//import {Upgrades,} from "openzeppelin-foundry-upgrades/Upgrades.sol"; /** -* @title Test for DebtVault -*/ -contract IncomeVaultRestrictedTest is Test, HelperContract { + * @title Test for the restricted functions of IncomeVault + */ +contract IncomeVaultRestrictedTest is HelperContract { uint256 resUint256; - uint8 resUint8; bool resBool; - bool resCallBool; - string resString; - uint8 CODE_NONEXISTENT = 255; - - // ADMIN balance payment - uint256 tokenBalance = 5000; // Arrange function setUp() public { - // Deploy CMTAT - CMTAT_CONTRACT = new CMTAT_STANDALONE( - ZERO_ADDRESS, - CMTAT_ADMIN, - IAuthorizationEngine(address(0)), - "CMTA Token", - "CMTAT", - DECIMALS, - "CMTAT_ISIN", - "https://cmta.ch", - IRuleEngine(address(0)), - "CMTAT_info", - FLAG - ); - - // Token payment - tokenPayment = new CMTAT_STANDALONE( - ZERO_ADDRESS, - TOKEN_PAYMENT_ADMIN, - IAuthorizationEngine(address(0)), - "CMTA Token", - "CMTAT", - DECIMALS, - "CMTAT_ISIN", - "https://cmta.ch", - IRuleEngine(address(0)), - "CMTAT_info", - FLAG - ); - Options memory opts; - opts.constructorData = abi.encode(ZERO_ADDRESS); - address proxy = Upgrades.deployTransparentProxy( - "IncomeVault.sol", - DEFAULT_ADMIN_ADDRESS, - abi.encodeCall(IncomeVault.initialize, ( DEFAULT_ADMIN_ADDRESS, - tokenPayment, - ICMTATSnapshot(address(CMTAT_CONTRACT)), - IRuleEngine(ZERO_ADDRESS), - IAuthorizationEngine(ZERO_ADDRESS), - TIME_LIMIT_TO_WITHDRAW)), - opts - ); - debtVault = IncomeVault(proxy); - vm.prank(TOKEN_PAYMENT_ADMIN); - tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, tokenBalance); - + _deployContracts(); } function testDepositRoleCanPerformDeposit() public { uint256 time = 200; // Allowance vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount); + tokenPayment.approve(address(incomeVault), defaultDepositAmount); // Act vm.prank(DEFAULT_ADMIN_ADDRESS); //Event vm.expectEmit(true, true, false, true); - emit newDeposit( - time, - DEFAULT_ADMIN_ADDRESS, - defaultDepositAmount - ); - debtVault.deposit(time, defaultDepositAmount); + emit newDeposit(time, DEFAULT_ADMIN_ADDRESS, defaultDepositAmount); + incomeVault.deposit(time, defaultDepositAmount); // Assert - resUint256 = debtVault.segregatedDividend(time); - assertEq(resUint256, defaultDepositAmount); + resUint256 = incomeVault.segregatedDividend(time); + assertEq(resUint256, defaultDepositAmount); } - - function _performOnlyDeposit() internal { - // Allowance - vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount); - // Act + function testCannotDepositZeroAmount() public { + vm.expectRevert(abi.encodeWithSelector(IncomeVault_NoAmountSend.selector)); vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(defaultSnapshotTime, defaultDepositAmount); - } - - function _performDeposit() internal { - _performOnlyDeposit(); - // Configure snapshot - - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.scheduleSnapshot(defaultSnapshotTime); - - // Mint token for Address 1 - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); + incomeVault.deposit(200, 0); } function testAdminCanWithdrawAll() public { // Arrange - // Deposit uint256 snapshotTime1 = block.timestamp + 50; - uint256 snapshotTime2 = block.timestamp + 50; + uint256 snapshotTime2 = block.timestamp + 100; uint256 depositAmount1 = 2000; uint256 depositAmount2 = 3000; - uint256 ALLOWANCE_NEEDED = 2000 + 3000; + uint256 ALLOWANCE_NEEDED = depositAmount1 + depositAmount2; // Allowance vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), ALLOWANCE_NEEDED); + tokenPayment.approve(address(incomeVault), ALLOWANCE_NEEDED); // Deposit 1 vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(snapshotTime1, depositAmount1); + incomeVault.deposit(snapshotTime1, depositAmount1); // Deposit 2 vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(snapshotTime2, depositAmount2); - + incomeVault.deposit(snapshotTime2, depositAmount2); + // Withdraw vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.withdrawAll(ALLOWANCE_NEEDED, ADDRESS2); + incomeVault.withdrawAll(ALLOWANCE_NEEDED, ADDRESS2); // Assert assertEq(tokenPayment.balanceOf(ADDRESS2), ALLOWANCE_NEEDED); @@ -142,35 +64,62 @@ contract IncomeVaultRestrictedTest is Test, HelperContract { function testAdminCanWithdrawSpecificTime() public { // Arrange - // Deposit uint256 snapshotTime1 = block.timestamp + 50; - uint256 snapshotTime2 = block.timestamp + 50; + uint256 snapshotTime2 = block.timestamp + 100; uint256 depositAmount1 = 2000; uint256 depositAmount2 = 3000; - uint256 ALLOWANCE_NEEDED = 2000 + 3000; + uint256 ALLOWANCE_NEEDED = depositAmount1 + depositAmount2; // Allowance vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), ALLOWANCE_NEEDED); + tokenPayment.approve(address(incomeVault), ALLOWANCE_NEEDED); // Deposit 1 vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(snapshotTime1, depositAmount1); + incomeVault.deposit(snapshotTime1, depositAmount1); // Deposit 2 vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(snapshotTime2, depositAmount2); - + incomeVault.deposit(snapshotTime2, depositAmount2); + // Withdraw vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.withdraw(snapshotTime1, depositAmount1, ADDRESS2); + incomeVault.withdraw(snapshotTime1, depositAmount1, ADDRESS2); + + // Assert + assertEq(tokenPayment.balanceOf(ADDRESS2), depositAmount1); + assertEq(incomeVault.segregatedDividend(snapshotTime1), 0); + assertEq(incomeVault.segregatedDividend(snapshotTime2), depositAmount2); + } + + function testCannotWithdrawMoreThanDepositedForATime() public { + uint256 time = block.timestamp + 50; + _performOnlyDeposit(time, defaultDepositAmount); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_NotEnoughAmount.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.withdraw(time, defaultDepositAmount + 1, ADDRESS2); + } + + function testDistributeRoleCanDistributeDividend() public { + // Arrange + _performDeposit(); + vm.warp(defaultSnapshotTime + 50); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + + // Act + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); // Assert - assertEq(tokenPayment.balanceOf(ADDRESS2),depositAmount1); + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount); + assertEq(incomeVault.claimedDividend(ADDRESS1, defaultSnapshotTime), true); } function testCanAdminSetStatusClaim() public { uint256 time = 122; vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(time, true); - resBool = debtVault.segregatedClaim(time); + incomeVault.setStatusClaim(time, true); + resBool = incomeVault.segregatedClaim(time); assertEq(resBool, true); } @@ -178,12 +127,12 @@ contract IncomeVaultRestrictedTest is Test, HelperContract { // Arrange uint256 time = 122; vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(time, true); + incomeVault.setStatusClaim(time, true); // Act vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(time, false); + incomeVault.setStatusClaim(time, false); // Assert - resBool = debtVault.segregatedClaim(time); + resBool = incomeVault.segregatedClaim(time); assertEq(resBool, false); } @@ -191,53 +140,73 @@ contract IncomeVaultRestrictedTest is Test, HelperContract { // Act uint256 time = 122; vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setTimeLimitToWithdraw(time); + incomeVault.setTimeLimitToWithdraw(time); // Assert - resUint256 = debtVault.timeLimitToWithdraw(); - assertEq(resUint256,time); + resUint256 = incomeVault.timeLimitToWithdraw(); + assertEq(resUint256, time); } /****** Attacker */ function testCannotAttackerSetStatusClaim() public { vm.expectRevert( - abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_OPERATOR_ROLE)); + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_OPERATOR_ROLE) + ); vm.prank(ATTACKER); - debtVault.setStatusClaim(122, true); + incomeVault.setStatusClaim(122, true); } function testCannotAttackerSetTimeLimitToWithdraw() public { vm.expectRevert( - abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_OPERATOR_ROLE)); + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_OPERATOR_ROLE) + ); vm.prank(ATTACKER); - debtVault.setTimeLimitToWithdraw(122); + incomeVault.setTimeLimitToWithdraw(122); } function testCannotAttackerDistributeDividend() public { vm.expectRevert( - abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_DISTRIBUTE_ROLE)); + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_DISTRIBUTE_ROLE) + ); vm.prank(ATTACKER); address[] memory addresses = new address[](0); - debtVault.distributeDividend(addresses, 12); + incomeVault.distributeDividend(addresses, 12); } function testCannotAttackerWithdrawAll() public { vm.expectRevert( - abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_WITHDRAW_ROLE)); + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_WITHDRAW_ROLE) + ); vm.prank(ATTACKER); - debtVault.withdrawAll(12,ADDRESS2 ); + incomeVault.withdrawAll(12, ADDRESS2); } function testCannotAttackerWithdraw() public { vm.expectRevert( - abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_WITHDRAW_ROLE)); + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_WITHDRAW_ROLE) + ); vm.prank(ATTACKER); - debtVault.withdraw(12, 12, ADDRESS2 ); + incomeVault.withdraw(12, 12, ADDRESS2); } function testCannotAttackerPerformDeposit() public { vm.expectRevert( - abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_DEPOSIT_ROLE)); + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, INCOME_VAULT_DEPOSIT_ROLE) + ); + vm.prank(ATTACKER); + incomeVault.deposit(12, 12); + } + + function testCannotAttackerSetRuleEngine() public { + vm.expectRevert(abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, bytes32(0))); + vm.prank(ATTACKER); + incomeVault.setRuleEngine(IRuleEngine(ADDRESS3)); + } + + function testCannotAttackerPause() public { + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, incomeVault.PAUSER_ROLE()) + ); vm.prank(ATTACKER); - debtVault.deposit(12, 12); + incomeVault.pause(); } } diff --git a/test/IncomeVaultStorage.t.sol b/test/IncomeVaultStorage.t.sol new file mode 100644 index 0000000..9ed43d4 --- /dev/null +++ b/test/IncomeVaultStorage.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol"; + +/** + * @title Checks the ERC-7201 namespaced storage of the IncomeVault + * @dev + * The slot constant in {IncomeVaultInternal} is hardcoded, so it is re-derived here with + * {SlotDerivation-erc7201Slot} and compared against what the deployed proxy actually stores. + */ +contract IncomeVaultStorageTest is HelperContract { + using SlotDerivation for string; + + string constant NAMESPACE = "IncomeVault.storage.IncomeVaultInternal"; + /// @dev the value hardcoded in IncomeVaultInternal + bytes32 constant EXPECTED_SLOT = 0xe4f8b033bcfc537db031b0e68e3c1ab0f1de86cf03893d031b6590510b0c0c00; + /// @dev the snapshot source lives in its own namespace, so a host embedding the dividend logic + /// can answer the snapshot hooks itself and never allocate this slot at all + string constant SNAPSHOT_NAMESPACE = "IncomeVault.storage.SnapshotSource"; + /// @dev the value hardcoded in IncomeVaultSnapshotModule + bytes32 constant EXPECTED_SNAPSHOT_SLOT = 0x45a69a32b5b7efb4ae8ac48e2427653ef15920a29875121a072e6b49aaccac00; + /// @dev claim delegation keeps its own namespace, so a host can reason about the two separately + string constant OPERATOR_NAMESPACE = "IncomeVault.storage.Operator"; + /// @dev the value hardcoded in IncomeVaultOperatorModule + bytes32 constant EXPECTED_OPERATOR_SLOT = 0x70af7571496f61583375b861df45fee91dcc3edadeaff09b686f7920599a5500; + + function setUp() public { + _deployContracts(); + } + + /** + * @notice The hardcoded constant matches the ERC-7201 derivation of the namespace + */ + function testStorageLocationMatchesTheErc7201Derivation() public pure { + assertEq(NAMESPACE.erc7201Slot(), EXPECTED_SLOT); + assertEq(SNAPSHOT_NAMESPACE.erc7201Slot(), EXPECTED_SNAPSHOT_SLOT); + assertEq(OPERATOR_NAMESPACE.erc7201Slot(), EXPECTED_OPERATOR_SLOT); + } + + /** + * @notice ERC-7201 requires the last byte of the slot to be zeroed + */ + function testStorageLocationIsAligned() public pure { + assertEq(uint256(EXPECTED_SLOT) & 0xff, 0); + assertEq(uint256(EXPECTED_SNAPSHOT_SLOT) & 0xff, 0); + assertEq(uint256(EXPECTED_OPERATOR_SLOT) & 0xff, 0); + } + + /** + * @notice The two namespaces are disjoint, so neither module can corrupt the other + */ + function testTheNamespacesDoNotOverlap() public pure { + assertTrue(EXPECTED_SLOT != EXPECTED_SNAPSHOT_SLOT); + assertTrue(EXPECTED_SLOT != EXPECTED_OPERATOR_SLOT); + assertTrue(EXPECTED_SNAPSHOT_SLOT != EXPECTED_OPERATOR_SLOT); + } + + /** + * @notice Claim delegation is stored in the operator namespace, not the distribution one + * @dev Finding M-6. The mapping used to be the last field of `IncomeVaultInternalStorage`; a host + * embedding only the distribution would have carried it. Reading the derived mapping slot proves + * where it actually lives rather than trusting the declaration. + */ + function testOperatorAuthorisationsLiveInTheOperatorNamespace() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(ADDRESS2, true); + assertTrue(incomeVault.isOperator(ADDRESS1, ADDRESS2)); + + // mapping(controller => mapping(operator => bool)) at field 0 of the operator namespace + bytes32 outer = keccak256(abi.encode(ADDRESS1, uint256(OPERATOR_NAMESPACE.erc7201Slot()))); + bytes32 inner = keccak256(abi.encode(ADDRESS2, uint256(outer))); + assertEq(uint256(vm.load(address(incomeVault), inner)), 1); + + // and the same derivation against the distribution namespace holds nothing + bytes32 strayOuter = keccak256(abi.encode(ADDRESS1, uint256(NAMESPACE.erc7201Slot()) + 7)); + bytes32 strayInner = keccak256(abi.encode(ADDRESS2, uint256(strayOuter))); + assertEq(uint256(vm.load(address(incomeVault), strayInner)), 0); + } + + /** + * @notice The proxy really stores the state at the namespaced slot, in the declared field order + * @dev field 0 is `_ERC20TokenPayment`, field 4 is `_timeLimitToWithdraw` + */ + function testProxyStoresTheStateAtTheNamespacedSlot() public view { + bytes32 slot = NAMESPACE.erc7201Slot(); + + bytes32 rawPaymentToken = vm.load(address(incomeVault), slot); + assertEq(address(uint160(uint256(rawPaymentToken))), address(incomeVault.ERC20TokenPayment())); + + bytes32 rawTimeLimit = vm.load(address(incomeVault), bytes32(uint256(slot) + 4)); + assertEq(uint256(rawTimeLimit), TIME_LIMIT_TO_WITHDRAW); + assertEq(uint256(rawTimeLimit), incomeVault.timeLimitToWithdraw()); + } + + /** + * @notice The snapshot source is stored in its own namespace, not in the internal one + */ + function testTheSnapshotSourceLivesInItsOwnNamespace() public view { + bytes32 raw = vm.load(address(incomeVault), SNAPSHOT_NAMESPACE.erc7201Slot()); + assertEq(address(uint160(uint256(raw))), address(snapshotEngine)); + assertEq(address(uint160(uint256(raw))), address(incomeVault.dividendSnapshotSource())); + } + + /** + * @notice Slot 0 is free: no state is declared outside the ERC-7201 namespaces + */ + function testNoStateInTheSequentialSlots() public view { + for (uint256 i = 0; i < 8; ++i) { + assertEq(vm.load(address(incomeVault), bytes32(i)), bytes32(0)); + } + } + + /** + * @notice The public getters kept by the migration still expose the whole state + */ + function testGettersExposeTheNamespacedState() public { + _performDeposit(); + assertEq(incomeVault.segregatedDividend(defaultSnapshotTime), defaultDepositAmount); + assertEq(incomeVault.segregatedClaim(defaultSnapshotTime), false); + assertEq(incomeVault.claimedDividend(ADDRESS1, defaultSnapshotTime), false); + assertEq(address(incomeVault.ERC20TokenPayment()), address(tokenPayment)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + assertEq(incomeVault.segregatedClaim(defaultSnapshotTime), true); + } +} diff --git a/test/NoForwarderDeployment.t.sol b/test/NoForwarderDeployment.t.sol new file mode 100644 index 0000000..1efb1b2 --- /dev/null +++ b/test/NoForwarderDeployment.t.sol @@ -0,0 +1,64 @@ +//SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {NoForwarderVaultMock} from "./mocks/NoForwarderVaultMock.sol"; + +/** + * @title Gasless support is a deployment decision — finding M-8 + * @dev + * {IncomeVaultBase} no longer inherits `ERC2771Module`; {IncomeVaultBaseERC2771} adds it, and the two + * shipped deployments inherit that. These tests cover the half the compiler cannot: that a vault built + * on the plain base really has no forwarder, and that the shipped ones still do. + */ +contract NoForwarderDeploymentTest is HelperContract { + NoForwarderVaultMock vault; + + function setUp() public { + _deployContracts(); + vault = new NoForwarderVaultMock(); + vault.initialize( + IERC20(address(tokenPayment)), ISnapshotSource(address(snapshotEngine)), TIME_LIMIT_TO_WITHDRAW + ); + } + + /** + * @notice A vault on the plain base carries no ERC-2771 entry point at all + * @dev `isTrustedForwarder` is `ERC2771ContextUpgradeable`'s. Its absence from the ABI is the + * evidence that the meta-transaction machinery is genuinely gone, not merely disabled with a zero + * address — which is all that was possible before the split. + */ + function testThePlainBaseHasNoForwarderEntryPoint() public { + (bool found,) = address(vault).call(abi.encodeWithSignature("isTrustedForwarder(address)", ADDRESS1)); + assertFalse(found, "the plain base must not expose isTrustedForwarder"); + } + + /** + * @notice The shipped deployments keep gasless support + */ + function testTheShippedDeploymentsStillCarryTheForwarder() public { + (bool found, bytes memory data) = + address(incomeVault).call(abi.encodeWithSignature("isTrustedForwarder(address)", ZERO_ADDRESS)); + assertTrue(found, "IncomeVault must still expose isTrustedForwarder"); + assertTrue(abi.decode(data, (bool)), "the configured forwarder must be trusted"); + // the suite deploys with a zero forwarder, so gasless is inert but the machinery is present + } + + /** + * @notice The forwarder-free vault still distributes dividends normally + * @dev The split must remove the context, not the behaviour. + */ + function testTheForwarderFreeVaultStillPaysDividends() public { + uint256 time = block.timestamp + 1; + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenPayment.transfer(address(this), 100); + tokenPayment.approve(address(vault), 100); + + vault.deposit(time, 100); + assertEq(vault.segregatedDividend(time), 100); + + vault.setStatusClaim(time, true); + assertTrue(vault.segregatedClaim(time)); + assertEq(vault.openClaimCount(), 1); + } +} diff --git a/test/Operator.t.sol b/test/Operator.t.sol new file mode 100644 index 0000000..d5b731e --- /dev/null +++ b/test/Operator.t.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {IERC7540Operator} from "../src/interfaces/IERC7540Operator.sol"; + +/** + * @title Claim delegation — finding E-1 + * @dev Shape borrowed from ERC-7540: the holder authorises an operator, the operator triggers the + * claim, and the dividends still go to the holder. + */ +contract OperatorTest is HelperContract { + address constant CUSTODIAN = address(31); + address constant STRANGER = address(32); + + function setUp() public { + _deployContracts(); + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + } + + /* ============ granting ============ */ + function testOperatorStartsUnset() public view { + assertEq(incomeVault.isOperator(ADDRESS1, CUSTODIAN), false); + } + + function testSetOperatorGrantsAndEmits() public { + vm.expectEmit(true, true, false, true); + emit IERC7540Operator.OperatorSet(ADDRESS1, CUSTODIAN, true); + vm.prank(ADDRESS1); + bool ok = incomeVault.setOperator(CUSTODIAN, true); + + assertTrue(ok, "ERC-7540 setOperator returns true"); + assertEq(incomeVault.isOperator(ADDRESS1, CUSTODIAN), true); + } + + function testSetOperatorRevokes() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, true); + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, false); + assertEq(incomeVault.isOperator(ADDRESS1, CUSTODIAN), false); + } + + /** + * @notice Authorisation is per holder — granting for one does not grant for another + */ + function testAuthorisationIsPerHolder() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, true); + assertEq(incomeVault.isOperator(ADDRESS2, CUSTODIAN), false); + } + + /* ============ claiming on behalf ============ */ + /** + * @notice The operator triggers the claim; the **holder** receives the dividends + */ + function testOperatorClaimsAndTheHolderIsPaid() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, true); + + vm.prank(CUSTODIAN); + incomeVault.claimDividendFor(ADDRESS1, defaultSnapshotTime); + + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount, "holder is paid"); + assertEq(tokenPayment.balanceOf(CUSTODIAN), 0, "operator receives nothing"); + assertEq(incomeVault.claimedDividend(ADDRESS1, defaultSnapshotTime), true); + } + + function testHolderCanUseClaimForOnThemselves() public { + vm.prank(ADDRESS1); + incomeVault.claimDividendFor(ADDRESS1, defaultSnapshotTime); + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount); + } + + function testOperatorBatchClaim() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, true); + + uint256[] memory times = new uint256[](1); + times[0] = defaultSnapshotTime; + vm.prank(CUSTODIAN); + incomeVault.claimDividendBatchFor(ADDRESS1, times); + + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount); + assertEq(tokenPayment.balanceOf(CUSTODIAN), 0); + } + + /* ============ refusals ============ */ + function testStrangerCannotClaimForAHolder() public { + vm.expectRevert(abi.encodeWithSelector(IncomeVault_UnauthorizedOperator.selector, ADDRESS1, STRANGER)); + vm.prank(STRANGER); + incomeVault.claimDividendFor(ADDRESS1, defaultSnapshotTime); + + assertEq(tokenPayment.balanceOf(ADDRESS1), 0); + } + + function testRevokedOperatorCannotClaim() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, true); + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, false); + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_UnauthorizedOperator.selector, ADDRESS1, CUSTODIAN)); + vm.prank(CUSTODIAN); + incomeVault.claimDividendFor(ADDRESS1, defaultSnapshotTime); + } + + /** + * @notice An operator for one holder cannot claim for another + */ + function testOperatorCannotCrossToAnotherHolder() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, true); + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_UnauthorizedOperator.selector, ADDRESS2, CUSTODIAN)); + vm.prank(CUSTODIAN); + incomeVault.claimDividendFor(ADDRESS2, defaultSnapshotTime); + } + + /* ============ every other rule still applies ============ */ + function testOperatorCannotClaimTwice() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, true); + vm.prank(CUSTODIAN); + incomeVault.claimDividendFor(ADDRESS1, defaultSnapshotTime); + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_DividendAlreadyClaimed.selector)); + vm.prank(CUSTODIAN); + incomeVault.claimDividendFor(ADDRESS1, defaultSnapshotTime); + } + + function testOperatorCannotClaimForAFrozenHolder() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, true); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setAddressFrozen(ADDRESS1, true, ""); + + vm.expectRevert( + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); + vm.prank(CUSTODIAN); + incomeVault.claimDividendFor(ADDRESS1, defaultSnapshotTime); + } + + function testOperatorCannotClaimOutsideTheWindow() public { + vm.prank(ADDRESS1); + incomeVault.setOperator(CUSTODIAN, true); + vm.warp(defaultSnapshotTime + TIME_LIMIT_TO_WITHDRAW + 1); + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_TooLateToWithdraw.selector, block.timestamp)); + vm.prank(CUSTODIAN); + incomeVault.claimDividendFor(ADDRESS1, defaultSnapshotTime); + } + + /* ============ conformance with the ERC-7540 operator subset ============ */ + /** + * @notice The interface id matches the value ERC-7540 assigns to its operator methods + * @dev + * This is what pins the signatures to the standard. ERC-7540 states that `0xe3bc4e65` represents + * "the operator methods that all ERC-7540 Vaults implement"; `IERC7540Operator` inherits nothing, + * so `type(...).interfaceId` is exactly `setOperator(address,bool) ^ isOperator(address,address)`. + * Change either signature and this fails — which is the point, because a custodian written against + * ERC-7540 would then silently stop working. + */ + function testOperatorInterfaceIdMatchesTheStandard() public pure { + assertEq(type(IERC7540Operator).interfaceId, bytes4(0xe3bc4e65)); + } + + /** + * @notice The vault really is callable through the standard interface type + */ + function testCallableThroughTheStandardInterface() public { + IERC7540Operator asStandard = IERC7540Operator(address(incomeVault)); + + vm.prank(ADDRESS1); + assertTrue(asStandard.setOperator(CUSTODIAN, true), "setOperator MUST return true"); + assertEq(asStandard.isOperator(ADDRESS1, CUSTODIAN), true); + + vm.prank(CUSTODIAN); + incomeVault.claimDividendFor(ADDRESS1, defaultSnapshotTime); + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount); + } + + /** + * @notice The vault deliberately does NOT advertise the id through ERC-165 + * @dev Sharing the operator methods does not make it an asynchronous ERC-7540 vault; a caller + * discovering `0xe3bc4e65` would reasonably expect the request lifecycle and ERC-7575's `share()`, + * neither of which exists here. Asserted so the under-claim is a decision, not an oversight. + */ + function testDoesNotClaimToBeAnErc7540Vault() public view { + assertEq(incomeVault.supportsInterface(bytes4(0xe3bc4e65)), false); + assertEq(incomeVault.supportsInterface(bytes4(0xce3bbe50)), false, "not an async deposit vault"); + assertEq(incomeVault.supportsInterface(bytes4(0x620ee8e4)), false, "not an async redeem vault"); + assertEq(incomeVault.supportsInterface(bytes4(0x2f0a18c5)), false, "not an ERC-7575 vault"); + } +} diff --git a/test/OperatorAuthorization.t.sol b/test/OperatorAuthorization.t.sol new file mode 100644 index 0000000..2f353f4 --- /dev/null +++ b/test/OperatorAuthorization.t.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {IERC7741} from "../src/interfaces/IERC7741.sol"; +import {IERC7540Operator} from "../src/interfaces/IERC7540Operator.sol"; +import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol"; + +/** + * @title ERC-7741 signed operator authorisation + * @dev The holder signs; anyone may submit. Nothing here uses `vm.prank` on the holder for the + * authorisation itself — that is the entire point of the standard. + */ +contract OperatorAuthorizationTest is HelperContract { + using SlotDerivation for string; + + uint256 holderKey; + address holder; + address constant CUSTODIAN = address(31); + address constant RELAYER = address(33); + + bytes32 constant TYPEHASH = keccak256( + "AuthorizeOperator(address controller,address operator,bool approved,bytes32 nonce,uint256 deadline)" + ); + + function setUp() public { + holderKey = 0xA11CE; + holder = vm.addr(holderKey); + _deployContracts(); + } + + function _sign(uint256 key, address controller, address operator, bool approved, bytes32 nonce, uint256 deadline) + internal + view + returns (bytes memory) + { + bytes32 structHash = keccak256(abi.encode(TYPEHASH, controller, operator, approved, nonce, deadline)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", incomeVault.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, digest); + return abi.encodePacked(r, s, v); + } + + /* ============ the standard's identifier ============ */ + /** + * @notice The interface id matches the value ERC-7741 assigns + * @dev Pins all four signatures: change any of them and this fails. + */ + function testInterfaceIdMatchesTheStandard() public pure { + assertEq(type(IERC7741).interfaceId, bytes4(0xa9e50872)); + } + + /** + * @notice ERC-7741 requires the id to be advertised — both variants do + */ + function testBothVariantsAdvertiseErc7741() public { + assertTrue(incomeVault.supportsInterface(bytes4(0xa9e50872)), "role-based variant"); + _deployOwnableVault(); + assertTrue(ownableVault.supportsInterface(bytes4(0xa9e50872)), "single-owner variant"); + } + + /** + * @notice The module's ERC-7201 slot is what its comment claims + */ + function testModuleStorageSlotDerivation() public pure { + assertEq( + string("IncomeVault.storage.ERC7741Module").erc7201Slot(), + 0xb93ff011b98f03386917a7b9b9106f5d9f85ba058e0b4e9b3aad1f6474a96800 + ); + } + + /* ============ the happy path ============ */ + /** + * @notice A relayer submits the holder's signature; the holder never transacts + */ + function testRelayerSubmitsTheHoldersAuthorization() public { + bytes32 nonce = keccak256("nonce-1"); + uint256 deadline = block.timestamp + 1 hours; + bytes memory sig = _sign(holderKey, holder, CUSTODIAN, true, nonce, deadline); + + vm.expectEmit(true, true, false, true); + emit IERC7540Operator.OperatorSet(holder, CUSTODIAN, true); + vm.prank(RELAYER); + bool ok = incomeVault.authorizeOperator(holder, CUSTODIAN, true, nonce, deadline, sig); + + assertTrue(ok, "MUST return true"); + assertEq(incomeVault.isOperator(holder, CUSTODIAN), true); + assertEq(incomeVault.authorizations(holder, nonce), true, "the nonce is spent"); + } + + /** + * @notice And the authorised operator can then actually claim for the holder + */ + function testAuthorizedOperatorCanClaim() public { + vm.prank(CMTAT_ADMIN); + snapshotEngine.scheduleSnapshot(defaultSnapshotTime); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(holder, ADDRESS1_INITIAL_AMOUNT); + _performOnlyDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + bytes32 nonce = keccak256("nonce-claim"); + uint256 deadline = block.timestamp + 1 hours; + vm.prank(RELAYER); + incomeVault.authorizeOperator( + holder, CUSTODIAN, true, nonce, deadline, _sign(holderKey, holder, CUSTODIAN, true, nonce, deadline) + ); + + vm.prank(CUSTODIAN); + incomeVault.claimDividendFor(holder, defaultSnapshotTime); + assertEq(tokenPayment.balanceOf(holder), defaultDepositAmount); + } + + function testSignedRevocation() public { + bytes32 n1 = keccak256("grant"); + bytes32 n2 = keccak256("revoke"); + uint256 deadline = block.timestamp + 1 hours; + vm.prank(RELAYER); + incomeVault.authorizeOperator( + holder, CUSTODIAN, true, n1, deadline, _sign(holderKey, holder, CUSTODIAN, true, n1, deadline) + ); + assertEq(incomeVault.isOperator(holder, CUSTODIAN), true); + + vm.prank(RELAYER); + incomeVault.authorizeOperator( + holder, CUSTODIAN, false, n2, deadline, _sign(holderKey, holder, CUSTODIAN, false, n2, deadline) + ); + assertEq(incomeVault.isOperator(holder, CUSTODIAN), false); + } + + /* ============ refusals ============ */ + function testCannotReplayANonce() public { + bytes32 nonce = keccak256("replay"); + uint256 deadline = block.timestamp + 1 hours; + bytes memory sig = _sign(holderKey, holder, CUSTODIAN, true, nonce, deadline); + + vm.prank(RELAYER); + incomeVault.authorizeOperator(holder, CUSTODIAN, true, nonce, deadline, sig); + + vm.expectRevert(abi.encodeWithSignature("IncomeVault_AuthorizationUsed(address,bytes32)", holder, nonce)); + vm.prank(RELAYER); + incomeVault.authorizeOperator(holder, CUSTODIAN, true, nonce, deadline, sig); + } + + function testCannotUseAnExpiredSignature() public { + bytes32 nonce = keccak256("expired"); + uint256 deadline = block.timestamp + 1 hours; + bytes memory sig = _sign(holderKey, holder, CUSTODIAN, true, nonce, deadline); + + vm.warp(deadline + 1); + vm.expectRevert(abi.encodeWithSignature("IncomeVault_AuthorizationExpired(uint256)", deadline)); + vm.prank(RELAYER); + incomeVault.authorizeOperator(holder, CUSTODIAN, true, nonce, deadline, sig); + } + + /** + * @notice A signature from anyone but the controller is refused + */ + function testCannotForgeAnAuthorization() public { + uint256 attackerKey = 0xBAD; + bytes32 nonce = keccak256("forged"); + uint256 deadline = block.timestamp + 1 hours; + bytes memory sig = _sign(attackerKey, holder, CUSTODIAN, true, nonce, deadline); + + vm.expectRevert(abi.encodeWithSignature("IncomeVault_InvalidAuthorization(address)", holder)); + vm.prank(RELAYER); + incomeVault.authorizeOperator(holder, CUSTODIAN, true, nonce, deadline, sig); + assertEq(incomeVault.isOperator(holder, CUSTODIAN), false); + } + + /** + * @notice Tampering with any signed field invalidates the signature + */ + function testCannotTamperWithTheSignedTerms() public { + bytes32 nonce = keccak256("tamper"); + uint256 deadline = block.timestamp + 1 hours; + // signed for CUSTODIAN, submitted for the relayer instead + bytes memory sig = _sign(holderKey, holder, CUSTODIAN, true, nonce, deadline); + + vm.expectRevert(abi.encodeWithSignature("IncomeVault_InvalidAuthorization(address)", holder)); + vm.prank(RELAYER); + incomeVault.authorizeOperator(holder, RELAYER, true, nonce, deadline, sig); + } + + function testCannotAuthorizeForTheZeroController() public { + bytes32 nonce = keccak256("zero"); + uint256 deadline = block.timestamp + 1 hours; + vm.expectRevert(abi.encodeWithSignature("IncomeVault_ControllerWithAddressZeroNotAllowed()")); + vm.prank(RELAYER); + incomeVault.authorizeOperator(ZERO_ADDRESS, CUSTODIAN, true, nonce, deadline, hex"00"); + } + + /* ============ invalidateNonce ============ */ + /** + * @notice A holder can burn a nonce so a leaked signature can never be used + */ + function testInvalidateNonceBurnsAPendingSignature() public { + bytes32 nonce = keccak256("leaked"); + uint256 deadline = block.timestamp + 365 days; + bytes memory sig = _sign(holderKey, holder, CUSTODIAN, true, nonce, deadline); + + vm.prank(holder); + incomeVault.invalidateNonce(nonce); + assertEq(incomeVault.authorizations(holder, nonce), true); + + vm.expectRevert(abi.encodeWithSignature("IncomeVault_AuthorizationUsed(address,bytes32)", holder, nonce)); + vm.prank(RELAYER); + incomeVault.authorizeOperator(holder, CUSTODIAN, true, nonce, deadline, sig); + } + + /** + * @notice Invalidating is per holder — it cannot burn someone else's nonce + */ + function testInvalidateNonceIsPerHolder() public { + bytes32 nonce = keccak256("mine"); + vm.prank(RELAYER); + incomeVault.invalidateNonce(nonce); + assertEq(incomeVault.authorizations(holder, nonce), false, "the holder's nonce is untouched"); + } + + /** + * @notice Nonces are unordered: a later one can be used before an earlier one + */ + function testNoncesAreUnordered() public { + uint256 deadline = block.timestamp + 1 hours; + bytes32 a = keccak256("a"); + bytes32 b = keccak256("b"); + bytes memory sigA = _sign(holderKey, holder, CUSTODIAN, true, a, deadline); + bytes memory sigB = _sign(holderKey, holder, RELAYER, true, b, deadline); + + vm.prank(RELAYER); + incomeVault.authorizeOperator(holder, RELAYER, true, b, deadline, sigB); + vm.prank(RELAYER); + incomeVault.authorizeOperator(holder, CUSTODIAN, true, a, deadline, sigA); + + assertEq(incomeVault.isOperator(holder, CUSTODIAN), true); + assertEq(incomeVault.isOperator(holder, RELAYER), true); + } +} diff --git a/test/OverrideMock.t.sol b/test/OverrideMock.t.sol new file mode 100644 index 0000000..c0871c6 --- /dev/null +++ b/test/OverrideMock.t.sol @@ -0,0 +1,95 @@ +//SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {IncomeVaultOverrideMock} from "./mocks/IncomeVaultOverrideMock.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/** + * @title The `virtual` convention is reached, not merely compiled — finding E-1 + * @dev + * `IncomeVaultOverrideMock` guards the convention at compile time: dropping `virtual` from any function + * it overrides fails with `Error (4334)`. That alone does not prove the override is *called* — a + * silently shadowed one compiles and never runs. + * + * These tests drive a real deposit-and-claim through the mock and assert the counters moved, which is + * what distinguishes an override that is reached from one that merely exists. + * + * The proxy is deployed directly rather than through `Upgrades`, because the plugin's upgrade-safety + * validation is for production contracts and this is a test double. + */ +contract OverrideMockTest is HelperContract { + IncomeVaultOverrideMock vault; + + function setUp() public { + _deployContracts(); + _mintCMTATTokens(); // schedules the snapshot at defaultSnapshotTime, then mints to ADDRESS1 + + IncomeVaultOverrideMock impl = new IncomeVaultOverrideMock(ZERO_ADDRESS); + vault = IncomeVaultOverrideMock( + address( + new TransparentUpgradeableProxy( + address(impl), + DEFAULT_ADMIN_ADDRESS, + abi.encodeCall( + IncomeVault.initialize, + ( + DEFAULT_ADMIN_ADDRESS, + IERC20(address(tokenPayment)), + ISnapshotSource(address(snapshotEngine)), + IRuleEngine(ZERO_ADDRESS), + TIME_LIMIT_TO_WITHDRAW + ) + ) + ) + ) + ); + } + + /** + * @notice A claim reaches both the public and the internal override + */ + function testAClaimReachesTheOverriddenRoutines() public { + uint256 time = defaultSnapshotTime; + + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenPayment.approve(address(vault), 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.deposit(time, 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.setStatusClaim(time, true); + vm.warp(time + 10); + + assertEq(vault.claimCount(), 0); + assertEq(vault.transferCount(), 0); + + vm.prank(ADDRESS1); + vault.claimDividend(time); + + assertEq(vault.claimCount(), 1, "public claimDividend override was not reached"); + assertEq(vault.transferCount(), 1, "internal _transferDividend override was not reached"); + } + + /** + * @notice The overridden payout still pays the right amount + * @dev An override that is reached but breaks the behaviour would be worse than one that is skipped. + */ + function testTheOverriddenPayoutStillPaysCorrectly() public { + uint256 time = defaultSnapshotTime; + + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenPayment.approve(address(vault), 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.deposit(time, 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.setStatusClaim(time, true); + vm.warp(time + 10); + + uint256 before = tokenPayment.balanceOf(ADDRESS1); + vm.prank(ADDRESS1); + vault.claimDividend(time); + + assertGt(tokenPayment.balanceOf(ADDRESS1), before, "the holder was not paid"); + assertEq(vault.paidDividend(time), tokenPayment.balanceOf(ADDRESS1) - before); + } +} diff --git a/test/RuleEngineIntegration.t.sol b/test/RuleEngineIntegration.t.sol index d69f54f..f1a92aa 100644 --- a/test/RuleEngineIntegration.t.sol +++ b/test/RuleEngineIntegration.t.sol @@ -1,234 +1,161 @@ // SPDX-License-Identifier: MPL-2.0 -pragma solidity ^0.8.20; - +pragma solidity ^0.8.24; import "./HelperContract.sol"; -import "RuleEngine/rules/validation/abstract/RuleAddressList/RuleWhitelistInvariantStorage.sol"; +import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; +import {RuleWhitelistMock} from "RuleEngine/mocks/rules/validation/RuleWhitelistMock.sol"; +import { + RuleWhitelistInvariantStorage +} from "RuleEngine/mocks/rules/validation/abstract/RuleAddressList/invariantStorage/RuleWhitelistInvariantStorage.sol"; +import {IRule} from "RuleEngine/interfaces/IRule.sol"; /** -* @title Integration test with the CMTAT -*/ -contract RuleEngineIntegration is RuleWhitelistInvariantStorage, Test, HelperContract { - // Defined in CMTAT.sol + * @title Integration test between the IncomeVault and the RuleEngine + */ +contract RuleEngineIntegration is HelperContract, RuleWhitelistInvariantStorage { + // Defined by the RuleEngine uint8 constant TRANSFER_OK = 0; - string constant TEXT_TRANSFER_OK = "No restriction"; + // Contracts RuleEngine ruleEngineMock; - RuleWhitelist ruleWhitelist; + RuleWhitelistMock ruleWhitelist; // Other variable uint256 resUint256; bool resBool; - uint256 ADDRESS1_BALANCE_INIT = 31; - uint256 ADDRESS2_BALANCE_INIT = 32; - uint256 ADDRESS3_BALANCE_INIT = 33; - - uint256 tokenBalance = 5000; // Arrange function setUp() public { - vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleWhitelist = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS); - // global arrange - uint8 decimals = 0; - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT = new CMTAT_STANDALONE( - ZERO_ADDRESS, - CMTAT_ADMIN, - IAuthorizationEngine(address(0)), - "CMTA Token", - "CMTAT", - decimals, - "CMTAT_ISIN", - "https://cmta.ch", - IRuleEngine(address(0)), - "CMTAT_info", - FLAG - ); + ruleWhitelist = new RuleWhitelistMock(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS); - // Token payment - tokenPayment = new CMTAT_STANDALONE( - ZERO_ADDRESS, - TOKEN_PAYMENT_ADMIN, - IAuthorizationEngine(address(0)), - "CMTA Token", - "CMTAT", - DECIMALS, - "CMTAT_ISIN", - "https://cmta.ch", - IRuleEngine(address(0)), - "CMTAT_info", - FLAG - ); - // IncomeVault deployment - Options memory opts; - opts.constructorData = abi.encode(ZERO_ADDRESS); - address proxy = Upgrades.deployTransparentProxy( - "IncomeVault.sol", - DEFAULT_ADMIN_ADDRESS, - abi.encodeCall(IncomeVault.initialize, ( DEFAULT_ADMIN_ADDRESS, - tokenPayment, - ICMTATSnapshot(address(CMTAT_CONTRACT)), - IRuleEngine(ZERO_ADDRESS), - IAuthorizationEngine(ZERO_ADDRESS), - TIME_LIMIT_TO_WITHDRAW)), - opts - ); - debtVault = IncomeVault(proxy); + _deployContracts(); - // specific arrange + // The vault is not a token bound to the engine: it only uses the read path, + // so no token has to be bound at deployment. + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, address(incomeVault)); vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, address(proxy)); - vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleEngineMock.addRuleValidation(ruleWhitelist); + ruleEngineMock.addRule(IRule(address(ruleWhitelist))); // We set the Rule Engine vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setRuleEngine(ruleEngineMock); - /** - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.mint(DEFAULT_ADMIN_ADDRESS, ADDRESS1_INITIAL_AMOUNT); - */ - vm.prank(TOKEN_PAYMENT_ADMIN); - tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, tokenBalance); - } - - function _performOnlyDeposit() internal { - // Allowance - vm.prank(DEFAULT_ADMIN_ADDRESS); - tokenPayment.approve(address(debtVault), defaultDepositAmount); - // Act - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.deposit(defaultSnapshotTime, defaultDepositAmount); + incomeVault.setRuleEngine(ruleEngineMock); } - function _performDeposit() internal { - _performOnlyDeposit(); - // Configure snapshot - - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.scheduleSnapshot(defaultSnapshotTime); - - // Mint token for Address 1 - vm.prank(CMTAT_ADMIN); - CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); - } - - /******* Transfer *******/ + /******* Claim *******/ function testCannotClaimWithoutAddressWhitelisted() public { // Arrange _performDeposit(); - // Act // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); - - // Contract pause - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.pause(); + incomeVault.setStatusClaim(defaultSnapshotTime, true); // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + 50); + // Act - // Claim deposit vm.expectRevert( - abi.encodeWithSelector(Errors.CMTAT_InvalidTransfer.selector, address(debtVault), ADDRESS1, defaultDepositAmount)); + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); } - function testCannotTransferWithoutFromAddressWhitelisted() public { - // Arrange + function testCannotClaimWithoutToAddressWhitelisted() public { + // Arrange: only the vault (the sender) is whitelisted vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleWhitelist.addAddressToTheList(address(debtVault)); + ruleWhitelist.addAddressToTheList(address(incomeVault)); - // Arrange _performDeposit(); - // Act - // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); + incomeVault.setStatusClaim(defaultSnapshotTime, true); - // Contract pause - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.pause(); + vm.warp(defaultSnapshotTime + 50); - // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - // Act - // Claim deposit vm.expectRevert( - abi.encodeWithSelector(Errors.CMTAT_InvalidTransfer.selector, address(debtVault), ADDRESS1, defaultDepositAmount)); + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); } - function testCannotTransferWithoutToAddressWhitelisted() public { - // Arrange + function testCannotClaimWithoutFromAddressWhitelisted() public { + // Arrange: only the holder (the recipient) is whitelisted vm.prank(DEFAULT_ADMIN_ADDRESS); ruleWhitelist.addAddressToTheList(ADDRESS1); - // Arrange _performDeposit(); - // Act - // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); - - // Contract pause - vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.pause(); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); - // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - // Act - // Claim deposit vm.expectRevert( - abi.encodeWithSelector(Errors.CMTAT_InvalidTransfer.selector, address(debtVault), ADDRESS1, defaultDepositAmount)); + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); } - function testCanMakeATransfer() public { + function testCanClaimWithBothAddressesWhitelisted() public { // Arrange address[] memory whitelist = new address[](2); whitelist[0] = ADDRESS1; - whitelist[1] = address(debtVault); + whitelist[1] = address(incomeVault); vm.prank(DEFAULT_ADMIN_ADDRESS); - (bool success, ) = address(ruleWhitelist).call( - abi.encodeWithSignature( - "addAddressesToTheList(address[])", - whitelist - ) - ); - require(success); - - // Act - // Arrange + ruleWhitelist.addAddressesToTheList(whitelist); + _performDeposit(); // Timeout - uint256 timeout = defaultSnapshotTime + 50; - vm.warp(timeout); - + vm.warp(defaultSnapshotTime + 50); + // Open claim vm.prank(DEFAULT_ADMIN_ADDRESS); - debtVault.setStatusClaim(defaultSnapshotTime, true); - + incomeVault.setStatusClaim(defaultSnapshotTime, true); + // Claim deposit vm.prank(ADDRESS1); - debtVault.claimDividend(defaultSnapshotTime); + incomeVault.claimDividend(defaultSnapshotTime); // Check balance resUint256 = tokenPayment.balanceOf(ADDRESS1); - assertEq(resUint256, defaultDepositAmount); + assertEq(resUint256, defaultDepositAmount); + } + + /******* canTransfer *******/ + function testCanTransferIsFalseWhenNotWhitelisted() public view { + assertEq(incomeVault.canTransfer(address(incomeVault), ADDRESS1, 11), false); + } + + function testCanTransferIsTrueWhenWhitelisted() public { + address[] memory whitelist = new address[](2); + whitelist[0] = ADDRESS1; + whitelist[1] = address(incomeVault); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleWhitelist.addAddressesToTheList(whitelist); + + assertEq(incomeVault.canTransfer(address(incomeVault), ADDRESS1, 11), true); + } + + function testCanTransferIsFalseWhenPausedEvenIfWhitelisted() public { + address[] memory whitelist = new address[](2); + whitelist[0] = ADDRESS1; + whitelist[1] = address(incomeVault); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleWhitelist.addAddressesToTheList(whitelist); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.pause(); + + assertEq(incomeVault.canTransfer(address(incomeVault), ADDRESS1, 11), false); } /******* detectTransferRestriction & messageForTransferRestriction *******/ @@ -238,16 +165,10 @@ contract RuleEngineIntegration is RuleWhitelistInvariantStorage, Test, HelperCon resBool = ruleWhitelist.addressIsListed(ADDRESS2); // Assert assertEq(resBool, true); - uint8 res1 = debtVault.detectTransferRestriction( - ADDRESS1, - ADDRESS2, - 11 - ); + uint8 res1 = incomeVault.detectTransferRestriction(ADDRESS1, ADDRESS2, 11); // Assert assertEq(res1, CODE_ADDRESS_FROM_NOT_WHITELISTED); - string memory message1 = debtVault.messageForTransferRestriction( - res1 - ); + string memory message1 = incomeVault.messageForTransferRestriction(res1); // Assert assertEq(message1, TEXT_ADDRESS_FROM_NOT_WHITELISTED); } @@ -261,35 +182,23 @@ contract RuleEngineIntegration is RuleWhitelistInvariantStorage, Test, HelperCon resBool = ruleWhitelist.addressIsListed(ADDRESS1); assertEq(resBool, true); // Act - uint8 res1 = debtVault.detectTransferRestriction( - ADDRESS1, - ADDRESS2, - 11 - ); + uint8 res1 = incomeVault.detectTransferRestriction(ADDRESS1, ADDRESS2, 11); // Assert assertEq(res1, CODE_ADDRESS_TO_NOT_WHITELISTED); // Act - string memory message1 = debtVault.messageForTransferRestriction( - res1 - ); + string memory message1 = incomeVault.messageForTransferRestriction(res1); // Assert assertEq(message1, TEXT_ADDRESS_TO_NOT_WHITELISTED); } function testDetectAndMessageWithFromAndToNotWhitelisted() public view { // Act - uint8 res1 = debtVault.detectTransferRestriction( - ADDRESS1, - ADDRESS2, - 11 - ); + uint8 res1 = incomeVault.detectTransferRestriction(ADDRESS1, ADDRESS2, 11); // Assert assertEq(res1, CODE_ADDRESS_FROM_NOT_WHITELISTED); // Act - string memory message1 = debtVault.messageForTransferRestriction( - res1 - ); + string memory message1 = incomeVault.messageForTransferRestriction(res1); // Assert assertEq(message1, TEXT_ADDRESS_FROM_NOT_WHITELISTED); @@ -302,26 +211,82 @@ contract RuleEngineIntegration is RuleWhitelistInvariantStorage, Test, HelperCon whitelist[0] = ADDRESS1; whitelist[1] = ADDRESS2; vm.prank(DEFAULT_ADMIN_ADDRESS); - (bool success, ) = address(ruleWhitelist).call( - abi.encodeWithSignature( - "addAddressesToTheList(address[])", - whitelist - ) - ); - require(success); + ruleWhitelist.addAddressesToTheList(whitelist); // Act - uint8 res1 = debtVault.detectTransferRestriction( - ADDRESS1, - ADDRESS2, - 11 - ); + uint8 res1 = incomeVault.detectTransferRestriction(ADDRESS1, ADDRESS2, 11); // Assert assertEq(res1, TRANSFER_OK); // Act - string memory message1 = debtVault.messageForTransferRestriction( - res1 - ); + string memory message1 = incomeVault.messageForTransferRestriction(res1); // Assert assertEq(message1, TEXT_TRANSFER_OK); } + + /******* setRuleEngine *******/ + function testCannotSetRuleEngineWithTheSameValue() public { + vm.expectRevert(abi.encodeWithSelector(IncomeVault_SameValue.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setRuleEngine(ruleEngineMock); + } + + function testAdminCanUnsetTheRuleEngine() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setRuleEngine(IRuleEngine(ZERO_ADDRESS)); + assertEq(address(incomeVault.ruleEngine()), ZERO_ADDRESS); + // Without a RuleEngine there is no rule restriction anymore + assertEq(incomeVault.canTransfer(address(incomeVault), ADDRESS1, 11), true); + } + + /******* distributeDividend — H-2 *******/ + /** + * @notice The issuer cannot push a dividend to an address the RuleEngine refuses + * @dev This is the compliance property the RuleEngine integration exists to provide: before the + * H-2 fix the push path skipped the engine entirely, so a non-whitelisted holder could be paid. + */ + function testCannotDistributeToANonWhitelistedHolder() public { + // only the vault is whitelisted, not the holder + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleWhitelist.addAddressToTheList(address(incomeVault)); + + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.expectRevert( + abi.encodeWithSelector( + IncomeVault_InvalidTransfer.selector, address(incomeVault), ADDRESS1, defaultDepositAmount + ) + ); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + + assertEq(tokenPayment.balanceOf(ADDRESS1), 0); + assertEq(incomeVault.claimedDividend(ADDRESS1, defaultSnapshotTime), false); + } + + /** + * @notice Once both addresses are whitelisted the distribution goes through + */ + function testCanDistributeWhenBothAddressesWhitelisted() public { + address[] memory whitelist = new address[](2); + whitelist[0] = ADDRESS1; + whitelist[1] = address(incomeVault); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleWhitelist.addAddressesToTheList(whitelist); + + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + address[] memory addresses = new address[](1); + addresses[0] = ADDRESS1; + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(addresses, defaultSnapshotTime); + + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount); + } } diff --git a/test/SetDividendSnapshotSource.t.sol b/test/SetDividendSnapshotSource.t.sol new file mode 100644 index 0000000..d9ac9ff --- /dev/null +++ b/test/SetDividendSnapshotSource.t.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {MinimalSnapshotSourceMock} from "./mocks/MinimalSnapshotSourceMock.sol"; + +/** + * @title Replacing the snapshot source — finding A-3 + */ +contract SetDividendSnapshotSourceTest is HelperContract { + MinimalSnapshotSourceMock newSource; + + function setUp() public { + _deployContracts(); + newSource = new MinimalSnapshotSourceMock(); + + _deployOwnableVault(); + } + + /* ============ the counter is exact ============ */ + function testOpenClaimCountTracksTheOpenPeriods() public { + assertEq(incomeVault.openClaimCount(), 0); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(100, true); + assertEq(incomeVault.openClaimCount(), 1); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(200, true); + assertEq(incomeVault.openClaimCount(), 2); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(100, false); + assertEq(incomeVault.openClaimCount(), 1); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(200, false); + assertEq(incomeVault.openClaimCount(), 0); + } + + /** + * @notice Repeating a status must not move the counter — otherwise it could never return to zero + */ + function testRepeatedStatusWritesDoNotDriftTheCounter() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(100, true); + incomeVault.setStatusClaim(100, true); + incomeVault.setStatusClaim(100, true); + assertEq(incomeVault.openClaimCount(), 1); + + incomeVault.setStatusClaim(100, false); + incomeVault.setStatusClaim(100, false); + assertEq(incomeVault.openClaimCount(), 0); + vm.stopPrank(); + + // and closing a period that was never opened cannot underflow + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(999, false); + assertEq(incomeVault.openClaimCount(), 0); + } + + /* ============ the gate ============ */ + function testCannotReplaceTheSourceWhileAPeriodIsOpen() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_ClaimPeriodOpen.selector, 1)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setDividendSnapshotSource(ISnapshotSource(address(newSource))); + + assertEq(address(incomeVault.dividendSnapshotSource()), address(snapshotEngine)); + } + + function testCanReplaceTheSourceOnceEveryPeriodIsClosed() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, false); + + vm.expectEmit(true, false, false, false); + emit DividendSnapshotSourceSet(ISnapshotSource(address(newSource))); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setDividendSnapshotSource(ISnapshotSource(address(newSource))); + + assertEq(address(incomeVault.dividendSnapshotSource()), address(newSource)); + } + + function testCannotReplaceWithTheSameSource() public { + vm.expectRevert(abi.encodeWithSelector(IncomeVault_SameValue.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setDividendSnapshotSource(ISnapshotSource(address(snapshotEngine))); + } + + function testCannotReplaceWithTheZeroAddress() public { + vm.expectRevert(abi.encodeWithSelector(IncomeVault_SnapshotSourceWithAddressZeroNotAllowed.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setDividendSnapshotSource(ISnapshotSource(ZERO_ADDRESS)); + } + + /* ============ access control, both variants ============ */ + function testAttackerCannotReplaceTheSourceRoleVariant() public { + vm.expectRevert(abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, ATTACKER, bytes32(0))); + vm.prank(ATTACKER); + incomeVault.setDividendSnapshotSource(ISnapshotSource(address(newSource))); + } + + function testAttackerCannotReplaceTheSourceOwnableVariant() public { + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", ATTACKER)); + vm.prank(ATTACKER); + ownableVault.setDividendSnapshotSource(ISnapshotSource(address(newSource))); + } + + function testOwnerCanReplaceTheSource() public { + vm.prank(OWNER); + ownableVault.setDividendSnapshotSource(ISnapshotSource(address(newSource))); + assertEq(address(ownableVault.dividendSnapshotSource()), address(newSource)); + } + + /* ============ the replacement is actually used ============ */ + function testClaimsUseTheNewSourceAfterTheSwap() public { + _performDeposit(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setDividendSnapshotSource(ISnapshotSource(address(newSource))); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + // the mock reports 100/400, so the holder gets a quarter rather than the whole deposit + vm.prank(ADDRESS1); + incomeVault.claimDividend(defaultSnapshotTime); + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount / 4); + } +} diff --git a/test/SnapshotSource.t.sol b/test/SnapshotSource.t.sol new file mode 100644 index 0000000..a149ed4 --- /dev/null +++ b/test/SnapshotSource.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {MinimalSnapshotSourceMock} from "./mocks/MinimalSnapshotSourceMock.sol"; + +/** + * @title The vault only requires what it calls — finding I-1 + */ +contract SnapshotSourceTest is HelperContract { + function setUp() public { + _deployContracts(); + } + + /** + * @notice A source implementing only the three functions the vault calls is enough + * @dev The whole point of I-1: no `snapshotExists`, `snapshotBalanceOf`, `snapshotBalanceOfExact`, + * `snapshotTotalSupply` or `snapshotTotalSupplyExact` — five functions of `ISnapshotState` the + * vault never calls and no longer demands. + */ + function testAThreeFunctionSourceIsAccepted() public { + MinimalSnapshotSourceMock minimal = new MinimalSnapshotSourceMock(); + + Options memory opts; + opts.constructorData = abi.encode(ZERO_ADDRESS); + address proxy = Upgrades.deployTransparentProxy( + "IncomeVault.sol", + DEFAULT_ADMIN_ADDRESS, + abi.encodeCall( + IncomeVault.initialize, + ( + DEFAULT_ADMIN_ADDRESS, + IERC20(address(tokenPayment)), + ISnapshotSource(address(minimal)), + IRuleEngine(ZERO_ADDRESS), + TIME_LIMIT_TO_WITHDRAW + ) + ), + opts + ); + IncomeVault vault = IncomeVault(proxy); + assertEq(address(vault.dividendSnapshotSource()), address(minimal)); + + // and a claim against it pays out on those balances: 100/400 of the deposit + tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, defaultDepositAmount); + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenPayment.approve(address(vault), defaultDepositAmount); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.deposit(defaultSnapshotTime, defaultDepositAmount); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vault.setStatusClaim(defaultSnapshotTime, true); + vm.warp(defaultSnapshotTime + 50); + + vm.prank(ADDRESS1); + vault.claimDividend(defaultSnapshotTime); + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount / 4); + } + + /** + * @notice The real `ISnapshotState` SnapshotEngine still satisfies the narrower interface + * @dev Signatures are copied verbatim from `ISnapshotState`, so narrowing the type rejects nothing + * that worked before. This is the compatibility half of I-1. + */ + function testTheRealSnapshotEngineStillSatisfiesIt() public view { + ISnapshotSource asSource = ISnapshotSource(address(snapshotEngine)); + assertEq(address(asSource), address(snapshotEngine)); + assertEq(address(incomeVault.dividendSnapshotSource()), address(snapshotEngine)); + + // the three calls the vault makes all resolve against the real engine + (uint256 bal, uint256 supply) = asSource.snapshotInfo(defaultSnapshotTime, ADDRESS1); + assertEq(bal, 0); + assertEq(supply, 0); + } +} diff --git a/test/TransferRestrictionCode.t.sol b/test/TransferRestrictionCode.t.sol new file mode 100644 index 0000000..9a7a58a --- /dev/null +++ b/test/TransferRestrictionCode.t.sol @@ -0,0 +1,107 @@ +//SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; + +/** + * @title The ERC-1404 view answers for the whole payout decision — finding H-1 + * @dev + * `detectTransferRestriction` used to consult only the RuleEngine, so a paused vault or a frozen + * holder was reported as unrestricted while `canTransfer` said false and the claim reverted. Two views + * on one contract disagreed about the same payout, and the one carrying the ERC-1404 name was wrong. + * + * The invariant these tests pin is **agreement**: `detectTransferRestriction(...) == 0` exactly when + * `canTransfer(...)` is true. Each state is checked through both views, so removing any branch of + * either breaks a test rather than silently reintroducing the gap. + */ +contract TransferRestrictionCodeTest is HelperContract { + uint8 constant OK = uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + uint8 constant PAUSED = uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_PAUSED); + uint8 constant DEACTIVATED = uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_DEACTIVATED); + uint8 constant FROM_FROZEN = uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_FROM_FROZEN); + uint8 constant TO_FROZEN = uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_REJECTED_TO_FROZEN); + + function setUp() public { + _deployContracts(); + } + + /// @dev The two views must never disagree, whatever the state. + function _assertAgree(address from, address to, uint256 value) internal view { + bool allowed = incomeVault.canTransfer(from, to, value); + uint8 code = incomeVault.detectTransferRestriction(from, to, value); + assertEq(allowed, code == OK, "canTransfer and detectTransferRestriction disagree"); + } + + /** + * @notice With nothing restricting it, the payout is reported as unrestricted + */ + function testUnrestrictedPayoutReportsOk() public view { + assertEq(incomeVault.detectTransferRestriction(address(incomeVault), ADDRESS1, 100), OK); + _assertAgree(address(incomeVault), ADDRESS1, 100); + } + + /** + * @notice A paused vault is reported as paused, not as unrestricted + * @dev This is the regression: before H-1 this returned 0 while the claim reverted. + */ + function testPausedVaultReportsPaused() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.pause(); + + assertEq(incomeVault.detectTransferRestriction(address(incomeVault), ADDRESS1, 100), PAUSED); + assertFalse(incomeVault.canTransfer(address(incomeVault), ADDRESS1, 100)); + _assertAgree(address(incomeVault), ADDRESS1, 100); + } + + /** + * @notice A deactivated vault reports the more specific code, not merely "paused" + * @dev Deactivation requires the pause state, so both branches match; the specific one must win. + */ + function testDeactivatedVaultReportsDeactivated() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.pause(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deactivateContract(); + + assertEq(incomeVault.detectTransferRestriction(address(incomeVault), ADDRESS1, 100), DEACTIVATED); + _assertAgree(address(incomeVault), ADDRESS1, 100); + } + + /** + * @notice A frozen recipient is reported, and distinguished from a frozen sender + */ + function testFrozenPartiesReportTheirOwnCode() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setAddressFrozen(ADDRESS1, true, "Blacklist"); + + assertEq(incomeVault.detectTransferRestriction(address(incomeVault), ADDRESS1, 100), TO_FROZEN); + _assertAgree(address(incomeVault), ADDRESS1, 100); + + // the same holder as the sender side yields the FROM code instead + assertEq(incomeVault.detectTransferRestriction(ADDRESS1, ADDRESS2, 100), FROM_FROZEN); + _assertAgree(ADDRESS1, ADDRESS2, 100); + } + + /** + * @notice Every code the vault can return has a message, without a RuleEngine configured + * @dev Previously every code answered "No restriction", including codes that mean something. + */ + function testEveryVaultCodeHasAMessage() public view { + assertEq(incomeVault.messageForTransferRestriction(OK), "NoRestriction"); + assertEq(incomeVault.messageForTransferRestriction(PAUSED), "EnforcedPause"); + assertEq(incomeVault.messageForTransferRestriction(DEACTIVATED), "ContractDeactivated"); + assertEq(incomeVault.messageForTransferRestriction(FROM_FROZEN), "AddrFromIsFrozen"); + assertEq(incomeVault.messageForTransferRestriction(TO_FROZEN), "AddrToIsFrozen"); + // a code the vault never issues, with no RuleEngine to ask + assertEq(incomeVault.messageForTransferRestriction(200), "UnknownCode"); + } + + /** + * @notice The messages are CMTAT's, so a console written against a CMTAT reads payouts unchanged + */ + function testMessagesMatchTheCmtatVocabulary() public view { + assertEq(incomeVault.messageForTransferRestriction(PAUSED), "EnforcedPause"); + assertEq(incomeVault.messageForTransferRestriction(FROM_FROZEN), "AddrFromIsFrozen"); + } +} diff --git a/test/UnclaimedDividend.t.sol b/test/UnclaimedDividend.t.sol new file mode 100644 index 0000000..65cabc7 --- /dev/null +++ b/test/UnclaimedDividend.t.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol"; + +/** + * @title Per-period residue accounting — finding E-3 + */ +contract UnclaimedDividendTest is HelperContract { + using SlotDerivation for string; + + string constant NAMESPACE = "IncomeVault.storage.IncomeVaultInternal"; + uint256 t1; + uint256 t2; + + function setUp() public { + _deployContracts(); + t1 = block.timestamp + 100; + t2 = block.timestamp + 200; + + vm.prank(CMTAT_ADMIN); + snapshotEngine.scheduleSnapshot(t1); + vm.prank(CMTAT_ADMIN); + snapshotEngine.scheduleSnapshot(t2); + + tokenPayment.mint(DEFAULT_ADMIN_ADDRESS, 10_000); + } + + function _deposit(uint256 time, uint256 amount) internal { + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenPayment.approve(address(incomeVault), amount); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.deposit(time, amount); + } + + /* ============ the view ============ */ + function testUnclaimedStartsAtTheDeposit() public { + _deposit(t1, 1_000); + assertEq(incomeVault.unclaimedDividend(t1), 1_000); + assertEq(incomeVault.paidDividend(t1), 0); + } + + function testUnclaimedFallsAsHoldersArePaid() public { + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, 1_000); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS2, 1_000); + _deposit(t1, 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(t1, true); + vm.warp(t1 + 10); + + vm.prank(ADDRESS1); + incomeVault.claimDividend(t1); + + assertEq(incomeVault.paidDividend(t1), 500); + assertEq(incomeVault.unclaimedDividend(t1), 500); + // the denominator is deliberately untouched + assertEq(incomeVault.segregatedDividend(t1), 1_000); + } + + /** + * @notice The residue an issuer sweeps is exactly the rounding dust + * @dev Three holders sharing 1_000 each receive floor(1_000/3) = 333, leaving 1 behind. + */ + function testUnclaimedIsTheRoundingDustOnceEveryoneClaimed() public { + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, 1_000); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS2, 1_000); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS3, 1_000); + _deposit(t1, 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(t1, true); + vm.warp(t1 + 10); + + vm.prank(ADDRESS1); + incomeVault.claimDividend(t1); + vm.prank(ADDRESS2); + incomeVault.claimDividend(t1); + vm.prank(ADDRESS3); + incomeVault.claimDividend(t1); + + assertEq(incomeVault.paidDividend(t1), 999); + assertEq(incomeVault.unclaimedDividend(t1), 1, "the dust is one wei of the payment token"); + + // and the issuer can sweep exactly that, in one step + // (read first: a call inside the argument list would consume the prank) + uint256 dust = incomeVault.unclaimedDividend(t1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.withdraw(t1, dust, ADDRESS3); + assertEq(incomeVault.unclaimedDividend(t1), 0); + } + + /* ============ the bug this closes ============ */ + /** + * @notice A fully-claimed period cannot be swept again into another period's funds + * @dev + * Before this change `segregatedDividend[t1]` still read 1_000 after the sole holder had taken all + * 1_000, so `withdraw(t1, 1_000)` succeeded and drained the money deposited for `t2`. + */ + function testCannotSweepAFullyClaimedPeriodIntoAnotherPeriod() public { + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, 1_000); + _deposit(t1, 1_000); + _deposit(t2, 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(t1, true); + vm.warp(t1 + 10); + + vm.prank(ADDRESS1); + incomeVault.claimDividend(t1); // takes all of t1 + + assertEq(incomeVault.segregatedDividend(t1), 1_000, "denominator unchanged, as designed"); + assertEq(incomeVault.unclaimedDividend(t1), 0, "but nothing is left for t1"); + + vm.expectRevert(abi.encodeWithSelector(IncomeVault_NotEnoughAmount.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.withdraw(t1, 1_000, ADDRESS3); + + // t2's money is intact and still claimable + assertEq(tokenPayment.balanceOf(address(incomeVault)), 1_000); + assertEq(incomeVault.unclaimedDividend(t2), 1_000); + } + + function testCanStillWithdrawWhatThePeriodActuallyHolds() public { + _deposit(t1, 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.withdraw(t1, 400, ADDRESS3); + + assertEq(tokenPayment.balanceOf(ADDRESS3), 400); + assertEq(incomeVault.unclaimedDividend(t1), 600); + assertEq(incomeVault.segregatedDividend(t1), 600); + } + + function testCannotWithdrawMoreThanThePeriodHolds() public { + _deposit(t1, 1_000); + vm.expectRevert(abi.encodeWithSelector(IncomeVault_NotEnoughAmount.selector)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.withdraw(t1, 1_001, ADDRESS3); + } + + /** + * @notice Distribution counts towards the paid total too, not just holder-initiated claims + */ + function testDistributionCountsTowardsPaid() public { + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, 1_000); + _deposit(t1, 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(t1, true); + vm.warp(t1 + 10); + + address[] memory list = new address[](1); + list[0] = ADDRESS1; + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.distributeDividend(list, t1); + + assertEq(incomeVault.paidDividend(t1), 1_000); + assertEq(incomeVault.unclaimedDividend(t1), 0); + } + + /* ============ over-drawn periods ============ */ + /** + * @notice A claim is never funded from another period's deposit + * @dev + * Deterministic reproduction of the sequence the invariant fuzzer found once: sweeping a period + * mid-window lowers the pro-rata denominator, so a holder claiming afterwards is priced against + * the reduced figure while the period no longer holds that much. Before the bound, the shortfall + * was silently taken from another period's money. + */ + function testAClaimCannotBeFundedByAnotherPeriod() public { + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, 1_000); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS2, 1_000); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS3, 1_000); + + _deposit(t1, 900); + _deposit(t2, 900); // a second period, whose money must stay untouched + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(t1, true); + vm.warp(t1 + 10); + + // one holder takes their third + vm.prank(ADDRESS1); + incomeVault.claimDividend(t1); + assertEq(incomeVault.paidDividend(t1), 300); + assertEq(incomeVault.unclaimedDividend(t1), 600); + + // the issuer sweeps everything the period still holds, mid-window + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.withdraw(t1, 600, ADDRESS3); + assertEq(incomeVault.segregatedDividend(t1), 300, "denominator lowered by the sweep"); + assertEq(incomeVault.unclaimedDividend(t1), 0, "nothing left for this period"); + + // the next holder is now priced against 300 and would be owed 100 the period cannot fund + vm.expectRevert(abi.encodeWithSelector(IncomeVault_NotEnoughAmount.selector)); + vm.prank(ADDRESS2); + incomeVault.claimDividend(t1); + + // t2's deposit is intact + assertEq(incomeVault.unclaimedDividend(t2), 900); + assertEq(tokenPayment.balanceOf(address(incomeVault)), 900); + } + + /** + * @notice `unclaimedDividend` reports zero rather than reverting on an over-drawn period + */ + function testUnclaimedSaturatesInsteadOfUnderflowing() public { + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, 1_000); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS2, 1_000); + _deposit(t1, 1_000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.setStatusClaim(t1, true); + vm.warp(t1 + 10); + + vm.prank(ADDRESS1); + incomeVault.claimDividend(t1); // paid 500 + vm.prank(DEFAULT_ADMIN_ADDRESS); + incomeVault.withdraw(t1, 500, ADDRESS3); // segregated down to 500 + + assertEq(incomeVault.paidDividend(t1), 500); + assertEq(incomeVault.segregatedDividend(t1), 500); + assertEq(incomeVault.unclaimedDividend(t1), 0, "a view must never revert"); + } + + /** + * @notice Truly over-drawn (`paid` strictly above `segregated`) still reports zero, not a panic + * @dev + * The test above reaches `paid == segregated`, where a saturating rule and a plain subtraction + * agree — so it does not actually pin the saturation. `paid > segregated` is **unreachable through + * the public API**: `withdraw` is bounded by `unclaimedDividend`, and `_transferDividend` refuses a + * payout larger than it, so neither can push `paid` past `segregated`. The branch is defensive, + * which is exactly why it needs `vm.store` to be covered at all. + * + * Without this, replacing the rule with `segregated - paid` passes the entire suite. + */ + function testUnclaimedSaturatesWhenTrulyOverDrawn() public { + _deposit(t1, 1_000); + + // _paidDividend is field 6 of the ERC-7201 struct; force it above _segregatedDividend + bytes32 slot = keccak256(abi.encode(t1, uint256(NAMESPACE.erc7201Slot()) + 6)); + vm.store(address(incomeVault), slot, bytes32(uint256(1_500))); + + assertEq(incomeVault.paidDividend(t1), 1_500, "storage write did not land"); + assertGt(incomeVault.paidDividend(t1), incomeVault.segregatedDividend(t1)); + assertEq(incomeVault.unclaimedDividend(t1), 0, "must saturate, not underflow"); + } +} diff --git a/test/VersionModule.t.sol b/test/VersionModule.t.sol new file mode 100644 index 0000000..f044c8a --- /dev/null +++ b/test/VersionModule.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "./HelperContract.sol"; +import {IERC3643Version} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; + +/** + * @title Version string of every deployment variant + * @dev + * Exhaustive on purpose: **every** deployable contract must appear here. A half-covered version + * test reads as authoritative while missing the variant it exists to catch. + */ +contract VersionModuleTest is HelperContract { + /// @dev must match `VERSION` in src/modules/VersionModule.sol and the CHANGELOG entry + string constant EXPECTED_VERSION = "2.0.0"; + + function setUp() public { + _deployContracts(); + + _deployOwnableVault(); + } + + function testIncomeVaultExposesTheVersion() public view { + assertEq(incomeVault.version(), EXPECTED_VERSION); + } + + function testIncomeVaultOwnable2StepExposesTheVersion() public view { + assertEq(ownableVault.version(), EXPECTED_VERSION); + } + + /** + * @notice Both variants answer through the ERC-3643 version interface + */ + function testVersionIsReachableThroughIERC3643Version() public view { + assertEq(IERC3643Version(address(incomeVault)).version(), EXPECTED_VERSION); + assertEq(IERC3643Version(address(ownableVault)).version(), EXPECTED_VERSION); + } + + /** + * @notice The version is a compile-time constant, identical on the implementation and the proxy + */ + function testVersionIsTheSameOnTheImplementation() public { + IncomeVault implementation = new IncomeVault(ZERO_ADDRESS); + assertEq(implementation.version(), EXPECTED_VERSION); + } +} diff --git a/test/invariant/IncomeVault.invariant.t.sol b/test/invariant/IncomeVault.invariant.t.sol new file mode 100644 index 0000000..6c3173f --- /dev/null +++ b/test/invariant/IncomeVault.invariant.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "../HelperContract.sol"; +import {IncomeVaultHandler} from "./IncomeVaultHandler.sol"; + +/** + * @title Invariants of the dividend accounting — finding B-3 + * @dev + * The example suites check named scenarios. These check properties that must hold across **every** + * ordering of deposits, claims, batch claims, both distribution variants, withdrawals, freezes, + * pauses and time warps. + */ +contract IncomeVaultInvariantTest is HelperContract { + IncomeVaultHandler handler; + + uint256 constant HOLDER_BALANCE = 1_000; + + function setUp() public { + _deployContracts(); + + uint256[3] memory times = [block.timestamp + 100, block.timestamp + 200, block.timestamp + 300]; + address[3] memory holders = [ADDRESS1, ADDRESS2, ADDRESS3]; + + // give every holder a snapshot balance so the pro-rata maths is non-degenerate + for (uint256 i = 0; i < 3; ++i) { + vm.prank(CMTAT_ADMIN); + snapshotEngine.scheduleSnapshot(times[i]); + } + for (uint256 i = 0; i < 3; ++i) { + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(holders[i], HOLDER_BALANCE); + } + + handler = new IncomeVaultHandler(incomeVault, tokenPayment, DEFAULT_ADMIN_ADDRESS, times, holders); + + // the handler drives every privileged action, so it needs the roles + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + incomeVault.grantRole(DEFAULT_ADMIN_ROLE_BYTES, address(handler)); + vm.stopPrank(); + + targetContract(address(handler)); + } + + bytes32 constant DEFAULT_ADMIN_ROLE_BYTES = bytes32(0); + + /** + * @notice The vault never pays out more than was deposited + * @dev The headline solvency property. `g_paid` is the sum of every balance increase actually + * observed on a holder, across all three payout paths. + */ + function invariant_neverPaysMoreThanWasDeposited() public view { + assertLe(handler.g_paid(), handler.g_deposited(), "paid out more than was ever deposited"); + } + + /** + * @notice A holder is paid at most once per dividend time + * @dev Crosses all three payout paths: `claimDividend`, `claimDividendBatch` and both + * `distributeDividend` variants must not be combinable into a double payment. + */ + function invariant_noHolderIsPaidTwiceForOneTime() public view { + for (uint256 h = 0; h < 3; ++h) { + address holder = handler.holders(h); + for (uint256 t = 0; t < 3; ++t) { + assertLe( + handler.g_payCount(holder, handler.times(t)), + 1, + "a holder was paid twice for the same dividend time" + ); + } + } + } + + /** + * @notice `claimedDividend` is monotonic — once set it is never cleared + * @dev Checked by construction: nothing in the contract writes `false`, and the pay counter above + * would exceed one if a cleared flag ever allowed a second payment. + */ + function invariant_claimedFlagIsMonotonic() public view { + for (uint256 h = 0; h < 3; ++h) { + address holder = handler.holders(h); + for (uint256 t = 0; t < 3; ++t) { + uint256 time = handler.times(t); + if (handler.g_payCount(holder, time) > 0) { + assertTrue( + incomeVault.claimedDividend(holder, time), "a holder was paid but is not marked as claimed" + ); + } + } + } + } + + /** + * @notice Every payout is explained by a period becoming claimed + */ + function invariant_noUnexplainedPayment() public view { + assertEq( + handler.g_unexplainedPayments(), 0, "a batch path paid a holder without any period transitioning to claimed" + ); + } + + /** + * @notice The vault's token balance always accounts for every deposit + * @dev balance == deposited - paid - withdrawn, where `paid` is measured as balance increases on + * the three holders. Value leaving to any *other* recipient breaks the identity, so this is a + * leakage check rather than a tautology. + */ + function invariant_balanceAccountsForEveryDeposit() public view { + assertEq( + tokenPayment.balanceOf(address(incomeVault)), + handler.g_deposited() - handler.g_paid() - handler.g_withdrawn(), + "vault balance does not reconcile with deposits, payouts and withdrawals" + ); + } + + /** + * @notice Every period's residue is actually backed by tokens the vault holds + * @dev + * The solvency property the earlier invariants missed. `sum(unclaimedDividend)` is what the vault + * still owes across periods; it must never exceed the balance, or one period's accounting is + * promising another period's money. Withdrawing from a fully-claimed period used to break exactly + * this. + */ + function invariant_everyPeriodResidueIsBacked() public view { + uint256 owed; + for (uint256 t = 0; t < 3; ++t) { + owed += incomeVault.unclaimedDividend(handler.times(t)); + } + assertLe( + owed, + tokenPayment.balanceOf(address(incomeVault)), + "the sum of per-period residues exceeds the tokens actually held" + ); + } + + /** + * @notice The per-time accounting never exceeds what is still held + * @dev `segregatedDividend` is the pro-rata denominator and is deliberately *not* decremented on + * a payout, so it is a record of what was deposited for a period, reduced only by `withdraw`. + */ + function invariant_segregatedNeverExceedsDeposits() public view { + uint256 sum; + for (uint256 t = 0; t < 3; ++t) { + sum += incomeVault.segregatedDividend(handler.times(t)); + } + assertLe(sum, handler.g_deposited(), "segregated accounting exceeds total deposits"); + } +} diff --git a/test/invariant/IncomeVaultHandler.sol b/test/invariant/IncomeVaultHandler.sol new file mode 100644 index 0000000..e8a49ac --- /dev/null +++ b/test/invariant/IncomeVaultHandler.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import {CommonBase} from "forge-std/Base.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; +import {IncomeVault} from "../../src/deployment/IncomeVault.sol"; +import {ERC20PaymentMock} from "../mocks/ERC20PaymentMock.sol"; + +/** + * @title Bounded random driver for the IncomeVault invariants + * @dev + * Actions are squeezed into a small legal domain — three dividend times, three holders — so a run + * explores *orderings* rather than wandering into reverts. Calls that legitimately revert (claiming + * outside the window, while paused, while frozen) are caught: the invariant is about what the vault + * does when it succeeds, not about which calls are allowed. + * + * Ghosts are deliberately **scalar totals** rather than per-time sums. A batch claim pays several + * periods in one balance delta and cannot be attributed to a single `time` from the outside; totals + * need no attribution and still express the properties that matter. + */ +contract IncomeVaultHandler is CommonBase, StdUtils { + IncomeVault public immutable vault; + ERC20PaymentMock public immutable payment; + address public immutable admin; + + uint256[3] public times; + address[3] public holders; + + /* ============ ghosts ============ */ + uint256 public g_deposited; + uint256 public g_paid; + uint256 public g_withdrawn; + /// @dev how many times a holder was actually paid for a time — must never exceed one + mapping(address holder => mapping(uint256 time => uint256)) public g_payCount; + uint256 public g_calls; + + constructor( + IncomeVault vault_, + ERC20PaymentMock payment_, + address admin_, + uint256[3] memory times_, + address[3] memory holders_ + ) { + vault = vault_; + payment = payment_; + admin = admin_; + times = times_; + holders = holders_; + } + + function _time(uint256 seed) internal view returns (uint256) { + return times[bound(seed, 0, 2)]; + } + + function _holder(uint256 seed) internal view returns (address) { + return holders[bound(seed, 0, 2)]; + } + + /// @dev snapshot which (holder, time) pairs are already marked claimed + function _claimedFlags(address holder) internal view returns (bool[3] memory f) { + for (uint256 i = 0; i < 3; ++i) { + f[i] = vault.claimedDividend(holder, times[i]); + } + } + + /// @dev payments observed on a batch path that no newly-claimed period explains + uint256 public g_unexplainedPayments; + + /** + * @dev Single-time paths: count a payment whenever the balance actually rises. + * Counting flag *transitions* instead would be blind to the very thing this is here to catch — + * a second payment for a period already marked claimed leaves the flag untouched. + */ + function _settleOne(address holder, uint256 time, uint256 balanceBefore) internal { + uint256 delta = payment.balanceOf(holder) - balanceBefore; + if (delta > 0) { + g_paid += delta; + g_payCount[holder][time] += 1; + } + } + + /** + * @dev Batch paths pay several periods in one delta, so payments are attributed by flag + * transition. A delta that no transition explains is a re-payment and is counted separately. + */ + function _settleBatch(address holder, uint256 balanceBefore, bool[3] memory claimedBefore) internal { + uint256 delta = payment.balanceOf(holder) - balanceBefore; + if (delta == 0) return; + g_paid += delta; + uint256 transitions; + for (uint256 i = 0; i < 3; ++i) { + if (!claimedBefore[i] && vault.claimedDividend(holder, times[i])) { + g_payCount[holder][times[i]] += 1; + ++transitions; + } + } + if (transitions == 0) { + ++g_unexplainedPayments; + } + } + + /* ============ actions ============ */ + function deposit(uint256 timeSeed, uint256 amount) external { + ++g_calls; + amount = bound(amount, 1, 1_000); + payment.mint(admin, amount); + vm.startPrank(admin); + payment.approve(address(vault), amount); + vault.deposit(_time(timeSeed), amount); + vm.stopPrank(); + g_deposited += amount; + } + + function setStatusClaim(uint256 timeSeed, bool status) external { + ++g_calls; + vm.prank(admin); + vault.setStatusClaim(_time(timeSeed), status); + } + + function claim(uint256 timeSeed, uint256 holderSeed) external { + ++g_calls; + address holder = _holder(holderSeed); + uint256 time = _time(timeSeed); + uint256 before = payment.balanceOf(holder); + vm.prank(holder); + try vault.claimDividend(time) { + _settleOne(holder, time, before); + } catch {} + } + + function claimBatch(uint256 holderSeed) external { + ++g_calls; + address holder = _holder(holderSeed); + uint256[] memory all = new uint256[](3); + for (uint256 i = 0; i < 3; ++i) { + all[i] = times[i]; + } + uint256 before = payment.balanceOf(holder); + bool[3] memory flags = _claimedFlags(holder); + vm.prank(holder); + try vault.claimDividendBatch(all) { + _settleBatch(holder, before, flags); + } catch {} + } + + function distribute(uint256 timeSeed, uint256 holderSeed) external { + ++g_calls; + address holder = _holder(holderSeed); + uint256 time = _time(timeSeed); + address[] memory list = new address[](1); + list[0] = holder; + uint256 before = payment.balanceOf(holder); + vm.prank(admin); + try vault.distributeDividend(list, time) { + _settleOne(holder, time, before); + } catch {} + } + + function distributeBestEffort(uint256 timeSeed) external { + ++g_calls; + uint256 time = _time(timeSeed); + address[] memory list = new address[](3); + uint256[3] memory befores; + bool[3][3] memory flags; + for (uint256 i = 0; i < 3; ++i) { + list[i] = holders[i]; + befores[i] = payment.balanceOf(holders[i]); + flags[i] = _claimedFlags(holders[i]); + } + vm.prank(admin); + try vault.distributeDividendBestEffort(list, time) { + for (uint256 i = 0; i < 3; ++i) { + _settleBatch(holders[i], befores[i], flags[i]); + } + } catch {} + } + + /** + * @dev The issuer sweeping a period. Bounded to what the period holds so the call itself is legal; + * whether sweeping *while claims are open* is wise is exactly what the solvency invariant probes. + */ + function withdraw(uint256 timeSeed, uint256 amount) external { + ++g_calls; + uint256 time = _time(timeSeed); + uint256 available = vault.segregatedDividend(time); + if (available == 0) return; + amount = bound(amount, 1, available); + vm.prank(admin); + try vault.withdraw(time, amount, admin) { + g_withdrawn += amount; + } catch {} + } + + function freeze(uint256 holderSeed, bool status) external { + ++g_calls; + vm.prank(admin); + vault.setAddressFrozen(_holder(holderSeed), status, ""); + } + + function pauseToggle(bool on) external { + ++g_calls; + vm.prank(admin); + if (on) try vault.pause() {} catch {} else try vault.unpause() {} catch {} + } + + function warp(uint256 secondsAhead) external { + ++g_calls; + vm.warp(block.timestamp + bound(secondsAhead, 1, 30 days)); + } +} diff --git a/test/mocks/CMTATDividendHostMock.sol b/test/mocks/CMTATDividendHostMock.sol new file mode 100644 index 0000000..154fb68 --- /dev/null +++ b/test/mocks/CMTATDividendHostMock.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import {CMTATUpgradeableInternalSnapshot} from "SnapshotEngine/deployment/CMTATUpgradeableInternalSnapshot.sol"; +import {IncomeVaultOpen} from "../../src/public/IncomeVaultOpen.sol"; +import {IncomeVaultRestricted} from "../../src/public/IncomeVaultRestricted.sol"; +import {IncomeVaultValidationCore} from "../../src/modules/IncomeVaultValidationCore.sol"; +import {IncomeVaultSnapshotCore} from "../../src/modules/IncomeVaultSnapshotCore.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title A CMTAT with internal snapshots that pays its own dividends — findings M-1 and M-2 + * @dev + * This is the scenario the modularity review was written against: a token that already has a + * validation stack and already records snapshots, embedding the distribution logic directly rather + * than deploying a separate vault beside it. + * + * Two things had to change before this file could exist, and it is the regression guard for both: + * + * - **M-1.** `IncomeVaultOpen` and `IncomeVaultRestricted` used to drag CMTAT's `PauseModule` and + * `EnforcementModule` in transitively, so a host that already had them could not linearize — + * `Error (5005)`, which no override can repair. They now depend on {IncomeVaultValidationCore}, + * which inherits nothing, and this contract answers it with the CMTAT's own `canTransfer`. + * - **M-2.** The snapshot source used to be a stored address behind a `snapshotEngine()` getter, + * which collided with the identically-named CMTAT getter that returns a *different type* — a + * collision no override list can resolve. It is now {IncomeVaultSnapshotCore}, three hooks that + * this contract answers **from its own snapshot records**, with no external contract and nothing + * stored. + * + * It only has to compile. Re-couple either dependency and this file stops compiling. + */ +contract CMTATDividendHostMock is CMTATUpgradeableInternalSnapshot, IncomeVaultOpen, IncomeVaultRestricted { + /// @notice Raised when the CMTAT's own validation stack rejects a payout + error CMTATDividendHost_InvalidTransfer(address from, address to, uint256 value); + + /* ============ the host IS its own snapshot source — finding M-2 ============ */ + /** + * @inheritdoc IncomeVaultSnapshotCore + * @dev Answered from the CMTAT's own snapshot records, not from an external engine. + */ + function _snapshotInfo(uint256 time, address tokenHolder) + internal + view + virtual + override + returns (uint256, uint256) + { + return snapshotInfo(time, tokenHolder); + } + + /// @inheritdoc IncomeVaultSnapshotCore + function _snapshotInfoBatch(uint256 time, address[] calldata addresses) + internal + view + virtual + override + returns (uint256[] memory, uint256) + { + return snapshotInfoBatch(time, addresses); + } + + /** + * @inheritdoc IncomeVaultSnapshotCore + * @dev The CMTAT overload takes `addresses` in calldata while this hook receives it in memory, so + * the rows are assembled here rather than forwarded. The answers still come from the same records. + */ + function _snapshotInfoBatch(uint256[] calldata times, address[] memory addresses) + internal + view + virtual + override + returns (uint256[][] memory balances, uint256[] memory supplies) + { + balances = new uint256[][](times.length); + supplies = new uint256[](times.length); + for (uint256 t = 0; t < times.length; ++t) { + uint256[] memory row = new uint256[](addresses.length); + uint256 supply; + for (uint256 i = 0; i < addresses.length; ++i) { + (row[i], supply) = snapshotInfo(times[t], addresses[i]); + } + balances[t] = row; + supplies[t] = supply; + } + } + + /* ============ the host answers the validation question itself — finding M-1 ============ */ + /** + * @inheritdoc IncomeVaultValidationCore + * @dev Delegates to the CMTAT's own pause, freeze and RuleEngine stack. No second copy of it. + */ + function _validateTransfer(address from, address to, uint256 value) internal view virtual override { + require(canTransfer(from, to, value), CMTATDividendHost_InvalidTransfer(from, to, value)); + } + + /* ============ Access control — open, the shape is the point ============ */ + /// @inheritdoc IncomeVaultRestricted + function _authorizeDeposit() internal view virtual override {} + /// @inheritdoc IncomeVaultRestricted + function _authorizeWithdraw() internal view virtual override {} + /// @inheritdoc IncomeVaultRestricted + function _authorizeDistribute() internal view virtual override {} + /// @inheritdoc IncomeVaultRestricted + function _authorizeOperator() internal view virtual override {} +} diff --git a/test/mocks/ERC20PaymentMock.sol b/test/mocks/ERC20PaymentMock.sol new file mode 100644 index 0000000..c3f415f --- /dev/null +++ b/test/mocks/ERC20PaymentMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @title Minimal ERC-20 used as payment token in the tests + * @dev The IncomeVault only requires the payment token to be a standard ERC-20. + */ +contract ERC20PaymentMock is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + function mint(address account, uint256 value) public { + _mint(account, value); + } + + function decimals() public pure override returns (uint8) { + return 0; + } +} diff --git a/test/mocks/EmbeddedDividendHostMock.sol b/test/mocks/EmbeddedDividendHostMock.sol new file mode 100644 index 0000000..d90232d --- /dev/null +++ b/test/mocks/EmbeddedDividendHostMock.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import {IncomeVaultOpen} from "../../src/public/IncomeVaultOpen.sol"; +import {IncomeVaultRestricted} from "../../src/public/IncomeVaultRestricted.sol"; +import {IncomeVaultValidationCore} from "../../src/modules/IncomeVaultValidationCore.sol"; +import {IncomeVaultSnapshotCore} from "../../src/modules/IncomeVaultSnapshotCore.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title A host that embeds the dividend logic without any CMTAT at all — findings M-1 and M-2 + * @dev + * This contract is the regression guard for the two modularity splits, and it only has to **compile**. + * + * Before M-1, `IncomeVaultOpen` and `IncomeVaultRestricted` each inherited CMTAT's `PauseModule` and + * `EnforcementModule` transitively, so a host that already had its own could not embed them — + * `Error (5005)`, linearization impossible, which no override can repair. Before M-2, the snapshot + * source was a stored address behind a `snapshotEngine()` getter that a host could not replace. + * + * It answers the questions the payout paths ask, and nothing more: + * + * - {IncomeVaultValidationCore-_validateTransfer} — here, a trivial "always allowed" policy standing in + * for whatever the host already owns; + * - the three {IncomeVaultSnapshotCore} hooks — answered **from the host itself**, with no external + * snapshot contract and no stored address, which is the M-2 property; + * - the four `_authorize*` hooks — here, open, because the point is the *shape* of the dependency, not + * the policy. + * + * Re-couple either dependency to a concrete implementation and this file stops compiling. + * {CMTATDividendHostMock} is the same guard for a host that *is* a CMTAT with internal snapshots. + */ +contract EmbeddedDividendHostMock is IncomeVaultOpen, IncomeVaultRestricted { + /// @dev stands in for whatever balances the host already records + uint256 public constant HOLDER_BALANCE = 100; + /// @dev stands in for the host's own total supply + uint256 public constant TOTAL_SUPPLY = 400; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Wire up the embedded dividend logic + * @param paymentToken the ERC-20 the dividends are paid in + * @param timeLimitToWithdraw_ the claim window length + */ + function initialize(IERC20 paymentToken, uint256 timeLimitToWithdraw_) public initializer { + _setERC20TokenPayment(paymentToken); + __IncomeVaultRestricted_init_unchained(timeLimitToWithdraw_); + } + + /* ============ the host IS its own snapshot source — finding M-2 ============ */ + /// @inheritdoc IncomeVaultSnapshotCore + function _snapshotInfo(uint256, address) internal pure virtual override returns (uint256, uint256) { + return (HOLDER_BALANCE, TOTAL_SUPPLY); + } + + /// @inheritdoc IncomeVaultSnapshotCore + function _snapshotInfoBatch(uint256, address[] calldata addresses) + internal + pure + virtual + override + returns (uint256[] memory balances, uint256) + { + balances = new uint256[](addresses.length); + for (uint256 i = 0; i < addresses.length; ++i) { + balances[i] = HOLDER_BALANCE; + } + return (balances, TOTAL_SUPPLY); + } + + /// @inheritdoc IncomeVaultSnapshotCore + function _snapshotInfoBatch(uint256[] calldata times, address[] memory addresses) + internal + pure + virtual + override + returns (uint256[][] memory balances, uint256[] memory supplies) + { + balances = new uint256[][](times.length); + supplies = new uint256[](times.length); + for (uint256 t = 0; t < times.length; ++t) { + uint256[] memory row = new uint256[](addresses.length); + for (uint256 i = 0; i < addresses.length; ++i) { + row[i] = HOLDER_BALANCE; + } + balances[t] = row; + supplies[t] = TOTAL_SUPPLY; + } + } + + /* ============ the host answers the validation question itself — finding M-1 ============ */ + /** + * @inheritdoc IncomeVaultValidationCore + * @dev Stands in for whatever policy the host already owns. Always allows. + */ + function _validateTransfer(address, address, uint256) internal view virtual override {} + + /* ============ Access control — open, the shape is the point ============ */ + /// @inheritdoc IncomeVaultRestricted + function _authorizeDeposit() internal view virtual override {} + /// @inheritdoc IncomeVaultRestricted + function _authorizeWithdraw() internal view virtual override {} + /// @inheritdoc IncomeVaultRestricted + function _authorizeDistribute() internal view virtual override {} + /// @inheritdoc IncomeVaultRestricted + function _authorizeOperator() internal view virtual override {} +} diff --git a/test/mocks/IncomeVaultOverrideMock.sol b/test/mocks/IncomeVaultOverrideMock.sol new file mode 100644 index 0000000..aabbd0f --- /dev/null +++ b/test/mocks/IncomeVaultOverrideMock.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import {IIncomeVault} from "../../src/interfaces/IIncomeVault.sol"; +import {IncomeVault} from "../../src/deployment/IncomeVault.sol"; +import {IncomeVaultOpen} from "../../src/public/IncomeVaultOpen.sol"; +import {IncomeVaultInternal} from "../../src/modules/IncomeVaultInternal.sol"; + +/** + * @title Compile-time guard for the `virtual` convention on the claim entrypoints + * @dev + * Removing `virtual` from any function this contract overrides breaks the build with + * `Error (4334): Trying to override non-virtual function`. That covers the **public** claim entrypoints + * and, since finding E-1 of `CLAUDE_ANALYSIS_SECOND.md`, the three core **internal** routines as well. + * + * Compiling is not enough on its own: a silently shadowed override compiles and is never called. For + * the two **state-changing** overrides the counters settle it — `test/OverrideMock.t.sol` drives a real + * claim through this contract and asserts both incremented. The `view` overrides cannot count anything, + * so those stay compile-guarded only; that is a real limit of this technique, not an oversight. + */ +contract IncomeVaultOverrideMock is IncomeVault { + /// @notice Times the public claim entrypoint was overridden through + uint256 public claimCount; + /// @notice Times the internal payout routine was overridden through + uint256 public transferCount; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address forwarderIrrevocable) IncomeVault(forwarderIrrevocable) {} + + function claimDividend(uint256 time) public virtual override(IIncomeVault, IncomeVaultOpen) { + ++claimCount; + super.claimDividend(time); + } + + function validateTimeCode(uint256 time) + public + view + virtual + override(IIncomeVault, IncomeVaultOpen) + returns (TIME_ERROR_CODE) + { + return super.validateTimeCode(time); + } + + /* ============ the internal routines — finding E-1 ============ */ + /** + * @inheritdoc IncomeVaultInternal + */ + function _transferDividend(uint256 time, address tokenHolder, uint256 tokenHolderDividend) + internal + virtual + override(IncomeVaultInternal) + { + ++transferCount; + super._transferDividend(time, tokenHolder, tokenHolderDividend); + } + + /** + * @inheritdoc IncomeVaultInternal + */ + function _computeDividend(uint256 time, uint256 senderBalance, uint256 tokenTotalSupply) + internal + view + virtual + override(IncomeVaultInternal) + returns (uint256) + { + return super._computeDividend(time, senderBalance, tokenTotalSupply); + } + + /** + * @inheritdoc IncomeVaultInternal + */ + function _computeDividendBatch( + uint256 time, + address[] calldata tokenHolders, + uint256[] memory tokenHoldersBalance, + uint256 tokenTotalSupply + ) internal view virtual override(IncomeVaultInternal) returns (uint256[] memory) { + return super._computeDividendBatch(time, tokenHolders, tokenHoldersBalance, tokenTotalSupply); + } +} diff --git a/test/mocks/MinimalSnapshotSourceMock.sol b/test/mocks/MinimalSnapshotSourceMock.sol new file mode 100644 index 0000000..24b1503 --- /dev/null +++ b/test/mocks/MinimalSnapshotSourceMock.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import {ISnapshotSource} from "../../src/interfaces/ISnapshotSource.sol"; + +/** + * @title A snapshot source implementing ONLY what the vault calls + * @dev + * This is the point of finding I-1: three functions, not the eight of `ISnapshotState`. If this + * contract compiles and the vault works against it, the extra five were never required. It returns + * fixed values — it exists to prove the interface is sufficient, not to model snapshot semantics. + */ +contract MinimalSnapshotSourceMock is ISnapshotSource { + uint256 public constant BALANCE = 100; + uint256 public constant TOTAL_SUPPLY = 400; + + /// @inheritdoc ISnapshotSource + function snapshotInfo(uint256, address) + external + pure + override + returns (uint256 tokenHolderBalance, uint256 totalSupply) + { + return (BALANCE, TOTAL_SUPPLY); + } + + /// @inheritdoc ISnapshotSource + function snapshotInfoBatch(uint256, address[] calldata addresses) + external + pure + override + returns (uint256[] memory tokenHolderBalances, uint256 totalSupply) + { + tokenHolderBalances = new uint256[](addresses.length); + for (uint256 i = 0; i < addresses.length; ++i) { + tokenHolderBalances[i] = BALANCE; + } + return (tokenHolderBalances, TOTAL_SUPPLY); + } + + /// @inheritdoc ISnapshotSource + function snapshotInfoBatch(uint256[] calldata times, address[] calldata addresses) + external + pure + override + returns (uint256[][] memory tokenHolderBalances, uint256[] memory totalSupplies) + { + tokenHolderBalances = new uint256[][](times.length); + totalSupplies = new uint256[](times.length); + for (uint256 t = 0; t < times.length; ++t) { + uint256[] memory row = new uint256[](addresses.length); + for (uint256 i = 0; i < addresses.length; ++i) { + row[i] = BALANCE; + } + tokenHolderBalances[t] = row; + totalSupplies[t] = TOTAL_SUPPLY; + } + } +} diff --git a/test/mocks/NoForwarderVaultMock.sol b/test/mocks/NoForwarderVaultMock.sol new file mode 100644 index 0000000..2dc4604 --- /dev/null +++ b/test/mocks/NoForwarderVaultMock.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import {IncomeVaultBase} from "../../src/IncomeVaultBase.sol"; +import {IncomeVaultValidationCore} from "../../src/modules/IncomeVaultValidationCore.sol"; +import {IncomeVaultRestricted} from "../../src/public/IncomeVaultRestricted.sol"; +import {IncomeVaultSnapshotModule} from "../../src/modules/IncomeVaultSnapshotModule.sol"; +import {ISnapshotSource} from "../../src/interfaces/ISnapshotSource.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title A deployment with no trusted forwarder — finding M-8 + * @dev + * Inherits {IncomeVaultBase} directly instead of {IncomeVaultBaseERC2771}, so it carries no ERC-2771 + * context: no immutable forwarder in the bytecode, no calldata-suffix handling, and `_msgSender()` is + * plain `msg.sender` with nothing able to override it. + * + * Before M-8 this contract could not exist. `ERC2771Module` was inherited by the base itself, so every + * deployment carried a forwarder whether it wanted one or not; opting out meant passing the zero + * address and still paying for the machinery. Gasless support is now a deployment decision, exactly + * like the access-control model and the transfer-restriction policy. + * + * Access control is left open because the point is the *shape* of the dependency, not the policy. + */ +contract NoForwarderVaultMock is IncomeVaultBase { + // NOTE: no `_disableInitializers()` here, unlike the shipped deployments. This double is deployed + // directly by the test rather than behind a proxy, because what it demonstrates is the absence of + // the ERC-2771 context, not the upgrade pattern. + + /** + * @notice Wire up a vault with no meta-transaction support + * @param paymentToken the ERC-20 the dividends are paid in + * @param snapshotSource where the holder balances come from + * @param timeLimitToWithdraw_ the claim window length + */ + function initialize(IERC20 paymentToken, ISnapshotSource snapshotSource, uint256 timeLimitToWithdraw_) + public + initializer + { + __IncomeVaultBase_init_unchained(paymentToken, snapshotSource, timeLimitToWithdraw_); + } + + /** + * @inheritdoc IncomeVaultValidationCore + * @dev Stands in for whatever policy a real deployment would choose. Always allows. + */ + function _validateTransfer(address, address, uint256) internal view virtual override {} + + /// @inheritdoc IncomeVaultRestricted + function _authorizeDeposit() internal view virtual override {} + /// @inheritdoc IncomeVaultRestricted + function _authorizeWithdraw() internal view virtual override {} + /// @inheritdoc IncomeVaultRestricted + function _authorizeDistribute() internal view virtual override {} + /// @inheritdoc IncomeVaultRestricted + function _authorizeOperator() internal view virtual override {} + /// @inheritdoc IncomeVaultSnapshotModule + function _authorizeSnapshotSourceManagement() internal view virtual override {} +} diff --git a/test/script/Deploy.t.sol b/test/script/Deploy.t.sol new file mode 100644 index 0000000..6904d98 --- /dev/null +++ b/test/script/Deploy.t.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.24; + +import "../HelperContract.sol"; +import {DeployIncomeVault} from "../../script/DeployIncomeVault.s.sol"; +import {DeployIncomeVaultOwnable2Step} from "../../script/DeployIncomeVaultOwnable2Step.s.sol"; + +/** + * @title The deployment scripts — finding C-4 + * @dev + * The scripts are the documented way to deploy, so they are tested like anything else. Each case + * drives `deploy(config)` directly, which is why that function is separated from the environment + * reading in `run()` — the tested code path is the one an operator runs. + * + * The assertions that matter are not "an address came back" but "the vault this produced actually + * pays a dividend". + */ +contract DeployScriptTest is HelperContract { + DeployIncomeVault roleScript; + DeployIncomeVaultOwnable2Step ownerScript; + + address constant PROXY_ADMIN = address(41); + address constant VAULT_ADMIN = address(42); + address constant VAULT_OWNER = address(43); + + function setUp() public { + _deployContracts(); + roleScript = new DeployIncomeVault(); + ownerScript = new DeployIncomeVaultOwnable2Step(); + } + + function _roleConfig() internal view returns (DeployIncomeVault.Config memory) { + return DeployIncomeVault.Config({ + proxyAdmin: PROXY_ADMIN, + admin: VAULT_ADMIN, + forwarder: ZERO_ADDRESS, + paymentToken: IERC20(address(tokenPayment)), + snapshotEngine: ISnapshotSource(address(snapshotEngine)), + ruleEngine: IRuleEngine(ZERO_ADDRESS), + timeLimitToWithdraw: TIME_LIMIT_TO_WITHDRAW + }); + } + + function _ownerConfig() internal view returns (DeployIncomeVaultOwnable2Step.Config memory) { + return DeployIncomeVaultOwnable2Step.Config({ + proxyAdmin: PROXY_ADMIN, + owner: VAULT_OWNER, + forwarder: ZERO_ADDRESS, + paymentToken: IERC20(address(tokenPayment)), + snapshotEngine: ISnapshotSource(address(snapshotEngine)), + ruleEngine: IRuleEngine(ZERO_ADDRESS), + timeLimitToWithdraw: TIME_LIMIT_TO_WITHDRAW + }); + } + + /* ============ role-based variant ============ */ + function testDeploysAnInitializedVault() public { + IncomeVault vault = roleScript.deploy(_roleConfig()); + + assertEq(address(vault.ERC20TokenPayment()), address(tokenPayment)); + assertEq(address(vault.dividendSnapshotSource()), address(snapshotEngine)); + assertEq(address(vault.ruleEngine()), ZERO_ADDRESS); + assertEq(vault.timeLimitToWithdraw(), TIME_LIMIT_TO_WITHDRAW); + assertEq(vault.version(), "2.0.0"); + assertTrue(vault.hasRole(bytes32(0), VAULT_ADMIN), "admin holds DEFAULT_ADMIN_ROLE"); + } + + /** + * @notice The deployed vault cannot be initialized a second time + */ + function testTheDeployedVaultIsAlreadyInitialized() public { + IncomeVault vault = roleScript.deploy(_roleConfig()); + vm.expectRevert(abi.encodeWithSignature("InvalidInitialization()")); + vault.initialize( + VAULT_ADMIN, + IERC20(address(tokenPayment)), + ISnapshotSource(address(snapshotEngine)), + IRuleEngine(ZERO_ADDRESS), + TIME_LIMIT_TO_WITHDRAW + ); + } + + /** + * @notice The real test: a vault the script produced actually pays a dividend + */ + function testTheDeployedVaultPaysADividendEndToEnd() public { + IncomeVault vault = roleScript.deploy(_roleConfig()); + + vm.prank(CMTAT_ADMIN); + snapshotEngine.scheduleSnapshot(defaultSnapshotTime); + vm.prank(CMTAT_ADMIN); + CMTAT_CONTRACT.mint(ADDRESS1, ADDRESS1_INITIAL_AMOUNT); + + tokenPayment.mint(VAULT_ADMIN, defaultDepositAmount); + vm.prank(VAULT_ADMIN); + tokenPayment.approve(address(vault), defaultDepositAmount); + vm.prank(VAULT_ADMIN); + vault.deposit(defaultSnapshotTime, defaultDepositAmount); + vm.prank(VAULT_ADMIN); + vault.setStatusClaim(defaultSnapshotTime, true); + + vm.warp(defaultSnapshotTime + 50); + vm.prank(ADDRESS1); + vault.claimDividend(defaultSnapshotTime); + + assertEq(tokenPayment.balanceOf(ADDRESS1), defaultDepositAmount); + } + + /* ============ single-owner variant ============ */ + function testDeploysAnInitializedOwnableVault() public { + IncomeVaultOwnable2Step vault = ownerScript.deploy(_ownerConfig()); + + assertEq(vault.owner(), VAULT_OWNER); + assertEq(address(vault.dividendSnapshotSource()), address(snapshotEngine)); + assertEq(vault.timeLimitToWithdraw(), TIME_LIMIT_TO_WITHDRAW); + assertEq(vault.version(), "2.0.0"); + } + + function testTheDeployedOwnableVaultIsOperableByItsOwner() public { + IncomeVaultOwnable2Step vault = ownerScript.deploy(_ownerConfig()); + vm.prank(VAULT_OWNER); + vault.setStatusClaim(defaultSnapshotTime, true); + assertEq(vault.segregatedClaim(defaultSnapshotTime), true); + } + + /* ============ configuration guards ============ */ + /** + * @notice The check the contract cannot do for itself: an EOA where a contract belongs + * @dev A mistyped address, or one copied from another chain, initializes fine and then reverts on + * the first claim. Catching it in the script is the whole point of having one. + */ + function testRejectsAPaymentTokenThatIsNotAContract() public { + DeployIncomeVault.Config memory config = _roleConfig(); + config.paymentToken = IERC20(address(0xBEEF)); + vm.expectRevert(bytes("DeployIncomeVault: PAYMENT_TOKEN is not a contract")); + roleScript.deploy(config); + } + + function testRejectsASnapshotEngineThatIsNotAContract() public { + DeployIncomeVault.Config memory config = _roleConfig(); + config.snapshotEngine = ISnapshotSource(address(0xBEEF)); + vm.expectRevert(bytes("DeployIncomeVault: SNAPSHOT_ENGINE is not a contract")); + roleScript.deploy(config); + } + + function testRejectsARuleEngineThatIsNotAContract() public { + DeployIncomeVault.Config memory config = _roleConfig(); + config.ruleEngine = IRuleEngine(address(0xBEEF)); + vm.expectRevert(bytes("DeployIncomeVault: RULE_ENGINE is set but is not a contract")); + roleScript.deploy(config); + } + + function testRejectsAZeroProxyAdmin() public { + DeployIncomeVault.Config memory config = _roleConfig(); + config.proxyAdmin = ZERO_ADDRESS; + vm.expectRevert(bytes("DeployIncomeVault: PROXY_ADMIN is zero")); + roleScript.deploy(config); + } + + function testRejectsAZeroAdmin() public { + DeployIncomeVault.Config memory config = _roleConfig(); + config.admin = ZERO_ADDRESS; + vm.expectRevert(bytes("DeployIncomeVault: VAULT_ADMIN is zero")); + roleScript.deploy(config); + } + + function testRejectsAZeroTimeLimit() public { + DeployIncomeVault.Config memory config = _roleConfig(); + config.timeLimitToWithdraw = 0; + vm.expectRevert(bytes("DeployIncomeVault: TIME_LIMIT_TO_WITHDRAW is zero")); + roleScript.deploy(config); + } + + function testRejectsAZeroOwnerOnTheOwnableVariant() public { + DeployIncomeVaultOwnable2Step.Config memory config = _ownerConfig(); + config.owner = ZERO_ADDRESS; + vm.expectRevert(bytes("DeployIncomeVaultOwnable2Step: VAULT_OWNER is zero")); + ownerScript.deploy(config); + } + + /** + * @notice A rule engine is optional and the zero address is accepted + */ + function testAZeroRuleEngineIsAccepted() public { + IncomeVault vault = roleScript.deploy(_roleConfig()); + assertEq(address(vault.ruleEngine()), ZERO_ADDRESS); + } +}