// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./interfaces/IRailEscrowVault.sol"; import "./libraries/RailTypes.sol"; /** * @title RailEscrowVault * @notice Holds tokens locked for outbound rail transfers * @dev Similar pattern to BridgeVault138. Manages per-trigger escrow tracking. */ contract RailEscrowVault is IRailEscrowVault, AccessControl { bytes32 public constant SETTLEMENT_OPERATOR_ROLE = keccak256("SETTLEMENT_OPERATOR_ROLE"); using SafeERC20 for IERC20; // token => triggerId => escrow amount mapping(address => mapping(uint256 => uint256)) private _escrow; // token => total escrow amount mapping(address => uint256) private _totalEscrow; /** * @notice Initializes the vault with an admin address * @param admin Address that will receive DEFAULT_ADMIN_ROLE */ constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); } /** * @notice Locks tokens for a rail transfer * @dev Requires SETTLEMENT_OPERATOR_ROLE. Transfers tokens from user to vault. * @param token Token address to lock * @param from Address to transfer tokens from * @param amount Amount to lock * @param triggerId The trigger ID associated with this escrow * @param rail The payment rail type */ function lock( address token, address from, uint256 amount, uint256 triggerId, RailTypes.Rail rail ) external override onlyRole(SETTLEMENT_OPERATOR_ROLE) { require(token != address(0), "RailEscrowVault: zero token"); require(from != address(0), "RailEscrowVault: zero from"); require(amount > 0, "RailEscrowVault: zero amount"); require(triggerId > 0, "RailEscrowVault: zero triggerId"); // Transfer tokens from user to vault IERC20(token).safeTransferFrom(from, address(this), amount); // Update escrow tracking _escrow[token][triggerId] += amount; _totalEscrow[token] += amount; emit Locked(token, from, amount, triggerId, uint8(rail)); } /** * @notice Releases escrowed tokens * @dev Requires SETTLEMENT_OPERATOR_ROLE. Transfers tokens from vault to recipient. * @param token Token address to release * @param to Recipient address * @param amount Amount to release * @param triggerId The trigger ID associated with this escrow */ function release( address token, address to, uint256 amount, uint256 triggerId ) external override onlyRole(SETTLEMENT_OPERATOR_ROLE) { require(token != address(0), "RailEscrowVault: zero token"); require(to != address(0), "RailEscrowVault: zero to"); require(amount > 0, "RailEscrowVault: zero amount"); require(triggerId > 0, "RailEscrowVault: zero triggerId"); require(_escrow[token][triggerId] >= amount, "RailEscrowVault: insufficient escrow"); // Update escrow tracking _escrow[token][triggerId] -= amount; _totalEscrow[token] -= amount; // Transfer tokens to recipient IERC20(token).safeTransfer(to, amount); emit Released(token, to, amount, triggerId); } /** * @notice Returns the escrow amount for a specific trigger * @param token Token address * @param triggerId The trigger ID * @return The escrow amount */ function getEscrowAmount(address token, uint256 triggerId) external view override returns (uint256) { return _escrow[token][triggerId]; } /** * @notice Returns the total escrow amount for a token * @param token Token address * @return The total escrow amount */ function getTotalEscrow(address token) external view override returns (uint256) { return _totalEscrow[token]; } }