Files
smom-dbis-138/contracts/bridge/trustless/InboxETH.sol
defiQUG f19c771760 refactor(bridge): trustless swap stack and fork test cleanups
Tighten EnhancedSwapRouter, InboxETH, SwapRouter, MerkleProofVerifier; align
DEXIntegration and ForkTests with updated behavior.

Made-with: Cursor
2026-04-12 06:44:20 -07:00

426 lines
15 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./BondManager.sol";
import "./ChallengeManager.sol";
import "./LiquidityPoolETH.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title InboxETH
* @notice Receives and processes claims from relayers for trustless bridge deposits
* @dev Permissionless claim submission requiring bonds and challenge mechanism
*/
contract InboxETH is ReentrancyGuard {
BondManager public immutable bondManager;
ChallengeManager public immutable challengeManager;
LiquidityPoolETH public immutable liquidityPool;
// Rate limiting
uint256 public constant MIN_DEPOSIT = 0.001 ether; // Minimum deposit to prevent dust
uint256 public constant COOLDOWN_PERIOD = 60 seconds; // Cooldown between claims per relayer
mapping(address => uint256) public lastClaimTime; // relayer => last claim timestamp
mapping(address => uint256) public claimsPerHour; // relayer => claims in current hour
mapping(address => uint256) public hourStart; // relayer => current hour start timestamp
uint256 public constant MAX_CLAIMS_PER_HOUR = 100; // Max claims per hour per relayer
// Relayer fees (optional, can be enabled via governance)
uint256 public relayerFeeBps = 0; // Basis points (0 = disabled, 100 = 1%)
mapping(uint256 => RelayerFee) public relayerFees; // depositId => RelayerFee
struct RelayerFee {
address relayer;
uint256 amount;
bool claimed;
}
struct ClaimData {
uint256 depositId;
address asset;
uint256 amount;
address recipient;
address relayer;
uint256 timestamp;
bool exists;
}
mapping(uint256 => ClaimData) public claims; // depositId => ClaimData
event RelayerFeeSet(uint256 newFeeBps);
event RelayerFeeClaimed(uint256 indexed depositId, address indexed relayer, uint256 amount);
event ClaimSubmitted(
uint256 indexed depositId,
address indexed relayer,
address asset,
uint256 amount,
address indexed recipient,
uint256 bondAmount,
uint256 challengeWindowEnd
);
error ZeroDepositId();
error ZeroAsset();
error ZeroAmount();
error ZeroRecipient();
error ClaimAlreadyExists();
error InsufficientBond();
error DepositTooSmall();
error CooldownActive();
error RateLimitExceeded();
error RelayerFeeNotEnabled();
/**
* @notice Constructor
* @param _bondManager Address of BondManager contract
* @param _challengeManager Address of ChallengeManager contract
* @param _liquidityPool Address of LiquidityPoolETH contract
*/
constructor(
address _bondManager,
address _challengeManager,
address _liquidityPool
) {
require(_bondManager != address(0), "InboxETH: zero bond manager");
require(_challengeManager != address(0), "InboxETH: zero challenge manager");
require(_liquidityPool != address(0), "InboxETH: zero liquidity pool");
bondManager = BondManager(payable(_bondManager));
challengeManager = ChallengeManager(payable(_challengeManager));
liquidityPool = LiquidityPoolETH(payable(_liquidityPool));
}
/**
* @notice Submit a claim for a deposit from source chain
* @param depositId Deposit ID from source chain (ChainID 138)
* @param asset Asset address (address(0) for native ETH)
* @param amount Deposit amount
* @param recipient Recipient address on Ethereum
* @return bondAmount Amount of bond posted
*/
function submitClaim(
uint256 depositId,
address asset,
uint256 amount,
address recipient,
bytes calldata
) external payable nonReentrant returns (uint256 bondAmount) {
if (depositId == 0) revert ZeroDepositId();
if (asset == address(0) && amount == 0) revert ZeroAmount();
if (recipient == address(0)) revert ZeroRecipient();
// Rate limiting checks
if (amount < MIN_DEPOSIT) revert DepositTooSmall();
// Cooldown check
if (block.timestamp < lastClaimTime[msg.sender] + COOLDOWN_PERIOD) {
revert CooldownActive();
}
// Hourly rate limit check
uint256 currentHour = block.timestamp / 3600;
if (hourStart[msg.sender] != currentHour) {
hourStart[msg.sender] = currentHour;
claimsPerHour[msg.sender] = 0;
}
if (claimsPerHour[msg.sender] >= MAX_CLAIMS_PER_HOUR) {
revert RateLimitExceeded();
}
// Check if claim already exists
if (claims[depositId].exists) revert ClaimAlreadyExists();
// Calculate required bond
uint256 requiredBond = bondManager.getRequiredBond(amount);
// Calculate relayer fee if enabled
uint256 relayerFee = 0;
uint256 bridgeAmount = amount;
if (relayerFeeBps > 0) {
relayerFee = (amount * relayerFeeBps) / 10000;
bridgeAmount = amount - relayerFee;
// Store relayer fee
relayerFees[depositId] = RelayerFee({
relayer: msg.sender,
amount: relayerFee,
claimed: false
});
}
if (msg.value < requiredBond) revert InsufficientBond();
// Post bond (pass relayer address explicitly)
bondAmount = bondManager.postBond{value: requiredBond}(depositId, bridgeAmount, msg.sender);
// Update rate limiting
lastClaimTime[msg.sender] = block.timestamp;
claimsPerHour[msg.sender]++;
// Register claim with ChallengeManager (use bridgeAmount after fee)
challengeManager.registerClaim(depositId, asset, bridgeAmount, recipient);
// Determine asset type for liquidity pool
LiquidityPoolETH.AssetType assetType = asset == address(0)
? LiquidityPoolETH.AssetType.ETH
: LiquidityPoolETH.AssetType.WETH;
// Add pending claim to liquidity pool (use bridgeAmount after fee deduction)
liquidityPool.addPendingClaim(bridgeAmount, assetType);
// Store claim data (use bridgeAmount for amount)
claims[depositId] = ClaimData({
depositId: depositId,
asset: asset,
amount: bridgeAmount, // Store bridge amount (after fee)
recipient: recipient,
relayer: msg.sender,
timestamp: block.timestamp,
exists: true
});
// Get challenge window end time
(uint256 challengeWindowEnd, ) = _getChallengeWindowEnd(depositId);
emit ClaimSubmitted(
depositId,
msg.sender,
asset,
bridgeAmount, // Emit bridge amount (after fee)
recipient,
bondAmount,
challengeWindowEnd
);
return bondAmount;
}
/**
* @notice Submit multiple claims in batch (gas optimization)
* @param depositIds Array of deposit IDs
* @param assets Array of asset addresses
* @param amounts Array of deposit amounts
* @param recipients Array of recipient addresses
* @param proofs Array of proof data
* @return totalBondAmount Total bond amount posted
*/
function submitClaimsBatch(
uint256[] calldata depositIds,
address[] calldata assets,
uint256[] calldata amounts,
address[] calldata recipients,
bytes[] calldata proofs
) external payable nonReentrant returns (uint256 totalBondAmount) {
uint256 length = depositIds.length;
require(length > 0, "InboxETH: empty array");
require(length <= 20, "InboxETH: batch too large"); // Prevent gas limit issues
require(length == assets.length && length == amounts.length &&
length == recipients.length && length == proofs.length,
"InboxETH: length mismatch");
// Calculate total required bond
uint256 totalRequiredBond = 0;
for (uint256 i = 0; i < length; i++) {
if (depositIds[i] == 0) revert ZeroDepositId();
if (assets[i] == address(0) && amounts[i] == 0) revert ZeroAmount();
if (recipients[i] == address(0)) revert ZeroRecipient();
if (claims[depositIds[i]].exists) revert ClaimAlreadyExists();
totalRequiredBond += bondManager.getRequiredBond(amounts[i]);
}
if (msg.value < totalRequiredBond) revert InsufficientBond();
// Process each claim
for (uint256 i = 0; i < length; i++) {
// Rate limiting checks (simplified for batch - check first item)
if (i == 0) {
if (amounts[i] < MIN_DEPOSIT) revert DepositTooSmall();
if (block.timestamp < lastClaimTime[msg.sender] + COOLDOWN_PERIOD) {
revert CooldownActive();
}
uint256 currentHour = block.timestamp / 3600;
if (hourStart[msg.sender] != currentHour) {
hourStart[msg.sender] = currentHour;
claimsPerHour[msg.sender] = 0;
}
}
if (claimsPerHour[msg.sender] + i >= MAX_CLAIMS_PER_HOUR) {
revert RateLimitExceeded();
}
// Calculate relayer fee if enabled
uint256 relayerFee = 0;
uint256 bridgeAmount = amounts[i];
if (relayerFeeBps > 0) {
relayerFee = (amounts[i] * relayerFeeBps) / 10000;
bridgeAmount = amounts[i] - relayerFee;
relayerFees[depositIds[i]] = RelayerFee({
relayer: msg.sender,
amount: relayerFee,
claimed: false
});
}
uint256 requiredBond = bondManager.getRequiredBond(bridgeAmount);
// Post bond
uint256 bondAmount = bondManager.postBond{value: requiredBond}(
depositIds[i],
bridgeAmount,
msg.sender
);
totalBondAmount += bondAmount;
// Register claim (use bridgeAmount)
challengeManager.registerClaim(depositIds[i], assets[i], bridgeAmount, recipients[i]);
// Determine asset type
LiquidityPoolETH.AssetType assetType = assets[i] == address(0)
? LiquidityPoolETH.AssetType.ETH
: LiquidityPoolETH.AssetType.WETH;
// Add pending claim (use bridgeAmount)
liquidityPool.addPendingClaim(bridgeAmount, assetType);
// Store claim data (use bridgeAmount)
claims[depositIds[i]] = ClaimData({
depositId: depositIds[i],
asset: assets[i],
amount: bridgeAmount,
recipient: recipients[i],
relayer: msg.sender,
timestamp: block.timestamp,
exists: true
});
// Get challenge window end time
(uint256 challengeWindowEnd, ) = _getChallengeWindowEnd(depositIds[i]);
emit ClaimSubmitted(
depositIds[i],
msg.sender,
assets[i],
bridgeAmount,
recipients[i],
bondAmount,
challengeWindowEnd
);
}
// Update rate limiting
lastClaimTime[msg.sender] = block.timestamp;
claimsPerHour[msg.sender] += length;
// Refund excess bond if any
if (msg.value > totalBondAmount) {
(bool success, ) = payable(msg.sender).call{value: msg.value - totalBondAmount}("");
require(success, "InboxETH: refund failed");
}
return totalBondAmount;
}
/**
* @notice Get claim status
* @param depositId Deposit ID
* @return exists True if claim exists
* @return finalized True if claim is finalized
* @return challenged True if claim was challenged
* @return challengeWindowEnd Timestamp when challenge window ends
*/
function getClaimStatus(
uint256 depositId
) external view returns (
bool exists,
bool finalized,
bool challenged,
uint256 challengeWindowEnd
) {
if (!claims[depositId].exists) {
return (false, false, false, 0);
}
ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId);
(challengeWindowEnd, ) = _getChallengeWindowEnd(depositId);
return (
true,
claim.finalized,
claim.challenged,
challengeWindowEnd
);
}
/**
* @notice Get claim data
* @param depositId Deposit ID
* @return Claim data
*/
function getClaim(uint256 depositId) external view returns (ClaimData memory) {
return claims[depositId];
}
/**
* @notice Internal function to get challenge window end time
* @param depositId Deposit ID
* @return challengeWindowEnd Timestamp
* @return exists True if claim exists
*/
function _getChallengeWindowEnd(
uint256 depositId
) internal view returns (uint256 challengeWindowEnd, bool exists) {
ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId);
if (claim.depositId == 0) {
return (0, false);
}
return (claim.challengeWindowEnd, true);
}
/**
* @notice Set relayer fee (only callable by owner/multisig in future upgrade)
* @param _relayerFeeBps New relayer fee in basis points (0 = disabled)
*/
function setRelayerFee(uint256 _relayerFeeBps) external {
// Note: In production, add access control (owner/multisig)
// For now, this is a placeholder for future governance
require(_relayerFeeBps <= 1000, "InboxETH: fee too high"); // Max 10%
relayerFeeBps = _relayerFeeBps;
emit RelayerFeeSet(_relayerFeeBps);
}
/**
* @notice Claim relayer fee for a finalized deposit
* @param depositId Deposit ID to claim fee for
*/
function claimRelayerFee(uint256 depositId) external nonReentrant {
if (relayerFeeBps == 0) revert RelayerFeeNotEnabled();
RelayerFee storage fee = relayerFees[depositId];
if (fee.relayer == address(0)) revert("InboxETH: no fee for deposit");
if (fee.claimed) revert("InboxETH: fee already claimed");
if (fee.relayer != msg.sender) revert("InboxETH: not fee recipient");
// Verify claim is finalized
ChallengeManager.Claim memory claim = challengeManager.getClaim(depositId);
if (!claim.finalized) revert("InboxETH: claim not finalized");
fee.claimed = true;
// Transfer fee to relayer
(bool success, ) = payable(msg.sender).call{value: fee.amount}("");
require(success, "InboxETH: fee transfer failed");
emit RelayerFeeClaimed(depositId, msg.sender, fee.amount);
}
/**
* @notice Get relayer fee for a deposit
* @param depositId Deposit ID
* @return fee Relayer fee information
*/
function getRelayerFee(uint256 depositId) external view returns (RelayerFee memory) {
return relayerFees[depositId];
}
}