Skip to content

Repository files navigation

IncomeVault

The IncomeVault is a prototype to perform coupon-payment dividend with a token supporting on-chain snapshots, typically a CMTAT bound to a SnapshotEngine.

IncomeVault architecture

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.

Introduction

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.

Compatibility

  • 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 of ISnapshotState as defined in the 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 !

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.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 & 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

Foundry

The project is developed with Foundry

Initialization

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.

Compilation

The official documentation is available in the Foundry website

 forge build --contracts src/deployment/IncomeVault.sol

Testing

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 --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

forge test --match-contract <contract name> --match-test <function name>

See also the test framework's official documentation, and that of the test commands.

Coverage

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

Tooling

Parts of this project were written with the help of AI coding assistants, principally Claude Code (Anthropic).

About

Solidity contracts to perform coupon payment on-chain

Topics

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Contributors

Languages