- 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
134 lines
5.3 KiB
Solidity
134 lines
5.3 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";
|
|
import {IAtomicFulfillerRegistry} from "./interfaces/IAtomicFulfillerRegistry.sol";
|
|
|
|
contract AtomicFulfillerRegistry is AccessControl, IAtomicFulfillerRegistry {
|
|
using SafeERC20 for IERC20;
|
|
|
|
bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE");
|
|
bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE");
|
|
bytes32 public constant AUTH_MANAGER_ROLE = keccak256("AUTH_MANAGER_ROLE");
|
|
|
|
struct LockedBond {
|
|
address fulfiller;
|
|
bytes32 corridorId;
|
|
uint256 amount;
|
|
bool active;
|
|
}
|
|
|
|
IERC20 public immutable bondToken;
|
|
|
|
mapping(address => uint256) public totalBond;
|
|
mapping(address => uint256) public lockedBond;
|
|
mapping(address => bool) public fulfillerActive;
|
|
mapping(address => mapping(bytes32 => bool)) public corridorAuthorization;
|
|
mapping(bytes32 => LockedBond) public lockedBonds;
|
|
|
|
event BondDeposited(address indexed fulfiller, uint256 amount);
|
|
event BondWithdrawn(address indexed fulfiller, address indexed recipient, uint256 amount);
|
|
event FulfillerActivationSet(address indexed fulfiller, bool active);
|
|
event CorridorAuthorizationSet(address indexed fulfiller, bytes32 indexed corridorId, bool authorized);
|
|
event BondLocked(bytes32 indexed obligationId, address indexed fulfiller, bytes32 indexed corridorId, uint256 amount);
|
|
event BondReleased(bytes32 indexed obligationId, address indexed fulfiller, uint256 amount);
|
|
event BondSlashed(bytes32 indexed obligationId, address indexed fulfiller, address indexed recipient, uint256 amount);
|
|
|
|
error InsufficientAvailableBond();
|
|
error UnauthorizedFulfiller();
|
|
error BondAlreadyLocked();
|
|
error BondMissing();
|
|
|
|
constructor(address bondToken_, address admin) {
|
|
bondToken = IERC20(bondToken_);
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
_grantRole(AUTH_MANAGER_ROLE, admin);
|
|
}
|
|
|
|
function depositBond(uint256 amount) external {
|
|
bondToken.safeTransferFrom(msg.sender, address(this), amount);
|
|
totalBond[msg.sender] += amount;
|
|
emit BondDeposited(msg.sender, amount);
|
|
}
|
|
|
|
function withdrawBond(uint256 amount, address recipient) external {
|
|
if (availableBond(msg.sender) < amount) revert InsufficientAvailableBond();
|
|
totalBond[msg.sender] -= amount;
|
|
bondToken.safeTransfer(recipient, amount);
|
|
emit BondWithdrawn(msg.sender, recipient, amount);
|
|
}
|
|
|
|
function setFulfillerActive(address fulfiller, bool active) external onlyRole(AUTH_MANAGER_ROLE) {
|
|
fulfillerActive[fulfiller] = active;
|
|
emit FulfillerActivationSet(fulfiller, active);
|
|
}
|
|
|
|
function setCorridorAuthorization(address fulfiller, bytes32 corridorId, bool authorized)
|
|
external
|
|
onlyRole(AUTH_MANAGER_ROLE)
|
|
{
|
|
corridorAuthorization[fulfiller][corridorId] = authorized;
|
|
emit CorridorAuthorizationSet(fulfiller, corridorId, authorized);
|
|
}
|
|
|
|
function lockBond(bytes32 obligationId, address fulfiller, bytes32 corridorId, uint256 amount)
|
|
external
|
|
onlyRole(COORDINATOR_ROLE)
|
|
{
|
|
if (lockedBonds[obligationId].active) revert BondAlreadyLocked();
|
|
if (!isFulfillerAuthorized(fulfiller, corridorId)) revert UnauthorizedFulfiller();
|
|
if (availableBond(fulfiller) < amount) revert InsufficientAvailableBond();
|
|
|
|
lockedBond[fulfiller] += amount;
|
|
lockedBonds[obligationId] = LockedBond({
|
|
fulfiller: fulfiller,
|
|
corridorId: corridorId,
|
|
amount: amount,
|
|
active: true
|
|
});
|
|
emit BondLocked(obligationId, fulfiller, corridorId, amount);
|
|
}
|
|
|
|
function releaseBond(bytes32 obligationId)
|
|
external
|
|
onlyRole(COORDINATOR_ROLE)
|
|
returns (uint256 amount)
|
|
{
|
|
LockedBond memory entry = lockedBonds[obligationId];
|
|
if (!entry.active) revert BondMissing();
|
|
lockedBond[entry.fulfiller] -= entry.amount;
|
|
delete lockedBonds[obligationId];
|
|
emit BondReleased(obligationId, entry.fulfiller, entry.amount);
|
|
return entry.amount;
|
|
}
|
|
|
|
function slashBond(bytes32 obligationId, address recipient)
|
|
external
|
|
onlyRole(SLASHER_ROLE)
|
|
returns (uint256 amount)
|
|
{
|
|
LockedBond memory entry = lockedBonds[obligationId];
|
|
if (!entry.active) revert BondMissing();
|
|
lockedBond[entry.fulfiller] -= entry.amount;
|
|
totalBond[entry.fulfiller] -= entry.amount;
|
|
delete lockedBonds[obligationId];
|
|
bondToken.safeTransfer(recipient, entry.amount);
|
|
emit BondSlashed(obligationId, entry.fulfiller, recipient, entry.amount);
|
|
return entry.amount;
|
|
}
|
|
|
|
function availableBond(address fulfiller) public view returns (uint256) {
|
|
return totalBond[fulfiller] - lockedBond[fulfiller];
|
|
}
|
|
|
|
function isFulfillerAuthorized(address fulfiller, bytes32 corridorId) public view returns (bool) {
|
|
return fulfillerActive[fulfiller] && corridorAuthorization[fulfiller][corridorId];
|
|
}
|
|
|
|
function canCover(address fulfiller, uint256 amount) external view returns (bool) {
|
|
return availableBond(fulfiller) >= amount;
|
|
}
|
|
}
|