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.49% +5.85%
==========================================
Files 110 123 +13
Lines 4920 5807 +887
Branches 1362 1637 +275
==========================================
+ Hits 2196 2932 +736
- Misses 2721 2872 +151
Partials 3 3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
sparrowDom
left a comment
There was a problem hiding this comment.
Thanks for providing the fixes for previous comments. Some more comments inline:
| uint32 msgType, | ||
| uint256 amount, | ||
| bytes memory body | ||
| ) internal nonReentrant { |
There was a problem hiding this comment.
🟡 we don't have a test for Reentrency. This would be pretty convenient to have.
| // the fee; the pool is NOT consulted. Security gate: stops an | ||
| // attacker draining the operator-funded pool by spamming bridge | ||
| // in/out with msg.value = 0. | ||
| // userFunded = false → operator/protocol-funded sends (yield deposits/withdraws/claims |
There was a problem hiding this comment.
⚪ Would be cool to add a monitoring section to this PR where we should probably add a capability to Talos to observe the balances of fee assets on contracts. The remote contract sometimes needs to pay the bridging fee to convey the message. It can be blocked if it fails
| /// vault, which would be indistinguishable from "no request" if stored verbatim; | ||
| /// the +1 offset keeps `0` meaning "no outstanding (unclaimed) queue request" while | ||
| /// a real id of 0 is safely represented as 1. Cleared to 0 once the claim lands. | ||
| uint256 public outstandingRequestId; |
There was a problem hiding this comment.
nit: another way to approach this would be to set a const REQUEST_ID_EMPTY = type(uint256).max;
And set it to that whenever you want to treat it as empty
There was a problem hiding this comment.
That's a great suggestion, will do it.
| /// 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.
nit: update this docs to reflect the changes where not only remoteStrategyBalance is taken into account
| Remote->>wOETH: deposit(OETH balance, Remote) | ||
| Remote->>wOETH: «OETH X» (wrapper pulls OETH on deposit) | ||
| wOETH-->>Remote: «wOETH shares» minted | ||
| Remote->>Remote: yieldBaseline = _viewCheckBalance() |
There was a problem hiding this comment.
_viewCheckBalance() − bridgeAdjustment
| Remote->>wOUSD: deposit(OUSD balance, Remote) | ||
| Remote->>wOUSD: «OUSD» (wrapper pulls on deposit) | ||
| wOUSD-->>Remote: «wOUSD shares» minted | ||
| Remote->>Remote: yieldBaseline = _viewCheckBalance() |
There was a problem hiding this comment.
_viewCheckBalance() − bridgeAdjustment
| Note over Remote: outstandingRequestId = requestId<br/>outstandingRequestAmount = amount | ||
|
|
||
| Note over Master,Remote: ─── Phase B: Remote sends WITHDRAW_REQUEST_ACK ─── | ||
| Remote->>Remote: yieldBaseline = _viewCheckBalance() |
There was a problem hiding this comment.
_viewCheckBalance() − bridgeAdjustment
| intermediate state; value lives in exactly one slot per row, and `checkBalance` | ||
| equals the total in every row. | ||
|
|
||
| | State | wOETH share value | OToken bal | bridgeAsset bal | queued\* | outstandingRequestId | checkBalance | |
There was a problem hiding this comment.
There are 2 new states: mint-failed; unwrap-ok/queue-fail);
| 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.
Can revert for multiple reasons:
- outstandingRequestId != 0
- amount == 0
- bridgeAssetHeld < amount
- shipOutOfBounds
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],
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