- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
80 lines
2.9 KiB
Solidity
80 lines
2.9 KiB
Solidity
// 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;
|
|
}
|
|
}
|