The IncomeVault is a prototype to perform coupon-payment dividend with a token supporting on-chain snapshots, typically a CMTAT bound to a SnapshotEngine.
Diagram source: doc/schema/plantuml/incomevault-architecture.puml. The detailed step-by-step flow is in doc/README.md.
This project has not undergone an audit and is provided as-is without any warranties.
The dividends are deposited in a Vault. Once the claims are open, a token holder can then perform a claim to get his dividends for a given period.
Currently, the vault supports only dividend under the form of another ERC-20 and it is suitable for the following use-case:
- 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)
The IncomeVault is not an ERC-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 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.
- The dividends can be paid with ERC-20 tokens as described in the ERC-20 specification
- The shares used to compute the dividends part are read through the interface
ISnapshotSource(src/interfaces/ISnapshotSource.sol), a strict subset ofISnapshotStateas defined in the SnapshotEngine repository. It declares only the three functions the vault calls, so anyISnapshotStateimplementation 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
SnapshotEnginebound 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.
The contracts are NOT audited, do not use them for production without auditing them !
Static analysis is run with Slither and 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.
| Version | Tool | Result | Report | Triage |
|---|---|---|---|---|
| v2.0.0 | Slither 0.11.5 | 0 High · 5 Med · 6 Low · 23 Info — nothing to fix | report | feedback |
| v2.0.0 | Aderyn 0.6.5 | 0 High · 10 Low — nothing to fix | report | feedback |
| v1.0.0 | Slither | superseded — predates the CMTAT v3 migration | report | — |
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.mdBoth 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.
Here a summary of the main documentation
| Document | Link/Files |
|---|---|
| Specification & technical choice | doc/README.md |
| Solidity API (docgen) | doc/solidityAPI/index.md |
| Toolchain | doc/TOOLCHAIN.md |
| Surya report | doc/surya |
See also Taurus - Equity Tokenization: How to Pay Dividend On-Chain Using CMTAT
The project is developed with Foundry
You must first initialize the submodules, with
git submodule update --init --recursive
The upgrade safety validation performed by the OpenZeppelin Foundry Upgrades plugin requires @openzeppelin/upgrades-core:
npm install
See also the command's documentation.
Later you can update all the submodules with:
forge update
See also the command's documentation.
The official documentation is available in the Foundry website
forge build --contracts src/deployment/IncomeVault.sol
You can run the tests with
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 --ffistraight after editing a contract therefore fails every test withFailed to run upgrade safety validation: … Build info file … is not from a full compilation, which names neither the cause nor the fix.make testdoes the full build first. (--ffiis 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
forge test --match-contract <contract name> --match-test <function name>
See also the test framework's official documentation, and that of the test commands.
make coverage # summary table in the terminal
make coverage-report # HTML in doc/coverage, needs lcov + genhtml
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 --exclude-tests --no-match-coverage '(test|mocks?|script)/'
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.
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 & Foundry forge coverage
Parts of this project were written with the help of AI coding assistants, principally Claude Code (Anthropic).
