Skip to content

Commit 97f6f1a

Browse files
committed
Bug fixes
1 parent d8331cf commit 97f6f1a

3 files changed

Lines changed: 56 additions & 6 deletions

File tree

contracts/contracts/strategies/crosschainV3/adapters/CCTPAdapter.sol

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,18 +208,17 @@ contract CCTPAdapter is AbstractAdapter, IMessageHandlerV2 {
208208
bytes calldata attestation
209209
) internal {
210210
(
211-
address burnToken,
211+
,
212212
address mintRecipient,
213213
uint256 amount,
214214
address msgSender,
215215
uint256 feeExecuted,
216216
bytes memory hookData
217217
) = CCTPMessageHelper.decodeBurnBody(body);
218-
// `burnToken` is the SOURCE-chain USDC address, which differs from this chain's
219-
// `usdcToken` for cross-chain transfers. CCTP's MessageTransmitter validates the
220-
// burn record cryptographically via the attestation; what gets minted here is
221-
// always the local USDC by protocol design. So no local burnToken equality check.
222-
burnToken; // silence unused-var
218+
// The source-chain burn token is intentionally not extracted or checked: it's the
219+
// SOURCE-chain USDC address (differs from this chain's `usdcToken` for cross-chain
220+
// transfers), CCTP's MessageTransmitter validates the burn record cryptographically via
221+
// the attestation, and what gets minted here is always the local USDC by protocol design.
223222
// The burn branch skips the pure-message branch's `transportRecipient` parity check,
224223
// so enforce mint-recipient parity here: a forged burn that mints elsewhere reverts
225224
// cleanly instead of silently delivering 0.

contracts/contracts/strategies/crosschainV3/adapters/SuperbridgeAdapter.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,14 @@ contract SuperbridgeAdapter is
278278
function processStoredMessage(address _target) external override {
279279
PendingMessage memory p = pendingFor[_target];
280280
require(p.exists, "Super: nothing pending");
281+
// Re-assert the incident-response levers on the deferred path too. The atomic path
282+
// gates on these in `_validateInbound`; a `pauseLane`/`revoke` issued after the message
283+
// was stored must block deferred delivery as well, otherwise the pause is ineffective
284+
// for exactly the split-delivery window it exists to cover. Check `authorised` first —
285+
// `revoke` leaves `paused` untouched. A held slot isn't dropped: its WETH stays on the
286+
// adapter until unpause / re-authorise (recoverable via `transferToken`).
287+
require(authorised[_target], "Super: not authorised");
288+
require(!laneConfig[_target].paused, "Super: lane paused");
281289
require(
282290
IERC20(weth).balanceOf(address(this)) >= p.intendedAmount,
283291
"Super: tokens not yet landed"

contracts/test/strategies/crosschainV3/split-inbound-adapter.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,49 @@ describe("Unit: SuperbridgeAdapter split delivery", function () {
239239
expect(await strategy.callCount()).to.equal(1);
240240
});
241241

242+
it("stored message honours a pause/revoke issued after it was stored (incident response)", async () => {
243+
const amount = ethers.utils.parseUnits("250", 6);
244+
const data = wrapEnvelope(strategy.address, amount, packPayload("pending"));
245+
const sRouter = await impersonateAndFund(await receiver.ccipRouter());
246+
247+
// Message stored while the lane is healthy (token leg not yet landed).
248+
await receiver
249+
.connect(sRouter)
250+
.ccipReceive(
251+
buildAny2EvmMessage({ data, transportSender: receiver.address })
252+
);
253+
expect(await receiver.hasPendingMessage(strategy.address)).to.equal(true);
254+
255+
// Canonical-bridge ETH lands and `receive()` wraps it to WETH.
256+
await deliverBridgeEth(amount);
257+
258+
// Incident: governance pauses the lane. Deferred delivery must be blocked too, not just
259+
// the atomic ccipReceive path.
260+
await receiver.connect(governor).pauseLane(strategy.address);
261+
await expect(
262+
receiver.processStoredMessage(strategy.address)
263+
).to.be.revertedWith("Super: lane paused");
264+
265+
// Revoke leaves `paused` untouched, so this exercises the `authorised` check.
266+
await receiver.connect(governor).unpauseLane(strategy.address);
267+
await receiver.connect(governor).revoke(strategy.address);
268+
await expect(
269+
receiver.processStoredMessage(strategy.address)
270+
).to.be.revertedWith("Super: not authorised");
271+
272+
// The message + its WETH were held, not lost: re-authorise + unpause and delivery resumes.
273+
await receiver.connect(governor).authorise(strategy.address, {
274+
paused: false,
275+
chainSelector: PEER_CHAIN,
276+
destGasLimit: DEST_GAS_LIMIT,
277+
});
278+
await receiver.processStoredMessage(strategy.address);
279+
expect(await receiver.hasPendingMessage(strategy.address)).to.equal(false);
280+
expect(await strategy.callCount()).to.equal(1);
281+
expect(await strategy.lastAmount()).to.equal(amount);
282+
expect(await wethMock.balanceOf(strategy.address)).to.equal(amount);
283+
});
284+
242285
it("multi-tenant: one adapter routes messages to distinct targets by envelope sender", async () => {
243286
const cfg = {
244287
paused: false,

0 commit comments

Comments
 (0)