Add OUSD V3 and OETHb migration contracts#2909
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2909 +/- ##
==========================================
+ Coverage 44.63% 50.57% +5.94%
==========================================
Files 110 123 +13
Lines 4920 5811 +891
Branches 1362 1641 +279
==========================================
+ Hits 2196 2939 +743
- Misses 2721 2869 +148
Partials 3 3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Moved the adapter participants close to the Bridge
naddison36
left a comment
There was a problem hiding this comment.
The next batch review comments
| } | ||
|
|
||
| function _opportunisticClaim() internal { | ||
| uint256 stored = outstandingRequestId; |
There was a problem hiding this comment.
nit: its not clear what store is later in the function code. I would use outstandingRequestIdMem instead
| return; | ||
| } | ||
| // `outstandingRequestId` stores the vault id verbatim. | ||
| uint256 vaultRequestId = stored; |
There was a problem hiding this comment.
Why create a new memory variable for what's stored in outstandingRequestId?
There was a problem hiding this comment.
Thanks, cleaned it up in this commit: 82f0179
| // auto-deposits once enough accumulates. A revert here would DoS the mint -> | ||
| // `_allocate` -> `deposit` path. Covers both `deposit` and `depositAll`. | ||
| if (_amount < IBridgeAdapter(outboundAdapter).minTransferAmount()) { | ||
| return; |
There was a problem hiding this comment.
Feels like we should emit an event here since we are not doing the requested deposit while not failing the tx
There was a problem hiding this comment.
There's a new DepositSkipped event being emitted now: 82f0179
| } | ||
|
|
||
| /// @inheritdoc IAny2EVMMessageReceiver | ||
| function ccipReceive(Client.Any2EVMMessage calldata message) |
There was a problem hiding this comment.
What guarantees, if any, does the CCIP Distributed Oracle Network (DON) provide for calling ccipReceive?
What if no DON participant calls ccipReceive? Can we retrigger the CCIP message?
From what I can tell, if the DON does not call ccipReceive then our strategy is stuck.
There was a problem hiding this comment.
If for someone the CCIP message relaying fails, anyone can manually trigger it again: https://docs.chain.link/ccip/concepts/manual-execution
I'll add it as a comment though
| } | ||
|
|
||
| uint64 nonce = _getNextYieldNonce(); | ||
| pendingDepositAmount = _amount; |
There was a problem hiding this comment.
nit: I like to make it clear when storage variables are writing back to storage. I'd add a comment like
// Store the pending deposit amount until the remote deposit is acknowledged.
| ); | ||
|
|
||
| uint64 nonce = _getNextYieldNonce(); | ||
| pendingWithdrawalAmount = _amount; |
There was a problem hiding this comment.
nit: I like to make it clear when storage variables are writing back to storage. I'd add a comment like
// Store the pending withdrawal amount until the remote withdrawal is claimed.
| function _processDepositAck(uint64 nonce, bytes memory payload) internal { | ||
| _markYieldNonceProcessed(nonce); | ||
| uint256 yieldBaseline = CrossChainV3Helper.decodeUint256(payload); | ||
| remoteStrategyBalance = yieldBaseline; |
There was a problem hiding this comment.
nit: I'd add a comment to make it clear this is a write to storage. eg
// Store the acknowledged deposit in the remote balance and clear the pending amount.
remoteStrategyBalance = yieldBaseline;
pendingDepositAmount = 0;
| `CCTPAdapter._quoteFee` calls `tokenMessenger.getMinFeeAmount(amount)`, | ||
| which is V2.1-only. If a chain has only V2.0 deployed, `quoteFee(amount > 0)` | ||
| reverts. Current deploys (OETHb) don't use CCTP at all, so this is a | ||
| non-issue. OUSD V3 spoke chains must be on V2.1 — check before deploying. |
There was a problem hiding this comment.
A link to where to check what CCTP version is deployed on each chain would be useful
| * | ||
| * Reverts when the chosen source doesn't cover `fee`. | ||
| */ | ||
| library NativeFeeHelper { |
There was a problem hiding this comment.
nit: There's only one function in this library. Feels like it'd be simpler if consume was moved to the BridgedWOETHMigrationStrategy strategy as an internal function.
There was a problem hiding this comment.
Removed it and made it an internal function: 82f0179
| function bridgeToRemote(uint256 _amount) | ||
| external | ||
| payable | ||
| onlyOperatorGovernorOrStrategist |
There was a problem hiding this comment.
it still feels like migrating over 15m USD worth of ETH should have some operational checks. Setting up an automated Talos Action without human checks doesn't feel right.
Unless there are some risks with partially migrated funds. Does migrating the funds ASAP mean less risk of an accounting error?
| function bridgeToRemote(uint256 _amount) | ||
| external | ||
| payable | ||
| onlyOperatorGovernorOrStrategist |
There was a problem hiding this comment.
The Operator is set to the Strategist in the deploy script so lets just use onlyGovernorOrStrategist.
// 3. Authorise the multichain strategist as the operator for `bridgeToRemote`.
{
contract: cMigration,
signature: "setOperator(address)",
args: [addresses.multichainStrategist],
sparrowDom
left a comment
There was a problem hiding this comment.
Minor comments otherwise LGTM
| /// must equal the vault (enforced by the require below); Master always forwards the | ||
| /// received bridgeAsset to `vaultAddress` on the leg-2 ack. | ||
| /// | ||
| /// Only the `remoteStrategyBalance` slice is drawable here: `_amount` must be |
There was a problem hiding this comment.
Proposed change of this section:
/// Drawable bound is `_drawableRemoteBalance()` = `remoteStrategyBalance +
/// min(0, bridgeAdjustment)`, NOT `remoteStrategyBalance` alone:
/// - a NEGATIVE `bridgeAdjustment` (net BRIDGE_OUT) is folded in, lowering the cap so we
/// never request more shares than Remote can actually unwrap (else it NACKs on Remote);
/// - a POSITIVE `bridgeAdjustment` is excluded here even though `checkBalance` counts it
/// (that + local bridgeAsset can report more). To realise it, `requestSettlement()`
/// first (folds bridgeAdjustment into remoteStrategyBalance) and/or use the local
/// bridgeAsset, then withdraw.
| /// on a WITHDRAW_CLAIM_ACK (the bridgeAsset is still held when this is computed). | ||
| /// `oTokenAmount` is in OToken (18dp) units, matching `_viewCheckBalance`; | ||
| /// `_yieldOnlyBaseline()` is the `oTokenAmount == 0` case. | ||
| /// @dev Clamps to 0 rather than reverting on a negative. `_viewCheckBalance - bridgeAdjustment` |
There was a problem hiding this comment.
suggested fix:
/// @dev Clamps to 0 rather than reverting on a negative. `_viewCheckBalance - bridgeAdjustment`
/// is principal + yield + retained fees and is never *economically* negative **while
/// (w)OTokens are up-only** — the only thing that pushes it a few wei negative is the
/// wOToken 4626 rounding against the strategy (~1 wei per BRIDGE_IN floor / BRIDGE_OUT
/// ceil) once a `withdrawAll` drains it near 0. Reverting on that dust would freeze the
/// serialized yield channel, so we clamp (matches checkBalance-never-reverts).
/// CAVEAT: a real negative rebase (loss/slashing) would make this genuinely negative; the
/// clamp then masks it, and because Master re-adds its OWN `bridgeAdjustment`,
/// `checkBalance` OVER-reports by ~`bridgeAdjustment` (not 0). Out of scope while
/// (w)OTokens never negative-rebase; revisit (signed baseline) if that changes. See DESIGN §3.10.
| leg-2 claim ack lands. | ||
|
|
||
| For `withdrawAll` (vault or governor sweep), `_withdrawRequest` is called with | ||
| `min(remoteStrategyBalance, inboundAdapter.maxTransferAmount())` so a sweep |
There was a problem hiding this comment.
this should be
`min(_drawableRemoteBalance(), inboundAdapter.maxTransferAmount())` so a sweep
| Master->>Master: _processWithdrawClaimAck success:<br/>_markYieldNonceProcessed(N+2)<br/>pendingWithdrawalAmount = 0<br/>remoteStrategyBalance = yieldBaseline | ||
| Master->>Vault: «WETH» transfer (forwards its full bridgeAsset balance) | ||
| Note over Master: safeTransfer(vaultAddress, balanceOf(this))<br/>emit Withdrawal(WETH, WETH, claimed) | ||
| else queue not yet matured (NACK) |
There was a problem hiding this comment.
proposed change:
Leg-2 NACKs on four conditions, not just an immature queue:
else NACK — outstandingRequestId != EMPTY / amount == 0 / bridgeAssetHeld < amount / ship out of [min,max]
| ) { | ||
| return; | ||
| } | ||
| // Best-effort: a mid-migration cleared inbound adapter must no-op the sweep, not |
There was a problem hiding this comment.
Proposed change:
// Best-effort no-op if the inbound adapter isn't wired yet (pre-configuration only —
// setInboundAdapter now rejects zero, so it can't be cleared mid-migration).
Summary
OUSD V3 cross-chain strategy pair (Master/Remote) with bridge-agnostic adapter family (CCIP, CCTP V2, Superbridge), plus Sepolia ⇄ Base Sepolia testnet harness. Foundation for OETHb Phase 1 migration and future OUSD V3 L2 deployment rollouts.
Changes