feat: add SUSD3Fund (staked USD3 integration)#198
Closed
maximebrugel wants to merge 9 commits into
Closed
Conversation
Wraps the sUSD3 subordinate tranche (staked USD3) behind the IFund state machine. The fund's asset is USDC, but sUSD3 stakes USD3, so deposits convert USDC -> USD3 -> sUSD3 and redemptions reverse it (double ERC-4626 conversion). - Deposits are synchronous (sUSD3 lockDuration is 0); a non-zero lock is rejected at commit with DepositLockActive(). - Redemptions are cooldown-gated (~30d): commit starts the sUSD3 cooldown and unlock claims once sUSD3.maxRedeem() > 0, with partial-fill support. - Operator-abortable recovery: cancelRedeem() cancels the cooldown (PROCESSING -> RECOVERING) and recover() re-wraps the tracked remainder back to the receiver -- the escape for a blocked/backing-limited redeem and any floor-rounding dust. - One shared wsUSD3 WrappedAsset backs all instances; deploy multiple funds to run concurrent settlements. Adds SUSD3Fund + SUSD3FundFactory (beacon proxy), ISUSD3Fund / ISUSD3 / IUSD3 interfaces, BelowMinDeposit + DepositLockActive errors, ERC4626 sUSD3/USD3 mocks, and unit/factory/fuzz/invariant/mainnet-fork tests. Documented in README.
|
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: defaults 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 |
The sUSD3 deposit lock (lockDuration) was active (30d) for essentially all of sUSD3's history and was only swapped to the withdrawal cooldown ~3 weeks ago; it is governance-controlled and could be re-enabled. Instead of rejecting a deposit while a lock is active, the fund now stakes synchronously in commit() and defers delivery of the wrapped shares until the per-account sUSD3.lockedUntil elapses (state() gates DEPOSIT -> UNLOCKING on it). Gating on lockedUntil (not lockDuration) keeps an in-flight deposit robust to mid-flight config changes. - Drop the DepositLockActive() commit guard and the now-unused error. - Add ISUSD3.lockedUntil(address). - Tests: mock-based async locked-deposit lifecycle, plus a fork test that re-enables the real lock via ProtocolConfig and verifies deferred delivery.
USD3 has no lock and its commitment-period transfer guard exempts staking into sUSD3, so the received USD3 is always immediately stakeable; only sUSD3's lock requires deferred delivery. Records the invariant where commit() stakes.
maxencerb
requested changes
Jul 6, 2026
maxencerb
reviewed
Jul 6, 2026
Mirror ParetoFund's resolve pattern: state() now gates DEPOSIT delivery on `depositReceived >= (hasResolvedAmounts ? resolvedOutput : order.output)` (in addition to the sUSD3 lock check), so a deposit that yields fewer sUSD3 shares than requested (e.g. an adverse rate move between create and commit) stays in PROCESSING. An OPERATOR_ROLE/owner `resolve(order, input, output)` lowers the threshold to unstick it. - Add hasResolvedAmounts/resolvedOutput storage (reset in create()). - Add resolve() + OrderResolved event to ISUSD3Fund. - Tests: stuck-deposit -> resolve -> unlock, not-processing revert, access control.
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.
Summary
Adds
SUSD3Fund— a new fund wrapping the sUSD3 subordinate tranche (3Jane's staked USD3) behind theIFundstate machine. The fund's accounting asset is USDC, but sUSD3 stakes USD3, which itself wraps USDC — so the fund performs a double ERC-4626 conversion (USDC → USD3 → sUSD3on deposit, and the reverse on redeem).Modeled on
ParetoFund(synchronous deposit / gated-async redeem), withCentrifugeFund's operator-abort + partial-unlock patterns for recovery. Multiple instances are deployed to run concurrent settlements (one active order each), all backed by a single sharedwsUSD3WrappedAsset.Design
sUSD3.lockDuration() == 0):commit()stakes atomically (measured by balance delta),unlock()delivers the wrapped shares. A non-zero lock is hard-rejected at commit withDepositLockActive()(deposits have no recovery path).commit()callssUSD3.startCooldown();unlock()becomes available oncesUSD3.maxRedeem(this) > 0(cooldown matured and not blocked by unrealized losses / backing floor). Partial-fill aware — redeems as much as sUSD3 allows and returns toPROCESSINGfor the remainder.cancelRedeem()cancels the cooldown (PROCESSING → RECOVERING);recover()re-wraps the trackedpendingRedeemSharesback to the receiver. This is the guaranteed escape for a stuck/backing-limited redeem, and clears any sub-wei floor-rounding residual. Only REDEEM orders recover.USD3.minDeposit()(BelowMinDepositincreate()); chained-conversion slippage guard; decimals sanity check on init.totalAssets()reports wrapper-wide aggregate AUM (shared wrapper caveat, as with the other funds).Files
src/funds/susd3/SUSD3Fund.sol,SUSD3FundFactory.sol(beacon proxy)src/interfaces/funds/susd3/ISUSD3Fund.sol,src/interfaces/integrations/susd3/{ISUSD3,IUSD3}.solsrc/libs/funds/LibFundsErrors.sol— addsBelowMinDeposit(),DepositLockActive()test/funds/susd3/andtest/mock/funds/susd3/### sUSD3 (Staked USD3) Integration+SUSD3FundFactoryin the factory listTesting
45 sUSD3 tests, all green — 27 unit + 9 factory + 3 fuzz (256 runs) + 2 invariants (256×64) + 4 mainnet-fork.
25_440_000(a recent block where the 30-day cooldown is active; the24_570_780block reused by the other funds' fork tests predates it).LibFundsErrorsedit is additive).forge build --sizes:SUSD3Fund= 17,426 B (7.1 KB under the limit);forge fmt/forge lintclean on the new code.Notes for reviewers / deployment
wsUSD3WrappedAssetowner must grant each new fundISSUER_ROLE. Don't revoke it mid-order.lockDurationand USD3minCommitmentTimestay0(a lock is hard-rejected at commit; a commitment change is a documented assumption).sUSD3/reference protocol source (used only for context) is intentionally not included in this PR.