Skip to content
Draft
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: 1 addition & 2 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/bin/sh

cd contracts
yarn run lint:js
pnpm run lint:js
29 changes: 29 additions & 0 deletions contracts/contracts/interfaces/crosschainV3/IBridgeReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

/**
* @title IBridgeReceiver
* @author Origin Protocol Inc
* @dev Receiver hook implemented by Master and Remote strategies. The configured inbound
* adapter forwards incoming bridge deliveries through this single entry point.
*
* The adapter MUST have transferred any inbound tokens to the strategy before invoking
* this function. Tokens-with-message arrives via sendTokensAndMessage on the source;
* message-only arrives via sendMessage on the source. In both cases the strategy reads
* the fields below to dispatch by message type.
*/
interface IBridgeReceiver {
/**
* @notice Called by the authorised receiver adapter upon inbound bridge delivery.
* @param nonce Yield-channel nonce (0 for bridge-channel messages).
* @param amount Token amount delivered with the message (0 for message-only).
* @param messageType Discriminator from CrossChainV3Helper message-type constants.
* @param payload Message-specific payload bytes (the envelope's body).
*/
function receiveFromBridge(
uint64 nonce,
uint256 amount,
uint8 messageType,
bytes calldata payload
) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

/**
* @title IOutboundAdapter
* @author Origin Protocol Inc
* @dev Bridge-agnostic outbound adapter interface used by Master / Remote strategies in the
* OUSD V3 cross-chain strategy pair. An adapter encapsulates a single bridge transport
* (CCTP, CCIP, canonical L1↔L2 bridges, etc.) so the strategy stays bridge-ignorant.
*
* Atomic bridges (CCTP, CCIP) can have a single adapter instance shared across multiple
* strategy pairs. Split-delivery bridges (canonical) get a dedicated instance per pair to
* prevent token misrouting.
*/
interface IOutboundAdapter {
/**
* @notice Send tokens together with a message to the configured peer.
* Used by the yield channel for deposits and withdrawal claim responses.
* @param token Token to bridge (must be approved to the adapter by the caller).
* @param amount Token amount to bridge.
* @param message Envelope-wrapped message bytes (see CrossChainV3Helper).
*/
function sendTokensAndMessage(
address token,
uint256 amount,
bytes calldata message
) external payable;

/**
* @notice Send a message-only payload to the configured peer.
* Used for acks, balance checks, settlement, and bridge-channel ops.
* @param message Envelope-wrapped message bytes (see CrossChainV3Helper).
*/
function sendMessage(bytes calldata message) external payable;

/**
* @notice Estimate the bridge fee for the given operation.
* @param amount Token amount to bridge (0 for message-only).
* @param message Envelope-wrapped message bytes.
* @return nativeFee Native gas fee required as msg.value.
* @return tokenFee Token-denominated fee (e.g., LINK for CCIP), if applicable.
*/
function estimateFee(uint256 amount, bytes calldata message)
external
view
returns (uint256 nativeFee, uint256 tokenFee);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

/**
* @title ISplitInboundAdapter
* @author Origin Protocol Inc
* @dev Interface for split-delivery inbound bridge adapters — those where the message and
* its companion token leg arrive in separate transactions (e.g., OP Stack canonical
* bridge for the tokens + a separate message bridge for the envelope).
*
* Atomic adapters (CCIP, CCTP V2 with combined token + message) do NOT implement this
* interface — they deliver in a single transaction and have no pending-slot lifecycle.
*
* Split-delivery adapters are multi-tenant: each pending slot is keyed by the destination
* strategy's address on this chain (which equals the source sender by CREATE2 parity),
* so callers pass that address when querying or finalising.
*/
interface ISplitInboundAdapter {
/**
* @notice Whether the adapter currently has a stored message for `_target` waiting for
* its companion token leg.
*/
function hasPendingMessage(address _target) external view returns (bool);

/**
* @notice Permissionless finaliser: if both message and tokens have arrived for
* `_target`, forward to it and clear that target's pending slot. Reverts when
* nothing is pending or the token leg hasn't landed yet, so off-chain automation
* can retry.
*/
function processStoredMessage(address _target) external;
}
149 changes: 149 additions & 0 deletions contracts/contracts/mocks/crosschainV3/MockBridgeAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { IOutboundAdapter } from "../../interfaces/crosschainV3/IOutboundAdapter.sol";
import { IBridgeReceiver } from "../../interfaces/crosschainV3/IBridgeReceiver.sol";
import { CrossChainV3Helper } from "../../strategies/crosschainV3/CrossChainV3Helper.sol";

/**
* @title MockBridgeAdapter
* @author Origin Protocol Inc
*
* @notice TEST-ONLY synchronous loopback adapter for the V3 strategy pair. Plays the role of
* both the outbound adapter on the source side and the receiver adapter on the
* destination side — it calls peer.receiveFromBridge() in the same transaction.
*
* Used by the Master+Remote unit tests to wire two strategy instances in-process
* without spinning up real bridges.
*/
contract MockBridgeAdapter is IOutboundAdapter {
using SafeERC20 for IERC20;

/// @notice Authorised sender on the local side (the strategy we adapt for).
address public sender;
/// @notice Peer receiver on the destination side (the other strategy).
address public peer;

/// @notice When false, sendTokensAndMessage / sendMessage are no-ops on the peer side.
/// Useful for simulating in-flight delays in tests; calls still consume tokens.
bool public deliveryEnabled = true;

// Inspection slots
bytes public lastMessageSent;
uint256 public lastAmountSent;
address public lastTokenSent;

event PeerConfigured(address peer);
event SenderConfigured(address sender);
event DeliveryToggled(bool enabled);
event MessageDelivered(uint8 messageType, uint64 nonce, uint256 amount);

function setPeer(address _peer) external {
peer = _peer;
emit PeerConfigured(_peer);
}

function setSender(address _sender) external {
sender = _sender;
emit SenderConfigured(_sender);
}

function setDeliveryEnabled(bool _enabled) external {
deliveryEnabled = _enabled;
emit DeliveryToggled(_enabled);
}

/// @inheritdoc IOutboundAdapter
function sendTokensAndMessage(
address token,
uint256 amount,
bytes calldata message
) external payable override {
_requireAuthorised();
lastMessageSent = message;
lastAmountSent = amount;
lastTokenSent = token;

// Pull tokens from the local strategy.
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);

if (!deliveryEnabled || peer == address(0)) {
return;
}

// Forward tokens to peer and call its receiver hook synchronously.
IERC20(token).safeTransfer(peer, amount);
_dispatch(amount, message);
}

/// @inheritdoc IOutboundAdapter
function sendMessage(bytes calldata message) external payable override {
_requireAuthorised();
lastMessageSent = message;
lastAmountSent = 0;
lastTokenSent = address(0);

if (!deliveryEnabled || peer == address(0)) {
return;
}

_dispatch(0, message);
}

/// @inheritdoc IOutboundAdapter
function estimateFee(uint256, bytes calldata)
external
pure
override
returns (uint256, uint256)
{
return (0, 0);
}

/**
* @dev Manually flush a previously-stored undelivered message to the peer.
* Useful in tests that toggled deliveryEnabled off to inspect in-flight state.
*/
function flushPendingDelivery() external {
require(deliveryEnabled, "Delivery still disabled");
require(lastMessageSent.length > 0, "Nothing to flush");

if (lastAmountSent > 0 && lastTokenSent != address(0)) {
IERC20(lastTokenSent).safeTransfer(peer, lastAmountSent);
}
_dispatch(lastAmountSent, lastMessageSent);

delete lastMessageSent;
lastAmountSent = 0;
lastTokenSent = address(0);
}

function _requireAuthorised() internal view {
require(
sender == address(0) || msg.sender == sender,
"MockBridgeAdapter: unauthorised sender"
);
}

function _dispatch(uint256 amount, bytes memory message) internal {
(uint32 version, uint32 msgType, uint64 nonce, , ) = CrossChainV3Helper
.unwrap(message);
require(
version == CrossChainV3Helper.ORIGIN_V3_MESSAGE_VERSION,
"MockBridgeAdapter: bad version"
);

bytes memory payload = CrossChainV3Helper.getPayload(message);
emit MessageDelivered(uint8(msgType), nonce, amount);

IBridgeReceiver(peer).receiveFromBridge(
nonce,
amount,
uint8(msgType),
payload
);
}
}
58 changes: 58 additions & 0 deletions contracts/contracts/mocks/crosschainV3/MockBridgeCallTarget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

/**
* @title MockBridgeCallTarget
* @notice TEST-ONLY recipient contract used to exercise the optional `callData` post-delivery
* hook on BRIDGE_IN / BRIDGE_OUT. Records successful invocations, can be flipped to
* always-revert, and exposes a gas-burning helper.
*/
contract MockBridgeCallTarget {
bool public alwaysRevert;
uint256 public callCount;
bytes32 public lastBridgeId;
address public lastCaller;
uint256 public lastValueObserved;
bytes public lastData;

event Pinged(
bytes32 indexed bridgeId,
address indexed caller,
uint256 token
);

function setAlwaysRevert(bool _r) external {
alwaysRevert = _r;
}

/// @dev Match the kind of post-mint hook a real composing contract would expose.
function onBridgeDelivered(bytes32 _bridgeId, uint256 _tokenAmount)
external
{
if (alwaysRevert) revert("MockTarget: intentional revert");
callCount += 1;
lastBridgeId = _bridgeId;
lastCaller = msg.sender;
lastValueObserved = _tokenAmount;
emit Pinged(_bridgeId, msg.sender, _tokenAmount);
}

/// @dev Spin-loop until gas exhaustion. Used to exercise out-of-gas in the post-call hook.
// solhint-disable-next-line no-empty-blocks
function burnGas() external {
while (true) {}
}

/// @dev Fallback used by tests that simply want to assert "any call landed".
fallback() external payable {
if (alwaysRevert) revert("MockTarget: intentional revert");
callCount += 1;
lastCaller = msg.sender;
lastValueObserved = msg.value;
lastData = msg.data;
}

receive() external payable {
if (alwaysRevert) revert("MockTarget: intentional revert");
}
}
30 changes: 30 additions & 0 deletions contracts/contracts/mocks/crosschainV3/MockBridgeReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { IBridgeReceiver } from "../../interfaces/crosschainV3/IBridgeReceiver.sol";

/**
* @title MockBridgeReceiver
* @notice TEST-ONLY recorder for `receiveFromBridge` calls. Used to assert what an
* inbound adapter forwarded after split-delivery store-and-process.
*/
contract MockBridgeReceiver is IBridgeReceiver {
uint64 public lastNonce;
uint256 public lastAmount;
uint8 public lastMessageType;
bytes public lastPayload;
uint256 public callCount;

function receiveFromBridge(
uint64 nonce,
uint256 amount,
uint8 messageType,
bytes calldata payload
) external override {
lastNonce = nonce;
lastAmount = amount;
lastMessageType = messageType;
lastPayload = payload;
callCount += 1;
}
}
Loading
Loading