Contract detail
BridgeInFacet
BridgeInFacet is a verified Robinhood Chain smart contract at 0x9db03579…88aaf179 with 0 indexed transactions.
- Address type
- Smart contract
- Transactions
- 0
- History coverage
- No indexed activity found
- Verification
- Verified
- Compiler
- v0.8.24+commit.e11b9ed9
- Verification match
- partial
- Creation transaction
- 0x0275adb7…62e2b1ba
- Created at block
- #28614873
- Language
- solidity
- EVM version
- cancun
- License
- none
- Compiler optimization
- Enabled · 200 runs
Verified contract
Contract source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {ECDSA} from "solady/utils/ECDSA.sol";
import {EfficientHashLib} from "solady/utils/EfficientHashLib.sol";
import {
AppStorage,
LibAppStorage,
BridgeInRequest,
SignerSet,
TokenConfig,
TokenMode,
TokenNotConfigured,
PendingBridgeIn,
MessageStatus,
TimelockReason,
OperatingMode,
AddressPolicy,
AddressStatus,
DeadlineExpired,
MAX_DECIMALS,
DEFAULT_QUEUE_DELAY
} from "../libs/LibAppStorage.sol";
import {LibPausable} from "../libs/LibPausable.sol";
import {LibReentrancyGuard} from "../libs/LibReentrancyGuard.sol";
import {LibRateLimiter} from "../libs/LibRateLimiter.sol";
import {LibBridgeIn} from "../libs/LibBridgeIn.sol";
// ═══════════════════════════════════════════════════════════════════════════════
// ERRORS
// ═══════════════════════════════════════════════════════════════════════════════
error ZeroAmount();
error ZeroRecipient();
error InvalidDecimals(uint8 decimals, uint8 maxDecimals);
error AmountTooSmallAfterConversion();
error BridgeInAlreadyProcessed(bytes32 messageId);
error NoValidSignerSet();
error ChainIdMismatch(uint256 expected, uint256 actual);
// Routing errors
error RecipientBlocked(address recipient);
error QuantityRateLimitExceeded(address token, uint256 available, uint256 requested, uint256 retryAfter);
error GlobalValueRateLimitExceeded(uint256 available, uint256 requested, uint256 retryAfter);
error AddressRateLimitExceeded(address recipient, uint256 available, uint256 requested, uint256 retryAfter);
// ═══════════════════════════════════════════════════════════════════════════════
// EVENTS
// ═══════════════════════════════════════════════════════════════════════════════
/// @notice Emitted when a bridge-in is queued for timelock
/// @param messageId Unique message identifier
/// @param recipient Recipient address
/// @param amount Original amount from request (source decimals)
/// @param reason Reason for queueing
event BridgeInQueued(
bytes32 indexed messageId, address indexed recipient, uint256 amount, TimelockReason reason, uint256 executeAfter
);
// ═══════════════════════════════════════════════════════════════════════════════
// FACET
// ═══════════════════════════════════════════════════════════════════════════════
/// @title BridgeInFacet
/// @notice Handles incoming bridge messages (GalaChain -> EVM) with routing
/// @dev Uses EIP-712 threshold signature verification from authorized signer set.
/// Routes transactions through instant path or timelock based on operating mode,
/// trust status, and rate limits.
contract BridgeInFacet {
using EnumerableSet for EnumerableSet.Bytes32Set;
using LibRateLimiter for LibRateLimiter.Bucket;
// =========================================================================
// Constants
// =========================================================================
/// @dev BridgeInRequest typehash for EIP-712 structured data
bytes32 public constant BRIDGE_IN_TYPEHASH = keccak256(
"BridgeInRequest(string sourceChain,string sourceToken,string sender,uint256 nonce,address token,address recipient,uint256 sourceAmount,uint8 sourceDecimals,uint256 deadline,uint256 attestedUsdValue)"
);
// =========================================================================
// Storage
// =========================================================================
/// @dev AppStorage at slot 0 - MUST be the only state variable
AppStorage internal s;
// =========================================================================
// External Functions
// =========================================================================
/// @notice Bridge tokens from GalaChain to EVM with automatic routing
/// @dev Verifies threshold signatures, consumes rate limits, and routes based on
/// operating mode and trust status. Instant path available for trusted recipients
/// within limits in Normal mode. Everything queued in Guarded mode.
/// @param request The bridge in request containing token, amount, recipient, etc.
/// @param signatures Array of signatures, sorted by signer address ascending
function bridgeIn(BridgeInRequest calldata request, bytes[] calldata signatures) external {
LibPausable.requireNotPaused();
LibReentrancyGuard.lock();
// === FAIL-FAST: Cheapest checks first ===
// 1. Zero checks (calldata reads only)
if (request.sourceAmount == 0) revert ZeroAmount();
if (request.recipient == address(0)) revert ZeroRecipient();
// 2. Deadline check
if (request.deadline != 0 && block.timestamp > request.deadline) {
revert DeadlineExpired(request.deadline, block.timestamp);
}
// 3. Blocked address check (before expensive operations)
if (s.addressPolicies[request.recipient].status == AddressStatus.Blocked) {
revert RecipientBlocked(request.recipient);
}
// === EXPENSIVE OPERATIONS ===
// 4. Verify signatures (expensive but must happen before state changes)
_verifySignatures(request, signatures);
// 5. Compute message ID
bytes32 messageId = _computeMessageId(request.sourceChain, request.sender, request.nonce);
// 6. Replay check using MessageStatus
if (s.messageStatus[messageId] != MessageStatus.Unknown) {
revert BridgeInAlreadyProcessed(messageId);
}
// 7. Token config check
TokenConfig storage config = s.tokenConfigs[request.token];
if (config.mode == TokenMode.NotConfigured) {
revert TokenNotConfigured(request.token);
}
// 8. Normalize amount from source to destination decimals
uint256 normalizedAmount = _normalizeAmount(request.sourceAmount, request.sourceDecimals, config.decimals);
if (normalizedAmount == 0) {
revert AmountTooSmallAfterConversion();
}
// === CONSUME RATE LIMITS (reverts if exceeded) ===
_consumeRateLimits(request.token, request.recipient, normalizedAmount, request.attestedUsdValue);
// === ROUTING DECISION ===
if (s.operatingMode == OperatingMode.Guarded) {
// Guarded mode: everything goes through timelock
s.messageStatus[messageId] = MessageStatus.Pending;
_queueBridgeIn(messageId, request, normalizedAmount, config.mode, TimelockReason.GuardedMode);
} else {
// Normal mode: check if eligible for instant execution
(bool eligible, TimelockReason reason) =
_checkInstantEligibility(request.token, request.recipient, request.attestedUsdValue);
if (eligible) {
// Mark as executed and process immediately
s.messageStatus[messageId] = MessageStatus.Executed;
LibBridgeIn.executeInstant(
messageId, request.token, request.recipient, normalizedAmount, config.mode, s.escrowAddress
);
} else {
// Not eligible for instant - queue with reason
s.messageStatus[messageId] = MessageStatus.Pending;
_queueBridgeIn(messageId, request, normalizedAmount, config.mode, reason);
}
}
LibReentrancyGuard.unlock();
}
/// @notice Check if a bridge-in message has been processed
/// @param messageId The message ID to check
/// @return True if the message has been processed (executed or reverted)
function isBridgeInProcessed(bytes32 messageId) external view returns (bool) {
MessageStatus status = s.messageStatus[messageId];
return status == MessageStatus.Executed || status == MessageStatus.Reverted;
}
/// @notice Compute message ID for a bridge-in request
/// @dev Useful for off-chain systems to pre-compute message IDs
/// @param sourceChain Source chain identifier
/// @param sender Sender identifier on source chain
/// @param nonce User-provided nonce
/// @return The message ID
function computeMessageId(string calldata sourceChain, string calldata sender, uint256 nonce)
external
pure
returns (bytes32)
{
return _computeMessageId(sourceChain, sender, nonce);
}
/// @notice Get the EIP-712 domain separator
/// @dev Useful for off-chain systems to construct valid signatures
/// @return The domain separator
function getDomainSeparator() external view returns (bytes32) {
return _domainSeparator();
}
// =========================================================================
// Private Functions - Rate Limit Consumption
// =========================================================================
/// @notice Consume all rate limit buckets with contextual error messages
/// @dev Reverts with specific error identifying which bucket was exceeded.
/// All buckets are consumed before routing decision.
/// @param token Token address (for quantity bucket)
/// @param recipient Recipient address (for address bucket)
/// @param normalizedAmount Amount in destination decimals
/// @param attestedUsdValue USD value from signed message (18 decimals)
function _consumeRateLimits(address token, address recipient, uint256 normalizedAmount, uint256 attestedUsdValue)
private
{
// 1. Quantity bucket (per-token)
_consumeQuantityBucketChecked(token, normalizedAmount);
// 2. Global value bucket (bridge-wide USD)
_consumeGlobalValueBucketChecked(attestedUsdValue);
// 3. Address bucket (per-recipient USD, no-op if not enabled or no attestation)
_consumeAddressBucketChecked(recipient, attestedUsdValue);
}
/// @notice Consume quantity bucket with contextual error
/// @param token Token address
/// @param amount Amount to consume
function _consumeQuantityBucketChecked(address token, uint256 amount) private {
LibRateLimiter.Bucket storage bucket = s.quantityBuckets[token];
// Check isEnabled from storage BEFORE expensive withAccumulatedRefill()
if (!bucket.isEnabled) {
revert QuantityRateLimitExceeded(token, 0, amount, type(uint256).max);
}
LibRateLimiter.Bucket memory current = bucket.withAccumulatedRefill();
if (amount > current.tokens) {
uint256 retryAfter = amount > current.capacity
? type(uint256).max
: block.timestamp + _ceilDiv(amount - current.tokens, current.refillRate);
revert QuantityRateLimitExceeded(token, current.tokens, amount, retryAfter);
}
// Consume (will succeed since we pre-checked)
bucket.consume(amount);
}
/// @notice Consume global value bucket with contextual error
/// @param valueAmount USD value to consume (18 decimals)
function _consumeGlobalValueBucketChecked(uint256 valueAmount) private {
if (valueAmount == 0) return;
LibRateLimiter.Bucket storage bucket = LibAppStorage.globalValueBucket();
if (!bucket.isEnabled) return; // Disabled bucket = no limit
LibRateLimiter.Bucket memory current = bucket.withAccumulatedRefill();
if (valueAmount > current.tokens) {
uint256 retryAfter = valueAmount > current.capacity
? type(uint256).max
: block.timestamp + _ceilDiv(valueAmount - current.tokens, current.refillRate);
revert GlobalValueRateLimitExceeded(current.tokens, valueAmount, retryAfter);
}
// Consume (will succeed since we pre-checked)
bucket.consume(valueAmount);
}
/// @notice Consume address bucket with contextual error
/// @dev No-op if usdValue is 0 (no USD attestation provided)
/// @dev No-op if bucket is not enabled (untrusted addresses)
/// @param recipient Recipient address
/// @param usdValue USD value to consume (18 decimals)
function _consumeAddressBucketChecked(address recipient, uint256 usdValue) private {
if (usdValue == 0) return; // No USD attestation = skip bucket consumption
AddressPolicy storage policy = s.addressPolicies[recipient];
if (!policy.bucket.isEnabled) return; // Not enabled = no limit
LibRateLimiter.Bucket memory current = policy.bucket.withAccumulatedRefill();
if (usdValue > current.tokens) {
uint256 retryAfter = usdValue > current.capacity
? type(uint256).max
: block.timestamp + _ceilDiv(usdValue - current.tokens, current.refillRate);
revert AddressRateLimitExceeded(recipient, current.tokens, usdValue, retryAfter);
}
// Consume (will succeed since we pre-checked)
policy.bucket.consume(usdValue);
}
/// @notice Ceiling division: ceil(a / b)
/// @dev Used for wait time calculations
function _ceilDiv(uint256 a, uint256 b) private pure returns (uint256) {
return (a + b - 1) / b;
}
// =========================================================================
// Private Functions - Instant Eligibility
// =========================================================================
/// @notice Check if transaction is eligible for instant execution
/// @dev Checks instant thresholds only. Bucket capacity is enforced via consume() which reverts.
/// Order: token instant limit -> trusted status -> per-address instant limit
/// All instant limits are USD-based (18 decimals). If a limit is configured but
/// attestedUsdValue is 0 (not attested), the transaction goes to timelock.
/// @param token The token being bridged
/// @param recipient The recipient address
/// @param attestedUsdValue The attested USD value (18 decimals), 0 if not attested
/// @return eligible True if eligible for instant path
/// @return reason If not eligible, the reason for timelock
function _checkInstantEligibility(address token, address recipient, uint256 attestedUsdValue)
private
view
returns (bool eligible, TimelockReason reason)
{
// 1. Check token instant value limit (USD-based)
uint256 tokenLimit = s.instantValueLimits[token];
if (tokenLimit > 0) {
// Attestation enforced only when the limit is configured
if (attestedUsdValue == 0) {
return (false, TimelockReason.MissingAttestation);
}
if (attestedUsdValue > tokenLimit) {
return (false, TimelockReason.TokenInstantLimitExceeded);
}
}
// 2. Check recipient is trusted
AddressPolicy storage policy = s.addressPolicies[recipient];
if (policy.status != AddressStatus.Trusted) {
return (false, TimelockReason.UntrustedRecipient);
}
// 3. Check per-address instant limit (USD-based)
if (policy.instantValueLimit > 0) {
// Attestation enforced only when the limit is configured
if (attestedUsdValue == 0) {
return (false, TimelockReason.MissingAttestation);
}
if (attestedUsdValue > policy.instantValueLimit) {
return (false, TimelockReason.PolicyInstantLimitExceeded);
}
}
return (true, TimelockReason.UntrustedRecipient); // reason ignored when eligible
}
// =========================================================================
// Private Functions - Queue Integration
// =========================================================================
/// @notice Queue a bridge-in request for delayed execution
/// @dev Pre-computes normalized amount and snapshots mode for deterministic execution.
/// Full request details emitted in event for off-chain indexing.
/// @param messageId The unique message identifier
/// @param request The bridge-in request (for event emission)
/// @param normalizedAmount Pre-computed amount in destination decimals
/// @param mode Execution mode snapshot (Lock or Burn)
/// @param reason The reason for queuing
function _queueBridgeIn(
bytes32 messageId,
BridgeInRequest calldata request,
uint256 normalizedAmount,
TokenMode mode,
TimelockReason reason
) private {
uint256 delay = s.queueDelay;
if (delay == 0) {
delay = DEFAULT_QUEUE_DELAY;
}
// Store minimal execution data with snapshots
s.pendingMessages[messageId] = PendingBridgeIn({
token: request.token,
recipient: request.recipient,
normalizedAmount: normalizedAmount,
mode: mode,
reason: reason,
executeAfter: block.timestamp + delay
});
// Add to queue
s.queuedIds.add(messageId);
// Emit full request for off-chain systems (indexers, UI, audit trail)
emit BridgeInQueued(messageId, request.recipient, request.sourceAmount, reason, block.timestamp + delay);
}
// =========================================================================
// Private Functions - Signature Verification
// =========================================================================
/// @notice Verify signatures for a bridge-in request
/// @dev Tries current signer set first, then previous set if still active
function _verifySignatures(BridgeInRequest calldata request, bytes[] calldata signatures) private view {
bytes32 structHash = _hashRequest(request);
bytes32 digest = _toTypedDataHash(_domainSeparator(), structHash);
// Try current signer set first
if (LibAppStorage.currentSignerSet().signers.length > 0) {
if (_tryVerifyWithSet(digest, signatures, LibAppStorage.currentSignerSet())) {
return;
}
}
// Try previous signer set if still active
if (
LibAppStorage.previousSignerSet().signers.length > 0
&& (LibAppStorage.previousSignerSet().activeUntilBlock == 0
|| block.number <= LibAppStorage.previousSignerSet().activeUntilBlock)
) {
if (_tryVerifyWithSet(digest, signatures, LibAppStorage.previousSignerSet())) {
return;
}
}
revert NoValidSignerSet();
}
/// @notice Try to verify signatures against a signer set
function _tryVerifyWithSet(bytes32 digest, bytes[] calldata signatures, SignerSet storage set)
private
view
returns (bool)
{
uint256 quorum = set.quorum;
if (signatures.length < quorum) {
return false;
}
address lastSigner = address(0);
for (uint256 i = 0; i < quorum; i++) {
address signer = ECDSA.tryRecoverCalldata(digest, signatures[i]);
// Solady returns address(0) on recovery failure
if (signer == address(0)) {
return false;
}
if (!_isSignerInSet(set, signer)) {
return false;
}
if (signer <= lastSigner) {
return false;
}
lastSigner = signer;
}
return true;
}
/// @notice Check if an address is in the signer set
function _isSignerInSet(SignerSet storage set, address signer) private view returns (bool) {
address[] storage signers = set.signers;
uint256 length = signers.length;
for (uint256 i = 0; i < length; i++) {
if (signers[i] == signer) {
return true;
}
}
return false;
}
/// @notice Compute the EIP-712 struct hash for a BridgeInRequest
function _hashRequest(BridgeInRequest calldata request) private pure returns (bytes32) {
return EfficientHashLib.hash(
BRIDGE_IN_TYPEHASH,
keccak256(bytes(request.sourceChain)),
keccak256(bytes(request.sourceToken)),
keccak256(bytes(request.sender)),
bytes32(request.nonce),
bytes32(uint256(uint160(request.token))),
bytes32(uint256(uint160(request.recipient))),
bytes32(request.sourceAmount),
bytes32(uint256(request.sourceDecimals)),
bytes32(request.deadline),
bytes32(request.attestedUsdValue)
);
}
/// @notice Get the EIP-712 domain separator
/// @dev Returns the domain separator computed and stored at initialization.
/// Reverts with ChainIdMismatch if chain ID has changed.
/// This is intentional - on a fork, the bridge should stop until reinitialized.
function _domainSeparator() private view returns (bytes32) {
if (block.chainid != s.domainChainId) {
revert ChainIdMismatch(s.domainChainId, block.chainid);
}
return s.domainSeparator;
}
/// @notice Compute EIP-712 typed data hash efficiently
/// @dev Equivalent to keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash))
function _toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) private pure returns (bytes32 digest) {
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache free memory pointer
// Store "\x19\x01" - value is right-aligned, so 0x19 ends up at 0x1e, 0x01 at 0x1f
mstore(0x00, 0x1901)
mstore(0x20, domainSeparator)
mstore(0x40, structHash)
digest := keccak256(0x1e, 0x42) // Hash 66 bytes starting at 0x1e
mstore(0x40, m) // Restore free memory pointer
}
}
// =========================================================================
// Private Functions - Message Processing
// =========================================================================
/// @notice Compute the chain-agnostic message ID for replay protection
/// @dev Only uses (sourceChain, sender, nonce) - identifies which message from whom
function _computeMessageId(string calldata sourceChain, string calldata sender, uint256 nonce)
private
pure
returns (bytes32)
{
return keccak256(abi.encode(keccak256(bytes(sourceChain)), keccak256(bytes(sender)), nonce));
}
/// @notice Normalize amount from source decimals to destination decimals
/// @param amount Raw amount in source decimals
/// @param sourceDecimals Token decimals on source chain
/// @param destDecimals Token decimals on destination chain
/// @return Normalized amount in destination decimals
function _normalizeAmount(uint256 amount, uint8 sourceDecimals, uint8 destDecimals) private pure returns (uint256) {
// Validate decimals are within bounds
if (sourceDecimals > MAX_DECIMALS) {
revert InvalidDecimals(sourceDecimals, MAX_DECIMALS);
}
if (destDecimals > MAX_DECIMALS) {
revert InvalidDecimals(destDecimals, MAX_DECIMALS);
}
if (sourceDecimals == destDecimals) {
return amount;
}
if (sourceDecimals > destDecimals) {
// Scale down (precision loss - floor division)
return amount / (10 ** (sourceDecimals - destDecimals));
}
// Scale up
return amount * (10 ** (destDecimals - sourceDecimals));
}
}