// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract AtomicObligationEscrow is AccessControl { using SafeERC20 for IERC20; bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE"); struct EscrowRecord { address token; address payer; uint256 totalAmount; uint256 releasedAmount; bool exists; } mapping(bytes32 => EscrowRecord) public escrows; event EscrowFunded(bytes32 indexed obligationId, address indexed token, address indexed payer, uint256 amount); event EscrowReleased(bytes32 indexed obligationId, address indexed to, uint256 amount); event EscrowRefunded(bytes32 indexed obligationId, address indexed to, uint256 amount); error EscrowExists(); error EscrowMissing(); error InsufficientEscrow(); constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); } function escrowFunds(bytes32 obligationId, address token, address from, uint256 amount) external onlyRole(COORDINATOR_ROLE) { if (escrows[obligationId].exists) revert EscrowExists(); escrows[obligationId] = EscrowRecord({ token: token, payer: from, totalAmount: amount, releasedAmount: 0, exists: true }); IERC20(token).safeTransferFrom(from, address(this), amount); emit EscrowFunded(obligationId, token, from, amount); } function release(bytes32 obligationId, address to, uint256 amount) external onlyRole(COORDINATOR_ROLE) { EscrowRecord storage record = escrows[obligationId]; if (!record.exists) revert EscrowMissing(); uint256 availableAmount = record.totalAmount - record.releasedAmount; if (availableAmount < amount) revert InsufficientEscrow(); record.releasedAmount += amount; IERC20(record.token).safeTransfer(to, amount); emit EscrowReleased(obligationId, to, amount); } function refundRemaining(bytes32 obligationId, address to) external onlyRole(COORDINATOR_ROLE) returns (uint256 refunded) { EscrowRecord storage record = escrows[obligationId]; if (!record.exists) revert EscrowMissing(); refunded = record.totalAmount - record.releasedAmount; record.releasedAmount = record.totalAmount; IERC20(record.token).safeTransfer(to, refunded); emit EscrowRefunded(obligationId, to, refunded); } function remaining(bytes32 obligationId) external view returns (uint256) { EscrowRecord memory record = escrows[obligationId]; if (!record.exists) revert EscrowMissing(); return record.totalAmount - record.releasedAmount; } }