Skip to content

Commit e9ede2e

Browse files
Docs: expand CFA v2.1 into professional spec
- Purpose/scope, definitions, dependencies, state model, invariants, resolution - Triggers, placeholder policy, lifecycle flows with precise steps - Rounding/safety/observability/failure modes/implementation notes/open items Test plan: - Render docs; verify sections and step details
1 parent 18fd8c1 commit e9ede2e

File tree

1 file changed

+130
-45
lines changed

1 file changed

+130
-45
lines changed

docs/rewards-aggregator-erc4626.md

Lines changed: 130 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -59,51 +59,136 @@ All routed targets MUST satisfy:
5959
- `assets += IERC4626(v).convertToAssets(IERC4626(v).balanceOf(address(this)))`
6060
- This is a view-only iteration. State-changing flows remain single-vault without loops.
6161

62-
## CFA streaming integration (v2.1) — Superfluid “flow” helper
63-
Status: planned in v2.1 (docs-first; implementation next).
64-
65-
Goal
66-
- Stream SENDx (SuperToken) per user at a rate proportional to their current value in the aggregator.
67-
- Use Superfluid’s SuperTokenV1Library `flow(ISuperToken token, address receiver, int96 rate)` helper to create/update/delete flows.
68-
69-
Key concepts
70-
- Token: `sendx` (constructor arg), an ISuperToken. The aggregator must be pre-funded with SENDx to cover the flow buffer.
71-
- Triggers: Recompute a user’s flow after each state-changing event that affects the user’s value:
72-
- deposit(receiver)
73-
- withdraw(owner)
74-
- transfer(from→to): recompute for both parties after proportional re-attribution
75-
- Value basis (per user): sum across the user’s active vaults of `convertToAssets(_userUnderlyingShares[user][vault])`.
76-
- Policy params:
77-
- `annualRateBps`: annualized rate in basis points (e.g., 300 = 3%)
78-
- `secondsPerYear`: denominator to convert annual to per-second
79-
- `exchangeRateWad`: asset→SENDx conversion (fixed-point); default 1e18 for 1:1
80-
- Flow math (per second):
81-
- `perSecond = floor( (sumAssets * exchangeRateWad) * annualRateBps / 10_000 / secondsPerYear / 1e18 )`
82-
- Use `int96` cast; if perSecond==0, call `flow(token, user, 0)` to delete.
83-
84-
Integration outline (pseudocode)
85-
- Import library: `using SuperTokenV1Library for ISuperToken;`
86-
- After event, recompute and set flow:
87-
```
88-
function _updateFlow(address user) internal {
89-
uint256 assets = 0;
90-
for (address v in _userActiveVaults[user]) {
91-
uint256 uShares = _userUnderlyingShares[user][v];
92-
if (uShares == 0) continue;
93-
assets += IERC4626(v).convertToAssets(uShares);
94-
}
95-
uint256 wad = assets * exchangeRateWad;
96-
uint256 annual = wad * annualRateBps / 10_000;
97-
uint256 perSec = annual / secondsPerYear / 1e18;
98-
int96 rate = int96(int256(perSec));
99-
sendx.flow(user, rate); // create/update/delete as needed
100-
}
101-
```
102-
103-
Operational notes
104-
- Pre-fund with SENDx to satisfy CFA buffer or the flow creation will revert.
105-
- Flows are per-user, from the aggregator to the user.
106-
- If you change `annualRateBps`/`exchangeRateWad`, you may optionally batch-recompute flows for a set of users (future helper).
62+
## CFA streaming integration (v2.1) — Professional spec
63+
64+
Status: planned (docs-first); implementation follows. The flowRate calculation component is not finalized; all calls to `flow` are placeholders pending that integration.
65+
66+
### Purpose and scope
67+
- Provide continuous SENDx streaming to users sized by the value of their aggregated SendEarn positions held via the aggregator.
68+
- Integrate Superfluid using SuperTokenV1Library `flow` helper to create/update/delete flows on state changes.
69+
- Scope covers deposit (vault token ingestion), withdraw, and redeem. ERC20 share transfers do not alter ledger or flows.
70+
71+
### Definitions
72+
- Vault: a SendEarn ERC4626 vault approved by the factory.
73+
- Vault shares: ERC4626 shares of a SendEarn vault (seASSET tokens) held by the aggregator.
74+
- Aggregated assets (per user): sum over user’s vaults of `IERC4626(v).convertToAssets(userUnderlyingShares[v])`.
75+
- sendx: the SuperToken used for streaming (constructor arg). Must be pre-funded in the aggregator.
76+
77+
### External dependencies
78+
- Superfluid protocol; use `SuperTokenV1Library` for `flow(ISuperToken token, address receiver, int96 rate)`.
79+
- SendEarnFactory (gating): `isSendEarn(vault)`; `SEND_EARN()` default vault.
80+
- ERC4626 interface: `convertToAssets(shares)`, `convertToShares(assets)`.
81+
82+
### State model (relevant to streaming)
83+
- Per-user, per-vault underlying shares:
84+
- `_userUnderlyingShares[user][vault] -> uint256`
85+
- Wrapper-wide active vaults (for view-only aggregation):
86+
- `_activeVaults[]` and `_isActiveVault[vault]`
87+
- Note: we do not modify per-user ledgers on ERC20 transfers. Flows/ledgers are updated only on deposit/withdraw/redeem.
88+
89+
### Invariants
90+
- Vault gating: `factory.isSendEarn(vault) == true` before accepting any vault shares.
91+
- Asset invariant: `IERC4626(vault).asset() == asset()` of the aggregator.
92+
- Single-vault mutations: withdraw/redeem operate on a single, resolved vault per action; no multi-vault loops.
93+
- No ledger changes on ERC20 share transfers.
94+
95+
### Resolution policy
96+
- For any action on behalf of `account`: resolve `vault = factory.affiliates(account)` if non-zero; else `factory.SEND_EARN()`.
97+
- Affiliate changes affect future actions only; existing per-user vault share ledgers are not migrated.
98+
99+
### Flows and triggers
100+
- Trigger `_recomputeAndFlow(user)` after:
101+
- Deposit (vault token ingestion)
102+
- Withdraw (assets)
103+
- Redeem (shares)
104+
- Transfers DO NOT trigger flow updates.
105+
106+
### Flow rate policy (placeholder)
107+
- `flowRate = f(aggregatedAssets(user), policy)` where:
108+
- `aggregatedAssets(user) = Σ_v convertToAssets(_userUnderlyingShares[user][v])`
109+
- Policy inputs (configurable): `annualRateBps`, `secondsPerYear`, `exchangeRateWad`
110+
- Compute per-second rate (placeholder):
111+
- `valueWad = aggregatedAssets * exchangeRateWad`
112+
- `annualWad = valueWad * annualRateBps / 10_000`
113+
- `perSecond = floor(annualWad / secondsPerYear / 1e18)`
114+
- `rate = int96(perSecond)`; if `rate == 0`, delete flow
115+
- NOTE: Final policy/oracle component to be integrated; until then, treat `flow()` invocations as stubs.
116+
117+
### Lifecycle flows
118+
119+
#### Deposit (vault token ingestion)
120+
1) User transfers SendEarn vault shares to SendEarnRewards (e.g., `depositVaultShares(vault, shares)`).
121+
2) Validate: `factory.isSendEarn(vault)` and `IERC4626(vault).asset() == asset()`.
122+
3) Compute assets: `assets = IERC4626(vault).convertToAssets(shares)` (beware rounding; prefer protocol’s conversion semantics).
123+
4) Ledger updates:
124+
- `_userUnderlyingShares[user][vault] += shares` (store underlying shares to preserve precise value accrual semantics)
125+
- Optionally maintain `totalAssetsByUser[user]` in view-only helpers by summing conversions on demand.
126+
5) Track wrapper-wide vault activity: add `vault` to `_activeVaults` if first use.
127+
6) Stream update (placeholder): call `sendx.flow(user, flowRate)`.
128+
7) Events: `Deposited(user, vault, assets, shares)`.
129+
130+
#### Withdraw (assets)
131+
1) Resolve vault; compute required shares: `shares = IERC4626(vault).previewWithdraw(assets)`.
132+
2) Verify user ledger has at least `shares` recorded; redeem from `vault` to this contract; send assets to `receiver=user`.
133+
3) Ledger updates: `_userUnderlyingShares[user][vault] -= shares`.
134+
4) Stream update (placeholder): call `sendx.flow(user, flowRate)`.
135+
5) Events: `Withdrawn(user, vault, assets, shares)`.
136+
137+
#### Redeem (shares)
138+
1) Resolve vault and redeem directly in shares path.
139+
2) Convert shares→assets via `IERC4626(vault).redeem(shares, this, this)` then send assets to `receiver=user`.
140+
3) Ledger updates and stream update as in Withdraw.
141+
142+
### Rounding and decimals
143+
- Use ERC4626 preview functions for forward-looking conversions (`previewWithdraw`, `previewRedeem`).
144+
- When converting vault shares to assets for aggregation, use `convertToAssets(shares)`; rounding follows the vault’s ERC4626 implementation.
145+
146+
### Reentrancy and safety
147+
- Wrap public entry points with `nonReentrant`.
148+
- Use `forceApprove` (reset-to-zero then set) for ERC20 approvals.
149+
- Never loop across multiple vaults in state-changing flows.
150+
151+
### Observability
152+
- Events: `Deposited(user, vault, assets, underlyingShares)`, `Withdrawn(user, vault, assets, underlyingShares)`.
153+
- Views: `userUnderlyingShares(user, vault)`, `totalAssets()` (wrapper-wide; view-only over `_activeVaults`).
154+
155+
### Failure modes
156+
- Not SendEarn vault: revert.
157+
- Asset mismatch: revert.
158+
- Insufficient underlying shares on Withdraw/Redeem: revert.
159+
- Flow creation/update may revert if SENDx buffer is insufficient (operator choice: pre-fund or skip flow set).
160+
161+
### Implementation notes
162+
- Keep vault interaction minimal and single-target per action.
163+
- Flow update hooks are invoked after ledger mutation for the acting user.
164+
- Do not attempt to adjust flows on ERC20 transfers.
165+
166+
### Open items
167+
- Plug in final flowRate component (oracle/policy); unit test flow lifecycle after integration.
168+
- Optional: on admin policy change, batch-recompute flows across a given user subset.
169+
Status: planned in v2.1 (docs-first; implementation next). Note: we do not yet have the final flowRate calculation component; calls to `flow` should be treated as placeholders until that piece is finalized.
170+
171+
Library
172+
- Use Superfluid’s SuperTokenV1Library `flow(ISuperToken token, address receiver, int96 rate)` to create/update/delete flows.
173+
174+
Deposit (vault token ingestion)
175+
1) User deposits a SendEarn vault token (shares) into SendEarnRewards.
176+
2) SendEarnRewards checks `factory.isSendEarn(vault)`.
177+
3) SendEarnRewards computes assets for the vault: `assets = IERC4626(vault).convertToAssets(shares)`.
178+
4) SendEarnRewards updates its internal mappings linking vaults ⇄ users ⇄ assets (e.g., `assetsByVault[user][vault] += assets`, `totalAssetsByUser[user] += assets`).
179+
5) SendEarnRewards calls `sendx.flow(user, flowRate)` via the library to reflect the new aggregated assets (flowRate: TODO — pending final component).
180+
181+
Withdraw
182+
1) SendEarnRewards withdraws assets held by the SendEarn underlying vault for the caller (redeeming vault shares it holds on behalf of the user). Receiver is the user linked to the vault.
183+
2) SendEarnRewards updates mappings (decrement the user’s assets for that vault and total assets).
184+
3) SendEarnRewards calls `sendx.flow(user, flowRate)` to reflect the reduced aggregated assets (flowRate: TODO — pending final component).
185+
186+
Redeem
187+
- Same as withdraw but the entry uses shares (wrapper redeem path should convert shares → assets and follow the same mapping + flow update sequence).
188+
189+
Notes
190+
- Exact `flowRate` computation is intentionally left as a TODO. We will integrate the final component (oracle/policy) to determine `flowRate` from the user’s aggregated assets.
191+
- This flow does not rely on re-attributing underlying shares during ERC20 transfers. Flows and mappings update on deposit/withdraw/redeem only.
107192

108193
References
109194
- SuperTokenV1Library: https://github.com/superfluid-finance/protocol-monorepo/blob/dev/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol

0 commit comments

Comments
 (0)