feat(EXSC-411): S3 — fee receivers & sweep distribution#1983
Draft
gvladika wants to merge 10 commits into
Draft
Conversation
Contributor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
b0e15d6 to
f83fbb7
Compare
Rebuild S3 on top of S2's accrual engine (dev-vault-wrapper <- S1 <- S2 <- S3). S2 owns accrual into undivided totals (accruedFeeAssets, accruedFeeShares); S3 adds the receiver config + a permissionless sweep that applies the integratorShareBps split at payout time over both reservoirs. - Receiver config (1..5 wallets, bps summing to 100%) set at initialize via a typed IntegratorReceivers param and mutable by the vaultWrapperAdmin; never emptiable, so an unconfigured-receivers state is impossible. - sweep() accrues pending management fees first, then distributes the idle-asset reservoir and the dilution-share reservoir in their native denominations (no adapter touch). Each split by integratorShareBps; integrator portion fanned across wallets (last absorbs the remainder); LI.FI paid to the factory's live lifiFeeRecipient. - A failing integrator transfer (e.g. a blacklisted wallet) is caught via a self-only trustedTransfer and redirected to LI.FI, so a single hostile wallet can never block the sweep. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeRabbit: a node_modules symlink with an absolute local path was accidentally staged via git add -A (it escaped the node_modules/ dir-pattern gitignore as a file). Untrack it; the on-disk symlink stays for local hooks.
…s.sol:48) CodeRabbit: 'bps summing to exactly 100%' reads ambiguously as [60,40]; clarify the denominator is 10_000.
…LiFiVaultWrapper.sol:193) CodeRabbit: the check rejects a 100% integrator share (>= 10000) so LI.FI always keeps a non-zero cut, but the doc said 'exceeds 100%'. Reword to match.
…rapperDistribution.t.sol:36,149) CodeRabbit: add a direct trustedTransfer external-caller revert (OnlySelf) test — the self-call guard is the main protection against arbitrary payout routing — and assert ReceiverBpsSumNot100 on the setIntegratorReceivers update path, not just at initialize.
9ef7a07 to
b9afb41
Compare
…-fee-split' into feature/exsc-411-s3-rebased-on-s2 # Conflicts: # src/VaultWrapper/LiFiVaultWrapper.sol # src/VaultWrapper/interfaces/ILiFiVaultWrapper.sol # test/solidity/VaultWrapper/LiFiVaultWrapperFees.t.sol
|
|
||
| /// @dev Integrator payout wallets (1..5), parallel to `_integratorReceiverBps`. Set at | ||
| /// `initialize` and mutable by the integrator; always non-empty after deploy. | ||
| address[] internal _integratorReceivers; |
…-fee-split' into feature/exsc-411-s3-fee-receivers-sweep-distribution
…-fee-split' into feature/exsc-411-s3-rebased-on-s2 # Conflicts: # src/VaultWrapper/LiFiVaultWrapper.sol
…-fee-split' into feature/exsc-411-s3-fee-receivers-sweep-distribution Brings in S2b (performance fee, per-fee-type split at accrual) and the S2 review fixes (up-only HWM re-anchor, saturating counters, overage booking, shared _splitFee, shared fee-test base). Port of S3 onto the split-at-accrual model (the semantic part of this merge): sweep no longer re-splits reservoirs by a single integratorShareBps at distribution — it pays out the four per-recipient counters (lifi/integrator x assets/shares) booked when each fee accrued. _distributeReservoir now takes the two tracked parts; the receiver fan-out and redirect-to-LI.FI behavior are unchanged. initialize takes uint16[4] splits plus the IntegratorReceivers set; the duplicate BPS_DENOMINATOR constant is dropped for LibVaultWrapperMath.BASIS_POINT_SCALE. Distribution tests assert against the tracked counters; the shared fee-test base gained _defaultReceivers() and all S2-side call sites pass the receiver set. The merged wrapper crosses solhint's max-states-count (16): scoped disable with justification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…paused The S2 base now includes the S5 pause module, so the ticket's 'sweep works while paused' behaviour is assertable — closes the deferral documented in the PR body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which Linear task belongs to this PR?
EXSC-411 — S3: Fee receivers & sweep distribution
Stacked on S2. The base of this PR is the S2 branch (#1993, the fee accrual engine — now including S2b's performance fee and per-fee-type split, and the S5 pause module via the merged lineage):
dev-vault-wrapper ← S1 ← S2 ← S3. The diff is only the S3 distribution layer; retarget todev-vault-wrapperonce S2 lands.Why did I implement it this way?
S2 splits every fee between LI.FI and the integrator the moment it accrues, into four per-recipient counters:
lifiFeeAssets/integratorFeeAssets(idle asset, held in the wrapper, excluded fromtotalAssets()) andlifiFeeShares/integratorFeeShares(dilution shares minted toaddress(this)). S3 is the payout layer: it folds receiver configuration and a permissionlesssweep()directly into the wrapper (S2 keeps all fee logic there, so no separate mixin), paying out exactly those tracked entitlements — nothing is re-split at distribution, so a later default-split change can never retroactively re-split already-earned fees.sweep()first calls_accrueFees()(so pending management/performance fees crystallize even while deposits are paused — tested), then pays out each reservoir in its own token: the idle asset as the asset, the dilution shares as wrapper shares. No adapter touch, so the sweep can never be blocked by underlying illiquidity. LI.FI's booked parts go to the factory's livelifiFeeRecipient()(an integrator can never redirect it); the integrator's booked parts are fanned across its 1..5 wallets by their bps, with the last wallet absorbing the integer-division remainder so the pool zeroes exactly.sweep(), never blockable. Each integrator transfer goes through a self-onlytrustedTransferwrapped intry/catch; a reverting wallet (e.g. a USDC-blacklisted address) has its share redirected to LI.FI in the same sweep instead of reverting. There is no separatesweepLifiFeesescape hatch — redirect makes one unnecessary. LI.FI's own transfer is not caught (a blacklisted LI.FI recipient is LI.FI's own, factory-governed, concern).initializevia a typedIntegratorReceiversdeploy param (1..5 non-zero wallets, bps summing to exactly 100%); thesetIntegratorReceiversmutator (gated onvaultWrapperAdmin) re-validates, so an empty/unconfigured receiver set is structurally impossible — no griefing window where a permissionless sweep could divert unconfigured integrator fees.__gap(uint256[46]); the subsystem is pre-release and the layout freezes before first deployment.sweep()zeroes all four counters before any transfer (CEI) and isnonReentrant;trustedTransferis self-only and deliberately notnonReentrantso the self-call doesn't trip the guard. If a counter ever saturated (S2's uint128 clamp), the sweep pays the tracked amount — the excess stays in the wrapper rather than reverting anything.Tests drive real accrual (deposit/withdraw + time warp through S2's engine), not seeded pools: payout equals the at-accrual tracked parts across both reservoirs, 1..5-wallet fan-out with last-receiver remainder, the blacklist→redirect-to-LI.FI path, permissionless sweep, live-recipient reads, sweep-crystallizes-pending-management, sweep accrues and pays out while deposits are paused (the ticket behaviour previously deferred to S5, now in the base), zero-split books everything to LI.FI, and the init/mutator validation reverts.
/pr-ready (local CodeRabbit) findings
max-states-count— scoped disable with justification (single beacon-proxy storage host; mixins deliberately rejected).Checklist before requesting a review
/pr-ready(local CodeRabbit) on this branch and resolved (or explicitly documented) all findings — see.agents/commands/pr-ready.mdChecklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)