393 lines
18 KiB
Solidity
393 lines
18 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
import {AtomicTypes} from "./AtomicTypes.sol";
|
|
import {IAtomicBridgeCoordinator} from "./interfaces/IAtomicBridgeCoordinator.sol";
|
|
import {IAtomicLiquidityVault} from "./interfaces/IAtomicLiquidityVault.sol";
|
|
import {IAtomicFulfillerRegistry} from "./interfaces/IAtomicFulfillerRegistry.sol";
|
|
import {AtomicObligationEscrow} from "./AtomicObligationEscrow.sol";
|
|
import {AtomicSettlementRouter} from "./AtomicSettlementRouter.sol";
|
|
import {AtomicFeePolicy} from "./AtomicFeePolicy.sol";
|
|
import {AtomicSlashingManager} from "./AtomicSlashingManager.sol";
|
|
|
|
contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridgeCoordinator {
|
|
bytes32 public constant CORRIDOR_MANAGER_ROLE = keccak256("CORRIDOR_MANAGER_ROLE");
|
|
bytes32 public constant SETTLEMENT_MANAGER_ROLE = keccak256("SETTLEMENT_MANAGER_ROLE");
|
|
bytes32 public constant INTENT_ADAPTER_ROLE = keccak256("INTENT_ADAPTER_ROLE");
|
|
|
|
IAtomicLiquidityVault public immutable liquidityVault;
|
|
IAtomicFulfillerRegistry public immutable fulfillerRegistry;
|
|
AtomicObligationEscrow public immutable obligationEscrow;
|
|
AtomicSettlementRouter public immutable settlementRouter;
|
|
AtomicFeePolicy public immutable feePolicy;
|
|
AtomicSlashingManager public immutable slashingManager;
|
|
address public immutable protocolTreasury;
|
|
|
|
struct CreateIntentParams {
|
|
uint64 sourceChain;
|
|
uint64 destinationChain;
|
|
address assetIn;
|
|
address assetOut;
|
|
uint256 amountIn;
|
|
uint256 minAmountOut;
|
|
address recipient;
|
|
uint256 deadline;
|
|
bytes32 routeId;
|
|
}
|
|
|
|
uint256 public intentNonce;
|
|
mapping(bytes32 => AtomicTypes.CorridorConfig) private _corridors;
|
|
mapping(bytes32 => AtomicTypes.AtomicIntent) private _intents;
|
|
mapping(bytes32 => AtomicTypes.AtomicCommitment) private _commitments;
|
|
mapping(bytes32 => AtomicTypes.AtomicObligation) private _obligations;
|
|
mapping(bytes32 => address) private _refundRecipients;
|
|
mapping(bytes32 => bool) private _asyncDestinationCorridors;
|
|
|
|
event CorridorConfigured(bytes32 indexed corridorId, address indexed assetIn, address indexed assetOut);
|
|
event AsyncDestinationCorridorSet(bytes32 indexed corridorId, bool enabled);
|
|
event IntentCreated(
|
|
bytes32 indexed obligationId, bytes32 indexed corridorId, bytes32 indexed intentId, address sender
|
|
);
|
|
event CommitmentAccepted(bytes32 indexed obligationId, address indexed fulfiller, uint256 bondAmount);
|
|
event DestinationFulfillmentRecorded(
|
|
bytes32 indexed obligationId, bytes32 indexed destinationTxHash, bytes32 indexed invoiceHash, address fulfiller
|
|
);
|
|
event SettlementInitiated(
|
|
bytes32 indexed obligationId, bytes32 indexed settlementId, bytes32 indexed settlementMode
|
|
);
|
|
event SettlementConfirmed(bytes32 indexed obligationId, uint256 replenishAmount);
|
|
event IntentRefunded(bytes32 indexed obligationId, uint256 refundedAmount);
|
|
event CorridorDegraded(bytes32 indexed corridorId, bytes32 indexed obligationId, bytes32 reason);
|
|
|
|
error CorridorDisabled();
|
|
error CorridorDegradedError();
|
|
error InvalidCorridor();
|
|
error InvalidDeadline();
|
|
error MaxNotionalExceeded();
|
|
error ReservedLiquidityLimitExceeded();
|
|
error SettlementBacklogExceeded();
|
|
error InvalidStatus();
|
|
error DeadlineNotReached();
|
|
error SettlementTimeoutNotReached();
|
|
error MinimumReplenishNotMet();
|
|
error ZeroPayer();
|
|
error ZeroRefundRecipient();
|
|
error AsyncDestinationProofRequired();
|
|
error NotAsyncDestinationCorridor();
|
|
error ZeroDestinationProof();
|
|
|
|
constructor(
|
|
address liquidityVault_,
|
|
address fulfillerRegistry_,
|
|
address obligationEscrow_,
|
|
address settlementRouter_,
|
|
address feePolicy_,
|
|
address slashingManager_,
|
|
address protocolTreasury_,
|
|
address admin
|
|
) {
|
|
liquidityVault = IAtomicLiquidityVault(liquidityVault_);
|
|
fulfillerRegistry = IAtomicFulfillerRegistry(fulfillerRegistry_);
|
|
obligationEscrow = AtomicObligationEscrow(obligationEscrow_);
|
|
settlementRouter = AtomicSettlementRouter(settlementRouter_);
|
|
feePolicy = AtomicFeePolicy(feePolicy_);
|
|
slashingManager = AtomicSlashingManager(slashingManager_);
|
|
protocolTreasury = protocolTreasury_;
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
_grantRole(CORRIDOR_MANAGER_ROLE, admin);
|
|
_grantRole(SETTLEMENT_MANAGER_ROLE, admin);
|
|
}
|
|
|
|
function getCorridorId(uint64 sourceChain, uint64 destinationChain, address assetIn, address assetOut)
|
|
public
|
|
pure
|
|
returns (bytes32)
|
|
{
|
|
return keccak256(abi.encode(sourceChain, destinationChain, assetIn, assetOut));
|
|
}
|
|
|
|
function configureCorridor(AtomicTypes.CorridorConfig calldata cfg) external onlyRole(CORRIDOR_MANAGER_ROLE) {
|
|
bytes32 corridorId = getCorridorId(cfg.sourceChain, cfg.destinationChain, cfg.assetIn, cfg.assetOut);
|
|
_corridors[corridorId] = cfg;
|
|
emit CorridorConfigured(corridorId, cfg.assetIn, cfg.assetOut);
|
|
}
|
|
|
|
function setCorridorDegraded(bytes32 corridorId, bool degraded) external onlyRole(CORRIDOR_MANAGER_ROLE) {
|
|
_corridors[corridorId].degraded = degraded;
|
|
}
|
|
|
|
function setAsyncDestinationCorridor(bytes32 corridorId, bool enabled) external onlyRole(CORRIDOR_MANAGER_ROLE) {
|
|
_asyncDestinationCorridors[corridorId] = enabled;
|
|
emit AsyncDestinationCorridorSet(corridorId, enabled);
|
|
}
|
|
|
|
function createIntent(CreateIntentParams calldata p) external nonReentrant returns (bytes32 obligationId) {
|
|
obligationId = _createIntent(p, msg.sender, msg.sender);
|
|
}
|
|
|
|
function createIntentFor(CreateIntentParams calldata p, address payer, address refundTo)
|
|
external
|
|
nonReentrant
|
|
onlyRole(INTENT_ADAPTER_ROLE)
|
|
returns (bytes32 obligationId)
|
|
{
|
|
if (payer == address(0)) revert ZeroPayer();
|
|
if (refundTo == address(0)) revert ZeroRefundRecipient();
|
|
obligationId = _createIntent(p, payer, refundTo);
|
|
}
|
|
|
|
function _createIntent(CreateIntentParams calldata p, address payer, address refundTo)
|
|
internal
|
|
returns (bytes32 obligationId)
|
|
{
|
|
bytes32 corridorId = getCorridorId(p.sourceChain, p.destinationChain, p.assetIn, p.assetOut);
|
|
AtomicTypes.CorridorConfig memory cfg = _corridors[corridorId];
|
|
if (!cfg.enabled) revert CorridorDisabled();
|
|
if (cfg.degraded) revert CorridorDegradedError();
|
|
if (cfg.assetIn != p.assetIn || cfg.assetOut != p.assetOut) revert InvalidCorridor();
|
|
if (p.deadline <= block.timestamp || p.deadline > block.timestamp + cfg.fulfilmentTimeout) {
|
|
revert InvalidDeadline();
|
|
}
|
|
if (p.amountIn > cfg.maxNotional) revert MaxNotionalExceeded();
|
|
|
|
bool asyncDestination = _asyncDestinationCorridors[corridorId];
|
|
if (!asyncDestination) {
|
|
AtomicTypes.CorridorLiquidityState memory state =
|
|
liquidityVault.getCorridorLiquidityState(corridorId, p.assetOut);
|
|
if (p.minAmountOut > state.freeLiquidity) revert ReservedLiquidityLimitExceeded();
|
|
if (state.totalLiquidity > 0) {
|
|
uint256 nextReserved = state.reservedLiquidity + p.minAmountOut;
|
|
if ((nextReserved * 10_000) / state.totalLiquidity > cfg.maxReservedBps) {
|
|
revert ReservedLiquidityLimitExceeded();
|
|
}
|
|
}
|
|
if (state.settlementBacklog > cfg.maxSettlementBacklog) revert SettlementBacklogExceeded();
|
|
}
|
|
|
|
bytes32 intentId = keccak256(
|
|
abi.encode(
|
|
block.chainid, payer, ++intentNonce, corridorId, p.amountIn, p.minAmountOut, p.deadline, p.routeId
|
|
)
|
|
);
|
|
obligationId = keccak256(abi.encode(intentId, p.recipient, refundTo));
|
|
|
|
_intents[obligationId] = AtomicTypes.AtomicIntent({
|
|
sourceChain: p.sourceChain,
|
|
destinationChain: p.destinationChain,
|
|
assetIn: p.assetIn,
|
|
assetOut: p.assetOut,
|
|
amountIn: p.amountIn,
|
|
minAmountOut: p.minAmountOut,
|
|
recipient: p.recipient,
|
|
deadline: p.deadline,
|
|
routeId: p.routeId,
|
|
intentId: intentId
|
|
});
|
|
_obligations[obligationId] = AtomicTypes.AtomicObligation({
|
|
obligationId: obligationId,
|
|
intentId: intentId,
|
|
sourceEscrow: p.amountIn,
|
|
destinationReserve: p.minAmountOut,
|
|
fulfiller: address(0),
|
|
status: AtomicTypes.ObligationStatus.IntentCreated,
|
|
settlementInitiatedAt: 0
|
|
});
|
|
|
|
_refundRecipients[obligationId] = refundTo;
|
|
obligationEscrow.escrowFunds(obligationId, p.assetIn, payer, p.amountIn);
|
|
if (!asyncDestination) {
|
|
liquidityVault.reserveLiquidity(corridorId, p.assetOut, obligationId, p.minAmountOut);
|
|
}
|
|
|
|
emit IntentCreated(obligationId, corridorId, intentId, payer);
|
|
}
|
|
|
|
function submitCommitment(bytes32 obligationId, bytes32 settlementMode) external nonReentrant {
|
|
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
|
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
|
if (_asyncDestinationCorridors[corridorId]) revert AsyncDestinationProofRequired();
|
|
_submitCommitment(obligationId, settlementMode, msg.sender, true);
|
|
}
|
|
|
|
function submitAsyncDestinationFulfillment(
|
|
bytes32 obligationId,
|
|
bytes32 settlementMode,
|
|
bytes32 destinationTxHash,
|
|
bytes32 invoiceHash
|
|
) external nonReentrant {
|
|
if (destinationTxHash == bytes32(0)) revert ZeroDestinationProof();
|
|
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
|
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
|
if (!_asyncDestinationCorridors[corridorId]) revert NotAsyncDestinationCorridor();
|
|
_submitCommitment(obligationId, settlementMode, msg.sender, false);
|
|
emit DestinationFulfillmentRecorded(obligationId, destinationTxHash, invoiceHash, msg.sender);
|
|
}
|
|
|
|
function _submitCommitment(
|
|
bytes32 obligationId,
|
|
bytes32 settlementMode,
|
|
address fulfiller,
|
|
bool releaseLocalLiquidity
|
|
) internal {
|
|
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
|
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
|
if (obligation.status != AtomicTypes.ObligationStatus.IntentCreated) revert InvalidStatus();
|
|
if (block.timestamp > intent.deadline) revert DeadlineNotReached();
|
|
|
|
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
|
bytes32 mode = settlementMode == bytes32(0) ? _corridors[corridorId].defaultSettlementMode : settlementMode;
|
|
uint256 requiredBond = feePolicy.requiredBond(corridorId, obligation.destinationReserve);
|
|
fulfillerRegistry.lockBond(obligationId, fulfiller, corridorId, requiredBond);
|
|
if (releaseLocalLiquidity) {
|
|
liquidityVault.fulfillReservedLiquidity(obligationId, intent.recipient);
|
|
}
|
|
|
|
_commitments[obligationId] = AtomicTypes.AtomicCommitment({
|
|
intentId: intent.intentId,
|
|
fulfiller: fulfiller,
|
|
reservedLiquidity: obligation.destinationReserve,
|
|
bondAmount: requiredBond,
|
|
expiry: block.timestamp + _corridors[corridorId].settlementTimeout,
|
|
settlementMode: mode
|
|
});
|
|
obligation.fulfiller = fulfiller;
|
|
obligation.status = AtomicTypes.ObligationStatus.Fulfilled;
|
|
|
|
emit CommitmentAccepted(obligationId, fulfiller, requiredBond);
|
|
}
|
|
|
|
function initiateSettlement(bytes32 obligationId, bytes calldata settlementData)
|
|
external
|
|
payable
|
|
nonReentrant
|
|
onlyRole(SETTLEMENT_MANAGER_ROLE)
|
|
returns (bytes32 settlementId)
|
|
{
|
|
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
|
AtomicTypes.AtomicCommitment memory commitment = _commitments[obligationId];
|
|
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
|
if (obligation.status != AtomicTypes.ObligationStatus.Fulfilled) revert InvalidStatus();
|
|
|
|
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
|
uint256 settlementAmount =
|
|
_releaseEscrowForSettlement(obligationId, corridorId, intent.amountIn, commitment.fulfiller);
|
|
settlementId = _executeSettlement(
|
|
obligationId, commitment.settlementMode, intent.assetIn, settlementAmount, intent.recipient, settlementData
|
|
);
|
|
|
|
obligation.status = AtomicTypes.ObligationStatus.SettlementPending;
|
|
obligation.settlementInitiatedAt = block.timestamp;
|
|
emit SettlementInitiated(obligationId, settlementId, commitment.settlementMode);
|
|
}
|
|
|
|
function _releaseEscrowForSettlement(bytes32 obligationId, bytes32 corridorId, uint256 amountIn, address fulfiller)
|
|
internal
|
|
returns (uint256 settlementAmount)
|
|
{
|
|
(uint256 fulfillerFee, uint256 protocolFee) = feePolicy.quoteFees(corridorId, amountIn);
|
|
settlementAmount = amountIn - fulfillerFee - protocolFee;
|
|
|
|
if (protocolFee > 0) {
|
|
obligationEscrow.release(obligationId, protocolTreasury, protocolFee);
|
|
}
|
|
if (fulfillerFee > 0) {
|
|
obligationEscrow.release(obligationId, fulfiller, fulfillerFee);
|
|
}
|
|
obligationEscrow.release(obligationId, address(settlementRouter), settlementAmount);
|
|
}
|
|
|
|
function _executeSettlement(
|
|
bytes32 obligationId,
|
|
bytes32 settlementMode,
|
|
address assetIn,
|
|
uint256 settlementAmount,
|
|
address recipient,
|
|
bytes calldata settlementData
|
|
) internal returns (bytes32 settlementId) {
|
|
settlementId = settlementRouter.executeSettlement{value: msg.value}(
|
|
obligationId, settlementMode, assetIn, settlementAmount, recipient, settlementData
|
|
);
|
|
}
|
|
|
|
function confirmSettlement(bytes32 obligationId, uint256 replenishAmount)
|
|
external
|
|
nonReentrant
|
|
onlyRole(SETTLEMENT_MANAGER_ROLE)
|
|
{
|
|
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
|
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
|
if (obligation.status != AtomicTypes.ObligationStatus.SettlementPending) revert InvalidStatus();
|
|
if (replenishAmount < obligation.destinationReserve) revert MinimumReplenishNotMet();
|
|
|
|
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
|
if (!_asyncDestinationCorridors[corridorId]) {
|
|
liquidityVault.reconcileSettlement(corridorId, intent.assetOut, replenishAmount, msg.sender);
|
|
}
|
|
fulfillerRegistry.releaseBond(obligationId);
|
|
obligation.status = AtomicTypes.ObligationStatus.Settled;
|
|
emit SettlementConfirmed(obligationId, replenishAmount);
|
|
}
|
|
|
|
function refundExpiredIntent(bytes32 obligationId) external nonReentrant {
|
|
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
|
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
|
if (obligation.status != AtomicTypes.ObligationStatus.IntentCreated) revert InvalidStatus();
|
|
if (block.timestamp <= intent.deadline) revert DeadlineNotReached();
|
|
|
|
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
|
(, address payer,,,) = obligationEscrow.escrows(obligationId);
|
|
address refundTo = _refundRecipients[obligationId];
|
|
if (refundTo == address(0)) {
|
|
refundTo = payer;
|
|
}
|
|
if (!_asyncDestinationCorridors[corridorId]) {
|
|
liquidityVault.releaseReservation(obligationId);
|
|
}
|
|
uint256 refunded = obligationEscrow.refundRemaining(obligationId, refundTo);
|
|
_corridors[corridorId].degraded = true;
|
|
obligation.status = AtomicTypes.ObligationStatus.Refunded;
|
|
emit IntentRefunded(obligationId, refunded);
|
|
emit CorridorDegraded(corridorId, obligationId, keccak256("FULFILMENT_TIMEOUT"));
|
|
}
|
|
|
|
function handleSettlementTimeout(bytes32 obligationId) external nonReentrant {
|
|
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
|
AtomicTypes.AtomicCommitment memory commitment = _commitments[obligationId];
|
|
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
|
if (obligation.status != AtomicTypes.ObligationStatus.SettlementPending) revert InvalidStatus();
|
|
if (block.timestamp <= commitment.expiry) revert SettlementTimeoutNotReached();
|
|
|
|
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
|
_corridors[corridorId].degraded = true;
|
|
slashingManager.slash(obligationId, protocolTreasury, keccak256("SETTLEMENT_TIMEOUT"));
|
|
obligation.status = AtomicTypes.ObligationStatus.Slashed;
|
|
emit CorridorDegraded(corridorId, obligationId, keccak256("SETTLEMENT_TIMEOUT"));
|
|
}
|
|
|
|
function getCorridorConfig(bytes32 corridorId) external view returns (AtomicTypes.CorridorConfig memory) {
|
|
return _corridors[corridorId];
|
|
}
|
|
|
|
function getIntent(bytes32 obligationId) external view returns (AtomicTypes.AtomicIntent memory) {
|
|
return _intents[obligationId];
|
|
}
|
|
|
|
function getCommitment(bytes32 obligationId) external view returns (AtomicTypes.AtomicCommitment memory) {
|
|
return _commitments[obligationId];
|
|
}
|
|
|
|
function getObligation(bytes32 obligationId) external view returns (AtomicTypes.AtomicObligation memory) {
|
|
return _obligations[obligationId];
|
|
}
|
|
|
|
function getRefundRecipient(bytes32 obligationId) external view returns (address) {
|
|
return _refundRecipients[obligationId];
|
|
}
|
|
|
|
function isAsyncDestinationCorridor(bytes32 corridorId) external view returns (bool) {
|
|
return _asyncDestinationCorridors[corridorId];
|
|
}
|
|
}
|