Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Master list of UniV3 forks:

### Breaking changes

* Removed BridgeSettler action `BRIDGE_ERC20_TO_LAYER_ZERO_OFT` in favor of
`BRIDGE_TO_LAYER_ZERO_OFT` that accepts ERC20 and Native tokens.

### Non-breaking changes

## 2025-09-29
Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ depth = 20
[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
bnb = "${BNB_MAINNET_RPC_URL}"
plasma = "${PLASMA_MAINNET_RPC_URL}"

[lint]
lint_on_build = false
6 changes: 3 additions & 3 deletions src/bridge/BridgeSettlerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ abstract contract BridgeSettlerBase is Basic, Relay, LayerZeroOFT {
} else if (action == uint32(IBridgeSettlerActions.BRIDGE_NATIVE_TO_RELAY.selector)) {
(address to, bytes32 requestId) = abi.decode(data, (address, bytes32));
bridgeNativeToRelay(to, requestId);
} else if (action == uint32(IBridgeSettlerActions.BRIDGE_ERC20_TO_LAYER_ZERO_OFT.selector)) {
(IERC20 token, address oft, bytes memory sendData) = abi.decode(data, (IERC20, address, bytes));
bridgeLayerZeroOFT(token, oft, sendData);
} else if (action == uint32(IBridgeSettlerActions.BRIDGE_TO_LAYER_ZERO_OFT.selector)) {
(IERC20 token, uint256 nativeFee, address oft, bytes memory sendData) = abi.decode(data, (IERC20, uint256, address, bytes));
bridgeLayerZeroOFT(token, nativeFee, oft, sendData);
} else {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/bridge/IBridgeSettlerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface IBridgeSettlerActions {
function BRIDGE_NATIVE_TO_STARGATE_V2(address pool, uint256 destinationGas, bytes calldata sendData) external;

/// @dev Bridge ERC20 through LayerZeroOFT
function BRIDGE_ERC20_TO_LAYER_ZERO_OFT(address token, address oft, bytes calldata sendData) external;
function BRIDGE_TO_LAYER_ZERO_OFT(address token, uint256 nativeFee, address oft, bytes calldata sendData) external;

/// @dev Bridge ERC20 through DeBridge
function BRIDGE_TO_DEBRIDGE(uint256 globalFee, bytes calldata createOrderData) external;
Expand Down
17 changes: 13 additions & 4 deletions src/core/LayerZeroOFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ import {SafeTransferLib} from "../vendor/SafeTransferLib.sol";
contract LayerZeroOFT {
using SafeTransferLib for IERC20;

function bridgeLayerZeroOFT(IERC20 token, address oft, bytes memory sendData) internal {
uint256 updatedInputAmount = token.fastBalanceOf(address(this));
token.safeApproveIfBelow(oft, updatedInputAmount);
IERC20 internal constant ETH = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat idle thought: we should enshrine https://eips.ethereum.org/EIPS/eip-7528 somewhere in this repo rather than redefining this ad-hoc wherever we need it


function bridgeLayerZeroOFT(IERC20 token, uint256 nativeFee, address oft, bytes memory sendData) internal {
uint256 updatedInputAmount;
if(token == ETH) {
uint256 value = address(this).balance;
updatedInputAmount = value - nativeFee;
nativeFee = value;
} else {
updatedInputAmount = token.fastBalanceOf(address(this));
token.safeApproveIfBelow(oft, updatedInputAmount);
}

assembly ("memory-safe") {
mstore(add(0xe0, sendData), updatedInputAmount)
Expand All @@ -18,7 +27,7 @@ contract LayerZeroOFT {
// temporarily clobber `sendData` size memory area
mstore(sendData, 0xc7c7f5b3) // selector for `send((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)`
// `send` doesn't clash with any relevant function of restricted targets so we can skip checking oft
if iszero(call(gas(), oft, selfbalance(), add(0x1c, sendData), add(0x04, len), 0x00, 0x00)) {
if iszero(call(gas(), oft, nativeFee, add(0x1c, sendData), add(0x04, len), 0x00, 0x00)) {
let ptr := mload(0x40)
returndatacopy(ptr, 0x00, returndatasize())
revert(ptr, returndatasize())
Expand Down
1 change: 0 additions & 1 deletion test/integration/BridgeSettler.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
pragma solidity ^0.8.25;

import {BridgeSettlerTestBase} from "../unit/BridgeSettler.t.sol";
import {MainnetSettler as Settler} from "src/chains/Mainnet/TakerSubmitted.sol";
import {MainnetDefaultFork} from "./BaseForkTest.t.sol";
import {IDeployer} from "src/deployer/IDeployer.sol";
import {DEPLOYER} from "src/deployer/DeployerAddress.sol";
Expand Down
69 changes: 64 additions & 5 deletions test/integration/LayerZeroOFT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol";
import {BridgeSettlerIntegrationTest} from "./BridgeSettler.t.sol";
import {ALLOWANCE_HOLDER} from "src/allowanceholder/IAllowanceHolder.sol";
import {IBridgeSettlerActions} from "src/bridge/IBridgeSettlerActions.sol";
import {ArbitrumBridgeSettler} from "src/chains/Arbitrum/BridgeSettler.sol";
import {PlasmaBridgeSettler} from "src/chains/Plasma/BridgeSettler.sol";
import {SafeTransferLib} from "src/vendor/SafeTransferLib.sol";

interface IOFT {
Expand Down Expand Up @@ -54,7 +54,7 @@ interface IOFT {
function quoteSend(SendParam calldata sendParam, bool payInLzToken) external view returns (MessagingFee memory);
}

contract LayerZeroOFTTest is BridgeSettlerIntegrationTest {
contract LayerZeroOFTEthereumTest is BridgeSettlerIntegrationTest {
using SafeTransferLib for IERC20;

// USDT0 OFTAdapter
Expand All @@ -63,9 +63,9 @@ contract LayerZeroOFTTest is BridgeSettlerIntegrationTest {
receive() external payable {}

function testBridgeERC20() public {
uint256 amount = 10000000;
// USDT
token = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
uint256 amount = 10000000;
deal(address(token), address(this), amount, true);
token.safeApprove(address(ALLOWANCE_HOLDER), amount);

Expand Down Expand Up @@ -100,8 +100,8 @@ contract LayerZeroOFTTest is BridgeSettlerIntegrationTest {
);
sendParam.amountLD = 0; // send 0 to let settler inject the value
bridgeActions[1] = abi.encodeCall(
IBridgeSettlerActions.BRIDGE_ERC20_TO_LAYER_ZERO_OFT,
(address(token), oft, abi.encode(sendParam, messagingFee, address(this)))
IBridgeSettlerActions.BRIDGE_TO_LAYER_ZERO_OFT,
(address(token), fee, oft, abi.encode(sendParam, messagingFee, address(this)))
);
sendParam.amountLD = amount;

Expand All @@ -120,3 +120,62 @@ contract LayerZeroOFTTest is BridgeSettlerIntegrationTest {
assertEq(balanceAfter - balanceBefore, amount, "Assets were not received");
}
}

contract LayerZeroOFTPlasmaTest is BridgeSettlerIntegrationTest {
using SafeTransferLib for IERC20;

// XPL NativeOFTAdapter
address oft = 0x405FBc9004D857903bFD6b3357792D71a50726b0;

receive() external payable {}

function _testBridgeSettler() internal virtual override {
bridgeSettler = new PlasmaBridgeSettler(bytes20(0));
}

function _testChainId() internal pure override returns (string memory) {
return "plasma";
}

function _testBlockNumber() internal pure override returns (uint256) {
return 3259177;
}

function testBridgeNative() public {
token = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
uint256 amount = 10 ether;
deal(address(this), amount);

IOFT.SendParam memory sendParam = IOFT.SendParam({
dstEid: uint32(30102), // BNB
to: bytes32(uint256(uint160(makeAddr("recipient")))),
amountLD: amount,
minAmountLD: amount,
extraOptions: bytes(""),
composeMsg: bytes(""),
oftCmd: bytes("")
});

(,, IOFT.OFTReceipt memory receipt) = IOFT(oft).quoteOFT(sendParam);
sendParam.minAmountLD = receipt.amountReceivedLD;

IOFT.MessagingFee memory messagingFee = IOFT(oft).quoteSend(sendParam, false);
uint256 fee = messagingFee.nativeFee;

sendParam.amountLD = 0; // send 0 to let settler inject the value
bytes[] memory bridgeActions = new bytes[](1);
bridgeActions[0] = abi.encodeCall(
IBridgeSettlerActions.BRIDGE_TO_LAYER_ZERO_OFT,
(address(token), fee, oft, abi.encode(sendParam, messagingFee, address(this)))
);
sendParam.amountLD = amount;

deal(address(this), amount + fee);
uint256 balanceBefore = address(oft).balance;
vm.expectCall(oft, amount + fee, abi.encodeCall(IOFT.send, (sendParam, messagingFee, address(this))));
bridgeSettler.execute{value: amount + fee}(bridgeActions, bytes32(0));
uint256 balanceAfter = address(oft).balance;

assertEq(balanceAfter - balanceBefore, amount, "Assets were not received");
}
}
Loading