Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
e4ec03d
docs: add CLAUDE.md and AGENTS.md agent guide + install snapshot engine
rya-sge Aug 19, 2026
909c02f
docs: add CLAUDE.md and AGENTS.md agent guide + install snapshot engine
rya-sge Aug 19, 2026
59c6916
style: order functions per Solidity style guide and complete NatSpe…
rya-sge Aug 19, 2026
7d2940e
docs: rename doc/specification.md to doc/README.md and fix inbound links
rya-sge Aug 19, 2026
c3edf62
docs: merge doc/technical.md into doc/README.m
rya-sge Aug 19, 2026
c7c2b3e
docs: regenerate surya schema and UML, add IncomeVaultValidationModule
rya-sge Aug 19, 2026
6f304c3
build: bump sol2uml to 2.5.26 and regenerate the UML class diagram
rya-sge Aug 19, 2026
f656d58
refactor: replace __gap with ERC-7201 namespaced storage
rya-sge Aug 19, 2026
8119232
docs: forbid history-expanding characters in commit message guidance
rya-sge Aug 19, 2026
8ac09e8
refactor: move access control to authorization hooks and add an Owna…
rya-sge Aug 19, 2026
11d666f
docs: replace the global and segregated-deposit drawio images with …
rya-sge Aug 19, 2026
78cc3c8
docs: replace the claimDividend drawio image with a PlantUML activity…
rya-sge Aug 19, 2026
ff658ec
docs: replace the RuleEngine drawio image with a PlantUML diagram and…
rya-sge Aug 19, 2026
5479627
docs: add a high-level architecture diagram and reference it from bot…
rya-sge Aug 19, 2026
6ac6d8d
docs: add a code-quality review and apply its gas, event and virtual…
rya-sge Aug 19, 2026
200759f
fix: apply the claim window to distributeDividend
rya-sge Aug 19, 2026
8c65164
fix: apply the transfer restrictions to distributeDividend
rya-sge Aug 19, 2026
4060fac
refactor: type the snapshot source with a minimal ISnapshotSource int…
rya-sge Aug 19, 2026
e3d2804
docs: expand finding H-3 and document the vault-freeze capability
rya-sge Aug 19, 2026
553a4ca
docs: compare IncomeVault with ERC-4626 and ERC-7540 vaults
rya-sge Aug 19, 2026
df46f36
fix: reject a zero timeLimitToWithdraw
rya-sge Aug 19, 2026
e1f378e
feat: allow migrating the snapshot source while no claim period is open
rya-sge Aug 19, 2026
a359527
feat: add a best-effort distribution that skips refused holders
rya-sge Aug 19, 2026
270a18c
test: cover deactivateContract and raise branch coverage to 97.56%
rya-sge Aug 19, 2026
15ff5bb
test: add accounting invariants and hoist the shared vault deployment
rya-sge Aug 19, 2026
2a9c975
feat: add depositBatch for crediting several dividend times at once
rya-sge Aug 19, 2026
ee40187
fix: bound withdraw by what a period still holds, and expose the residue
rya-sge Aug 19, 2026
3b29218
feat: add ERC-7540-style claim delegation and bound payouts to their…
rya-sge Aug 19, 2026
8f7c1dc
feat: declare the ERC-7540 operator subset as its own interface
rya-sge Aug 19, 2026
d84cf2e
feat: add tested deployment scripts for both vault variants
rya-sge Aug 19, 2026
3f5978c
feat: add ERC-7741 signed operator authorisation
rya-sge Aug 19, 2026
9ee38c8
build: add a Makefile and route npm scripts and CI through it
rya-sge Aug 19, 2026
b195172
refactor: split the transfer-restriction question from its CMTAT answer
rya-sge Aug 20, 2026
5d9cff9
refactor: make the snapshot source a hook so a CMTAT can be its own
rya-sge Aug 20, 2026
dd0f856
ocs: record M-9 as already-satisfied and warn that the validation ini…
rya-sge Aug 20, 2026
08e63b4
feat: state the distribution API as IIncomeVault and let the compiler…
rya-sge Aug 20, 2026
1795a62
refactor: give claim delegation its own module and ERC-7201 namespace
rya-sge Aug 20, 2026
aff1e67
refactor: give src/ a layout that says what each file is
rya-sge Aug 20, 2026
8b520bd
refactor: make gasless support a deployment choice via IncomeVaultBa…
rya-sge Aug 20, 2026
57de19e
Add CMTAT standard comparison
rya-sge Aug 20, 2026
54a97b6
Update documentation
rya-sge Aug 20, 2026
5eb5d4a
docs: allow non-snapshot record dates and address ERC-4626 deposits …
rya-sge Aug 20, 2026
2867d98
docs: regenerate the Solidity API and make docgen write in place
rya-sge Aug 20, 2026
0169589
style: restore the ERC-7201 storage NatSpec and patch solidity-docg…
rya-sge Aug 20, 2026
265bac1
Add slither and aderyn report
rya-sge Aug 20, 2026
0161b53
docs: drop doc-path pointers from contract comments and replace [TOC…
rya-sge Aug 20, 2026
8c60570
Improve toc generator
rya-sge Aug 20, 2026
b61ef97
fix: report pause, deactivation and freeze through detectTransferRest…
rya-sge Aug 20, 2026
bfbf6ed
perf: extract the saturating remainder rule and read each period slot…
rya-sge Aug 21, 2026
db68f8c
refactor: make the core payout internals overridable and prove the g…
rya-sge Aug 21, 2026
8ca6f07
refactor: give the deposit write, its check and its event a single owner
rya-sge Aug 21, 2026
2a0dbc2
Change future release from 1.1.0 to 2.0.0 and remove file reference i…
rya-sge Aug 24, 2026
54df69f
docs: record what G-1 actually became — nine references removed, not two
rya-sge Aug 24, 2026
b98bd52
docs: correct and expand the analysis of paying dividends in the se…
rya-sge Aug 24, 2026
306722c
style: unwrap CHANGELOG.md to one line per block
rya-sge Aug 24, 2026
25cdbb4
Improve doc
rya-sge Aug 24, 2026
08de9d6
Fix Agents and claude files
rya-sge Aug 24, 2026
7877407
docs: mark the release as 2.0.0-rc0 while VERSION stays 2.0.0
rya-sge Aug 24, 2026
df14d6d
Remove coverage doc + build: fail the EIP-170 check when it measures…
rya-sge Aug 26, 2026
7997f25
docs: name the oversized mock in check_sizes.py and give its exit con…
rya-sge Aug 26, 2026
3a08f01
Run forge fmt
rya-sge Aug 26, 2026
fa7bfde
Run forge fmt
rya-sge Aug 26, 2026
0fe2822
docs: regenerate the Surya schema for the current src/ (6 -> 21 per…
rya-sge Aug 26, 2026
767350e
docs: drop references to the removed modularity document and fix thr…
rya-sge Aug 26, 2026
1f2ef1d
build: track doc/coverage and require it to be regenerated with any src
rya-sge Aug 31, 2026
9c0be39
Remove outdated file
rya-sge Aug 31, 2026
b15c4ca
docs: regenerate the API reference and drop the retracted coverage-do…
rya-sge Aug 31, 2026
eb0d4e2
docs: replace three dramatised phrasings in doc/README.md with plain …
rya-sge Aug 31, 2026
597f102
docs: disclose AI coding assistant use in both READMEs
rya-sge Aug 31, 2026
55597c0
docs: replace the README coverage section with the make targets and…
rya-sge Aug 31, 2026
aae5f42
fix(test): measure batch deposit gas with lastCallGas and stop assert…
rya-sge Aug 31, 2026
b5a9afc
Merge pull request #8 from CMTA/update
rya-sge Aug 31, 2026
7f212b6
docs: split the nine CMTAT Distribution amendments into their own file
rya-sge Aug 31, 2026
d8914b8
Add spec + docs: split the three Distribution additions into their …
rya-sge Aug 31, 2026
a01c153
docs: split the ERC-4626 deposit question into its own file with the…
rya-sge Aug 31, 2026
c657b0f
Update doc
rya-sge Aug 31, 2026
95e9d6d
Merge branch 'main' into dev
rya-sge Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
# `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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ broadcast/
lib/
out/
docOut/
lcov.info
cache/
.~lock.test.odt#
nethereum-gen.settings
Expand All @@ -14,3 +15,4 @@ cache_hardhat/
#drawio
*.bkp
*.dtmp
history
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
213 changes: 213 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

120 changes: 120 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

213 changes: 213 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
115 changes: 82 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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/' <report>` 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/)

Expand All @@ -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).
Expand All @@ -70,20 +103,37 @@ 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

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

```
Expand All @@ -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).
Loading
Loading