// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /** * @title BondManager * @notice Manages bonds for trustless bridge claims with dynamic sizing and slashing * @dev Bonds are posted in ETH. Slashed bonds split 50% to challenger, 50% burned (sent to address(0)). */ contract BondManager is ReentrancyGuard { // Bond configuration uint256 public immutable bondMultiplier; // Basis points (11000 = 110%) uint256 public immutable minBond; // Minimum bond amount in wei // Bond tracking struct Bond { address relayer; // Slot 0 (20 bytes) + 12 bytes padding uint256 amount; // Slot 1 uint256 depositId; // Slot 2 bool slashed; // Slot 3 (1 byte) + 31 bytes padding bool released; // Slot 4 (1 byte) + 31 bytes padding // Note: Could pack slashed and released in same slot, but keeping separate for clarity } mapping(uint256 => Bond) public bonds; // depositId => Bond mapping(address => uint256) public totalBonds; // relayer => total bonded amount /// @notice Total ETH held in active (non-released, non-slashed) bonds. Used for value-conservation invariant. uint256 public totalEthHeld; event BondPosted( uint256 indexed depositId, address indexed relayer, uint256 bondAmount ); event BondSlashed( uint256 indexed depositId, address indexed relayer, address indexed challenger, uint256 bondAmount, uint256 challengerReward, uint256 burnedAmount ); event BondReleased( uint256 indexed depositId, address indexed relayer, uint256 bondAmount ); error ZeroDepositId(); error ZeroRelayer(); error InsufficientBond(); error BondNotFound(); error BondAlreadySlashed(); error BondAlreadyReleased(); error BondNotReleased(); /** * @notice Constructor sets bond parameters * @param _bondMultiplier Bond multiplier in basis points (11000 = 110% = 1.1x) * @param _minBond Minimum bond amount in wei */ constructor(uint256 _bondMultiplier, uint256 _minBond) { require(_bondMultiplier >= 10000, "BondManager: multiplier must be >= 100%"); require(_minBond > 0, "BondManager: minBond must be > 0"); bondMultiplier = _bondMultiplier; minBond = _minBond; } /** * @notice Post bond for a claim * @param depositId Deposit ID from source chain * @param depositAmount Amount of the deposit (used to calculate bond size) * @param relayer Address of the relayer posting the bond (can be different from msg.sender if called by InboxETH) * @return bondAmount The bond amount that was posted */ function postBond( uint256 depositId, uint256 depositAmount, address relayer ) external payable nonReentrant returns (uint256) { if (depositId == 0) revert ZeroDepositId(); if (relayer == address(0)) revert ZeroRelayer(); // Check if bond already exists require(bonds[depositId].relayer == address(0), "BondManager: bond already posted"); // Calculate required bond amount uint256 requiredBond = getRequiredBond(depositAmount); if (msg.value < requiredBond) revert InsufficientBond(); // Store bond information bonds[depositId] = Bond({ relayer: relayer, amount: msg.value, depositId: depositId, slashed: false, released: false }); totalBonds[relayer] += msg.value; totalEthHeld += msg.value; emit BondPosted(depositId, msg.sender, msg.value); return msg.value; } /** * @notice Slash bond due to fraudulent claim * @param depositId Deposit ID associated with the bond * @param challenger Address of the challenger proving fraud * @return challengerReward Amount sent to challenger * @return burnedAmount Amount burned (sent to address(0)) */ function slashBond( uint256 depositId, address challenger ) external nonReentrant returns (uint256 challengerReward, uint256 burnedAmount) { Bond storage bond = bonds[depositId]; if (bond.relayer == address(0)) revert BondNotFound(); if (bond.slashed) revert BondAlreadySlashed(); if (challenger == address(0)) revert ZeroRelayer(); // Mark bond as slashed bond.slashed = true; uint256 bondAmount = bond.amount; // Update relayer's total bonds and total ETH held totalBonds[bond.relayer] -= bondAmount; totalEthHeld -= bondAmount; // Split bond: 50% to challenger, 50% burned challengerReward = bondAmount / 2; burnedAmount = bondAmount - challengerReward; // Handle odd amounts // Transfer to challenger (bool success1, ) = payable(challenger).call{value: challengerReward}(""); require(success1, "BondManager: challenger transfer failed"); // Burn remaining amount (send to address(0)) // Note: In practice, sending ETH to address(0) doesn't actually burn it, // but it makes the funds inaccessible. For true burning, consider using a burn mechanism. (bool success2, ) = payable(address(0)).call{value: burnedAmount}(""); require(success2, "BondManager: burn transfer failed"); emit BondSlashed( depositId, bond.relayer, challenger, bondAmount, challengerReward, burnedAmount ); return (challengerReward, burnedAmount); } /** * @notice Release bond after successful claim finalization * @param depositId Deposit ID associated with the bond * @return bondAmount Amount returned to relayer */ function releaseBond( uint256 depositId ) external nonReentrant returns (uint256) { Bond storage bond = bonds[depositId]; if (bond.relayer == address(0)) revert BondNotFound(); if (bond.slashed) revert BondAlreadySlashed(); if (bond.released) revert BondAlreadyReleased(); // Mark bond as released bond.released = true; uint256 bondAmount = bond.amount; address relayer = bond.relayer; // Cache to save gas // Update relayer's total bonds totalBonds[relayer] -= bondAmount; // Transfer bond back to relayer (bool success, ) = payable(relayer).call{value: bondAmount}(""); require(success, "BondManager: release transfer failed"); emit BondReleased(depositId, relayer, bondAmount); return bondAmount; } /** * @notice Release multiple bonds in batch (gas optimization) * @param depositIds Array of deposit IDs to release bonds for * @return totalReleased Total amount released */ function releaseBondsBatch(uint256[] calldata depositIds) external nonReentrant returns (uint256 totalReleased) { uint256 length = depositIds.length; require(length > 0, "BondManager: empty array"); require(length <= 50, "BondManager: batch too large"); // Prevent gas limit issues for (uint256 i = 0; i < length; i++) { uint256 depositId = depositIds[i]; if (depositId == 0) continue; // Skip zero IDs Bond storage bond = bonds[depositId]; if (bond.relayer == address(0)) continue; // Skip non-existent bonds if (bond.slashed) continue; // Skip slashed bonds if (bond.released) continue; // Skip already released bond.released = true; uint256 bondAmount = bond.amount; address relayer = bond.relayer; // Cache to save gas totalBonds[relayer] -= bondAmount; totalEthHeld -= bondAmount; totalReleased += bondAmount; (bool success, ) = payable(relayer).call{value: bondAmount}(""); require(success, "BondManager: release transfer failed"); emit BondReleased(depositId, relayer, bondAmount); } return totalReleased; } /** * @notice Calculate required bond amount for a deposit * @param depositAmount Amount of the deposit * @return requiredBond Minimum bond amount required */ function getRequiredBond(uint256 depositAmount) public view returns (uint256) { uint256 calculatedBond = (depositAmount * bondMultiplier) / 10000; return calculatedBond > minBond ? calculatedBond : minBond; } /** * @notice Get bond information for a deposit * @param depositId Deposit ID to check * @return relayer Address that posted the bond * @return amount Bond amount * @return slashed Whether bond has been slashed * @return released Whether bond has been released */ function getBond( uint256 depositId ) external view returns ( address relayer, uint256 amount, bool slashed, bool released ) { Bond memory bond = bonds[depositId]; return (bond.relayer, bond.amount, bond.slashed, bond.released); } /** * @notice Get total bonds posted by a relayer * @param relayer Address to check * @return Total amount of bonds posted */ function getTotalBonds(address relayer) external view returns (uint256) { return totalBonds[relayer]; } // Allow contract to receive ETH receive() external payable {} }