Robinhood MainnetChain ID: 4663
Contract detail

PredictionMarketBridge

PredictionMarketBridge is a verified Robinhood Chain smart contract at 0xe538da0b…f87fde01 with 4 indexed transactions.

Address type
Smart contract
Transactions
4
First indexed block
#203484
Latest indexed block
#621727
ERC-20 transfers
1
Emitted events
4
Verification
Verified
Compiler
v0.8.30+commit.73712a01
Verification match
full
Creation transaction
0x88dce6ad…3b9d250e
Created at block
#203484
Language
solidity
EVM version
prague
License
none
Compiler optimization
Enabled · 200 runs
Related activity

Transactions

Verified contract

Contract source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import { MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {
    IERC20Metadata
} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {
    SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {
    OptionsBuilder
} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
import "./PredictionMarketBridgeBase.sol";
import "./interfaces/IPredictionMarketBridge.sol";
import "../interfaces/IPredictionMarketTokenFactory.sol";
import "../interfaces/IPredictionMarketToken.sol";

/// @title PredictionMarketBridge
/// @notice Bridge for position tokens on Ethereal (source chain)
/// @dev Extends PredictionMarketBridgeBase with Ethereal-specific logic
contract PredictionMarketBridge is
    PredictionMarketBridgeBase,
    IPredictionMarketBridge
{
    using SafeERC20 for IERC20;
    using OptionsBuilder for bytes;

    // ============ Constants ============
    uint128 private constant GAS_FOR_BRIDGE = 2_000_000;

    // ============ Storage ============

    /// @notice Token factory for validating token origin
    IPredictionMarketTokenFactory public immutable factory;

    // ============ Constructor ============
    constructor(address endpoint_, address owner_, address factory_)
        PredictionMarketBridgeBase(endpoint_, owner_)
    {
        factory = IPredictionMarketTokenFactory(factory_);
    }

    // ============ Bridge Function ============

    /// @inheritdoc IPredictionMarketBridge
    function bridge(
        address token,
        address recipient,
        uint256 amount,
        bytes32 refCode
    ) external payable nonReentrant returns (bytes32 bridgeId) {
        if (token == address(0) || recipient == address(0)) {
            revert ZeroAddress();
        }
        if (amount == 0) revert ZeroAmount();

        // C-4: Validate token was deployed by the factory (not just interface check)
        // This prevents fake tokens with matching pickConfigId/isPredictorToken
        // from corrupting bridge mappings and stealing escrowed tokens
        bytes32 pickConfigId;
        bool isPredictorToken;

        try IPredictionMarketToken(token).pickConfigId() returns (
            bytes32 _pickConfigId
        ) {
            pickConfigId = _pickConfigId;
        } catch {
            revert InvalidToken(token);
        }

        try IPredictionMarketToken(token).isPredictorToken() returns (
            bool _isPredictorToken
        ) {
            isPredictorToken = _isPredictorToken;
        } catch {
            revert InvalidToken(token);
        }

        // Verify the token address matches what the factory would deploy
        address expectedToken =
            factory.predictAddress(pickConfigId, isPredictorToken);
        if (token != expectedToken) {
            revert InvalidToken(token);
        }

        // Transfer tokens to this contract (escrow)
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        _escrowedBalances[token] += amount;

        // Generate unique bridge ID
        bridgeId = _generateBridgeId();

        // Create pending bridge record
        uint64 createdAt = uint64(block.timestamp);
        _pendingBridges[bridgeId] = PendingBridge({
            token: token,
            sender: msg.sender,
            recipient: recipient,
            amount: amount,
            createdAt: createdAt,
            lastRetryAt: createdAt,
            status: BridgeStatus.PENDING
        });

        // Track sender's bridges
        _senderBridges[msg.sender].push(bridgeId);

        // Build and send LZ message in scoped block to reduce stack depth
        {
            // Encode message with token metadata (no source address needed,
            // remote computes token from factory.predictAddress)
            bytes memory payload = abi.encode(
                bridgeId,
                pickConfigId,
                isPredictorToken,
                IERC20Metadata(token).name(),
                IERC20Metadata(token).symbol(),
                recipient,
                amount
            );
            bytes memory message = abi.encode(CMD_BRIDGE, payload);

            // Calculate ACK fee with buffer to prepay on remote chain
            uint128 ackFeeWithBuffer = _getAckFeeWithBuffer();

            // Build options - include ACK fee as value to send to remote
            bytes memory options = OptionsBuilder.newOptions()
                .addExecutorLzReceiveOption(GAS_FOR_BRIDGE, ackFeeWithBuffer);

            // Quote fee
            MessagingFee memory fee =
                _quote(_bridgeConfig.remoteEid, message, options, false);
            if (msg.value < fee.nativeFee) {
                revert InsufficientFee(fee.nativeFee, msg.value);
            }

            // Send message
            _lzSend(
                _bridgeConfig.remoteEid,
                message,
                options,
                fee,
                payable(msg.sender)
            );

            // Refund excess ETH
            _refundExcess(fee.nativeFee);
        }

        emit BridgeInitiated(
            bridgeId, token, msg.sender, recipient, amount, createdAt, refCode
        );
    }

    // ============ Quote Functions ============

    /// @inheritdoc IPredictionMarketBridge
    function quoteBridge(address token, uint256 amount)
        external
        view
        returns (MessagingFee memory fee)
    {
        // Read actual token metadata for accurate quote
        string memory name = IERC20Metadata(token).name();
        string memory symbol = IERC20Metadata(token).symbol();

        // Build message with actual metadata for accurate fee calculation
        bytes memory payload = abi.encode(
            bytes32(0), // bridgeId placeholder
            bytes32(0), // pickConfigId placeholder
            false, // isPredictorToken placeholder
            name,
            symbol,
            address(0), // recipient placeholder
            amount
        );
        bytes memory message = abi.encode(CMD_BRIDGE, payload);

        // Include ACK fee with buffer in the quote
        uint128 ackFeeWithBuffer = _getAckFeeWithBuffer();
        bytes memory options = OptionsBuilder.newOptions()
            .addExecutorLzReceiveOption(GAS_FOR_BRIDGE, ackFeeWithBuffer);
        return _quote(_bridgeConfig.remoteEid, message, options, false);
    }

    /// @inheritdoc IPredictionMarketBridgeBase
    function quoteRetry(bytes32 bridgeId)
        external
        view
        returns (MessagingFee memory fee)
    {
        PendingBridge storage pending = _pendingBridges[bridgeId];

        // Re-read token metadata for accurate quote
        string memory name = IERC20Metadata(pending.token).name();
        string memory symbol = IERC20Metadata(pending.token).symbol();

        bytes memory payload = abi.encode(
            bridgeId,
            bytes32(0), // pickConfigId placeholder
            false, // isPredictorToken placeholder
            name,
            symbol,
            pending.recipient,
            pending.amount
        );
        bytes memory message = abi.encode(CMD_BRIDGE, payload);

        // Include ACK fee with buffer in the quote
        uint128 ackFeeWithBuffer = _getAckFeeWithBuffer();
        bytes memory options = OptionsBuilder.newOptions()
            .addExecutorLzReceiveOption(GAS_FOR_BRIDGE, ackFeeWithBuffer);
        return _quote(_bridgeConfig.remoteEid, message, options, false);
    }

    // ============ Abstract Implementation ============

    /// @dev Build retry message for Ethereal -> Remote bridge
    function _buildRetryMessage(bytes32 bridgeId, PendingBridge storage pending)
        internal
        view
        override
        returns (bytes memory message, uint128 gasLimit)
    {
        // Re-read token metadata
        bytes32 pickConfigId =
            IPredictionMarketToken(pending.token).pickConfigId();
        bool isPredictorToken =
            IPredictionMarketToken(pending.token).isPredictorToken();
        string memory name = IERC20Metadata(pending.token).name();
        string memory symbol = IERC20Metadata(pending.token).symbol();

        // Encode same message as original bridge
        bytes memory payload = abi.encode(
            bridgeId,
            pickConfigId,
            isPredictorToken,
            name,
            symbol,
            pending.recipient,
            pending.amount
        );
        message = abi.encode(CMD_BRIDGE, payload);
        gasLimit = GAS_FOR_BRIDGE;
    }

    /// @dev Handle incoming bridge from remote (release escrowed tokens)
    function _handleBridge(bytes memory data) internal override {
        (
            bytes32 bridgeId,
            bytes32 pickConfigId,
            bool isPredictorToken,
            address recipient,
            uint256 amount
        ) = abi.decode(data, (bytes32, bytes32, bool, address, uint256));

        // Compute source token address from factory
        address token = factory.predictAddress(pickConfigId, isPredictorToken);

        // Check if this bridge was already processed (idempotency)
        if (_processedBridges[bridgeId]) {
            // Already processed - just re-send ACK
            emit BridgeProcessed(bridgeId, true);
            _trySendAck(bridgeId);
            return;
        }

        // Mark as processed BEFORE releasing (prevents reentrancy issues)
        _processedBridges[bridgeId] = true;

        if (_escrowedBalances[token] < amount) {
            revert InsufficientEscrowBalance(amount, _escrowedBalances[token]);
        }

        // Release tokens
        _escrowedBalances[token] -= amount;
        IERC20(token).safeTransfer(recipient, amount);

        emit BridgeProcessed(bridgeId, false);
        emit TokensReleased(bridgeId, token, recipient, amount);

        // Send ACK back to remote (if contract has sufficient balance)
        _trySendAck(bridgeId);
    }

    /// @dev Handle ACK from remote (mark outgoing bridge as completed)
    function _handleAck(bytes memory data) internal override {
        bytes32 bridgeId = abi.decode(data, (bytes32));
        PendingBridge storage pending = _pendingBridges[bridgeId];

        if (pending.status == BridgeStatus.PENDING) {
            pending.status = BridgeStatus.COMPLETED;
            emit BridgeCompleted(bridgeId);
        }
    }
}