Tighten EnhancedSwapRouter, InboxETH, SwapRouter, MerkleProofVerifier; align DEXIntegration and ForkTests with updated behavior. Made-with: Cursor
426 lines
15 KiB
Solidity
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];
|
|
}
|
|
}
|