- Introduced Aggregator.sol for Chainlink-compatible oracle functionality, including round-based updates and access control. - Added OracleWithCCIP.sol to extend Aggregator with CCIP cross-chain messaging capabilities. - Created .gitmodules to include OpenZeppelin contracts as a submodule. - Developed a comprehensive deployment guide in NEXT_STEPS_COMPLETE_GUIDE.md for Phase 2 and smart contract deployment. - Implemented Vite configuration for the orchestration portal, supporting both Vue and React frameworks. - Added server-side logic for the Multi-Cloud Orchestration Portal, including API endpoints for environment management and monitoring. - Created scripts for resource import and usage validation across non-US regions. - Added tests for CCIP error handling and integration to ensure robust functionality. - Included various new files and directories for the orchestration portal and deployment scripts.
363 lines
16 KiB
Solidity
363 lines
16 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "./interfaces/ISettlementOrchestrator.sol";
|
|
import "./interfaces/IRailTriggerRegistry.sol";
|
|
import "./interfaces/IRailEscrowVault.sol";
|
|
import "./interfaces/IAccountWalletRegistry.sol";
|
|
import "./interfaces/IPolicyManager.sol";
|
|
import "./interfaces/IDebtRegistry.sol";
|
|
import "./interfaces/IComplianceRegistry.sol";
|
|
import "./interfaces/IeMoneyToken.sol";
|
|
import "./libraries/RailTypes.sol";
|
|
import "./libraries/ISO20022Types.sol";
|
|
import "./libraries/ReasonCodes.sol";
|
|
|
|
/**
|
|
* @title SettlementOrchestrator
|
|
* @notice Coordinates trigger lifecycle and fund locking/release
|
|
* @dev Supports both vault and lien escrow modes. Integrates with PolicyManager, DebtRegistry, ComplianceRegistry.
|
|
*/
|
|
contract SettlementOrchestrator is ISettlementOrchestrator, AccessControl {
|
|
bytes32 public constant SETTLEMENT_OPERATOR_ROLE = keccak256("SETTLEMENT_OPERATOR_ROLE");
|
|
bytes32 public constant RAIL_ADAPTER_ROLE = keccak256("RAIL_ADAPTER_ROLE");
|
|
|
|
IRailTriggerRegistry public immutable triggerRegistry;
|
|
IRailEscrowVault public immutable escrowVault;
|
|
IAccountWalletRegistry public immutable accountWalletRegistry;
|
|
IPolicyManager public immutable policyManager;
|
|
IDebtRegistry public immutable debtRegistry;
|
|
IComplianceRegistry public immutable complianceRegistry;
|
|
|
|
// triggerId => escrow mode (1 = vault, 2 = lien)
|
|
mapping(uint256 => uint8) private _escrowModes;
|
|
// triggerId => rail transaction reference
|
|
mapping(uint256 => bytes32) private _railTxRefs;
|
|
// triggerId => lien ID (if using lien mode)
|
|
mapping(uint256 => uint256) private _triggerLiens;
|
|
// triggerId => locked account address (for lien mode)
|
|
mapping(uint256 => address) private _lockedAccounts;
|
|
|
|
// Rail-specific escrow mode configuration (default: vault)
|
|
mapping(RailTypes.Rail => uint8) private _railEscrowModes;
|
|
|
|
/**
|
|
* @notice Initializes the orchestrator with registry addresses
|
|
* @param admin Address that will receive DEFAULT_ADMIN_ROLE
|
|
* @param triggerRegistry_ Address of RailTriggerRegistry
|
|
* @param escrowVault_ Address of RailEscrowVault
|
|
* @param accountWalletRegistry_ Address of AccountWalletRegistry
|
|
* @param policyManager_ Address of PolicyManager
|
|
* @param debtRegistry_ Address of DebtRegistry
|
|
* @param complianceRegistry_ Address of ComplianceRegistry
|
|
*/
|
|
constructor(
|
|
address admin,
|
|
address triggerRegistry_,
|
|
address escrowVault_,
|
|
address accountWalletRegistry_,
|
|
address policyManager_,
|
|
address debtRegistry_,
|
|
address complianceRegistry_
|
|
) {
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
require(triggerRegistry_ != address(0), "SettlementOrchestrator: zero triggerRegistry");
|
|
require(escrowVault_ != address(0), "SettlementOrchestrator: zero escrowVault");
|
|
require(accountWalletRegistry_ != address(0), "SettlementOrchestrator: zero accountWalletRegistry");
|
|
require(policyManager_ != address(0), "SettlementOrchestrator: zero policyManager");
|
|
require(debtRegistry_ != address(0), "SettlementOrchestrator: zero debtRegistry");
|
|
require(complianceRegistry_ != address(0), "SettlementOrchestrator: zero complianceRegistry");
|
|
|
|
triggerRegistry = IRailTriggerRegistry(triggerRegistry_);
|
|
escrowVault = IRailEscrowVault(escrowVault_);
|
|
accountWalletRegistry = IAccountWalletRegistry(accountWalletRegistry_);
|
|
policyManager = IPolicyManager(policyManager_);
|
|
debtRegistry = IDebtRegistry(debtRegistry_);
|
|
complianceRegistry = IComplianceRegistry(complianceRegistry_);
|
|
|
|
// Set default escrow modes (can be changed by admin)
|
|
_railEscrowModes[RailTypes.Rail.FEDWIRE] = RailTypes.ESCROW_MODE_VAULT;
|
|
_railEscrowModes[RailTypes.Rail.SWIFT] = RailTypes.ESCROW_MODE_VAULT;
|
|
_railEscrowModes[RailTypes.Rail.SEPA] = RailTypes.ESCROW_MODE_VAULT;
|
|
_railEscrowModes[RailTypes.Rail.RTGS] = RailTypes.ESCROW_MODE_VAULT;
|
|
}
|
|
|
|
/**
|
|
* @notice Sets the escrow mode for a rail
|
|
* @dev Requires DEFAULT_ADMIN_ROLE
|
|
* @param rail The rail type
|
|
* @param mode The escrow mode (1 = vault, 2 = lien)
|
|
*/
|
|
function setRailEscrowMode(RailTypes.Rail rail, uint8 mode) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
require(mode == RailTypes.ESCROW_MODE_VAULT || mode == RailTypes.ESCROW_MODE_LIEN, "SettlementOrchestrator: invalid mode");
|
|
_railEscrowModes[rail] = mode;
|
|
}
|
|
|
|
/**
|
|
* @notice Validates a trigger and locks funds
|
|
* @dev Requires SETTLEMENT_OPERATOR_ROLE. Checks compliance, policy, and locks funds via vault or lien.
|
|
* @param triggerId The trigger ID
|
|
*/
|
|
function validateAndLock(uint256 triggerId) external override onlyRole(SETTLEMENT_OPERATOR_ROLE) {
|
|
IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId);
|
|
require(trigger.state == RailTypes.State.CREATED, "SettlementOrchestrator: invalid state");
|
|
|
|
// Resolve wallet address from walletRefId if needed (simplified - in production, use AccountWalletRegistry)
|
|
address accountAddress = _resolveAccountAddress(trigger.accountRefId);
|
|
require(accountAddress != address(0), "SettlementOrchestrator: cannot resolve account");
|
|
|
|
// Check compliance
|
|
require(complianceRegistry.isAllowed(accountAddress), "SettlementOrchestrator: account not compliant");
|
|
require(!complianceRegistry.isFrozen(accountAddress), "SettlementOrchestrator: account frozen");
|
|
|
|
// Check policy
|
|
(bool allowed, ) = policyManager.canTransfer(trigger.token, accountAddress, address(0), trigger.amount);
|
|
require(allowed, "SettlementOrchestrator: transfer blocked by policy");
|
|
|
|
// Determine escrow mode for this rail
|
|
uint8 escrowMode = _railEscrowModes[trigger.rail];
|
|
_escrowModes[triggerId] = escrowMode;
|
|
|
|
if (escrowMode == RailTypes.ESCROW_MODE_VAULT) {
|
|
// Lock funds in vault
|
|
escrowVault.lock(trigger.token, accountAddress, trigger.amount, triggerId, trigger.rail);
|
|
} else if (escrowMode == RailTypes.ESCROW_MODE_LIEN) {
|
|
// Place a temporary lien
|
|
uint256 lienId = debtRegistry.placeLien(
|
|
accountAddress,
|
|
trigger.amount,
|
|
0, // no expiry
|
|
100, // priority
|
|
ReasonCodes.LIEN_BLOCK
|
|
);
|
|
_triggerLiens[triggerId] = lienId;
|
|
_lockedAccounts[triggerId] = accountAddress;
|
|
}
|
|
|
|
// Update trigger state to VALIDATED
|
|
triggerRegistry.updateState(triggerId, RailTypes.State.VALIDATED, ReasonCodes.OK);
|
|
|
|
emit Validated(triggerId, trigger.accountRefId, trigger.token, trigger.amount);
|
|
}
|
|
|
|
/**
|
|
* @notice Marks a trigger as submitted to the rail
|
|
* @dev Requires RAIL_ADAPTER_ROLE. Records the rail transaction reference.
|
|
* @param triggerId The trigger ID
|
|
* @param railTxRef The rail transaction reference
|
|
*/
|
|
function markSubmitted(
|
|
uint256 triggerId,
|
|
bytes32 railTxRef
|
|
) external override onlyRole(RAIL_ADAPTER_ROLE) {
|
|
IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId);
|
|
require(
|
|
trigger.state == RailTypes.State.VALIDATED,
|
|
"SettlementOrchestrator: invalid state"
|
|
);
|
|
require(railTxRef != bytes32(0), "SettlementOrchestrator: zero railTxRef");
|
|
|
|
_railTxRefs[triggerId] = railTxRef;
|
|
|
|
// Update trigger state
|
|
triggerRegistry.updateState(triggerId, RailTypes.State.SUBMITTED_TO_RAIL, ReasonCodes.OK);
|
|
triggerRegistry.updateState(triggerId, RailTypes.State.PENDING, ReasonCodes.OK);
|
|
|
|
emit Submitted(triggerId, railTxRef);
|
|
}
|
|
|
|
/**
|
|
* @notice Confirms a trigger as settled
|
|
* @dev Requires RAIL_ADAPTER_ROLE. Releases escrow for outbound, mints for inbound.
|
|
* @param triggerId The trigger ID
|
|
* @param railTxRef The rail transaction reference (for verification)
|
|
*/
|
|
function confirmSettled(uint256 triggerId, bytes32 railTxRef) external override onlyRole(RAIL_ADAPTER_ROLE) {
|
|
IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId);
|
|
require(
|
|
trigger.state == RailTypes.State.PENDING || trigger.state == RailTypes.State.SUBMITTED_TO_RAIL,
|
|
"SettlementOrchestrator: invalid state"
|
|
);
|
|
require(_railTxRefs[triggerId] == railTxRef, "SettlementOrchestrator: railTxRef mismatch");
|
|
|
|
// Determine if this is inbound or outbound based on message type
|
|
bool isInbound = _isInboundMessage(trigger.msgType);
|
|
|
|
if (isInbound) {
|
|
// Inbound: mint tokens to the account
|
|
address recipient = _resolveAccountAddress(trigger.accountRefId);
|
|
require(recipient != address(0), "SettlementOrchestrator: cannot resolve recipient");
|
|
require(complianceRegistry.isAllowed(recipient), "SettlementOrchestrator: recipient not compliant");
|
|
require(!complianceRegistry.isFrozen(recipient), "SettlementOrchestrator: recipient frozen");
|
|
|
|
IeMoneyToken(trigger.token).mint(recipient, trigger.amount, ReasonCodes.OK);
|
|
} else {
|
|
// Outbound: tokens have been sent via rail, so we need to burn them
|
|
uint8 escrowMode = _escrowModes[triggerId];
|
|
address accountAddress = _lockedAccounts[triggerId] != address(0)
|
|
? _lockedAccounts[triggerId]
|
|
: _resolveAccountAddress(trigger.accountRefId);
|
|
|
|
if (escrowMode == RailTypes.ESCROW_MODE_VAULT) {
|
|
// Transfer tokens from vault to this contract, then burn
|
|
escrowVault.release(trigger.token, address(this), trigger.amount, triggerId);
|
|
IeMoneyToken(trigger.token).burn(address(this), trigger.amount, ReasonCodes.OK);
|
|
} else if (escrowMode == RailTypes.ESCROW_MODE_LIEN) {
|
|
// For lien mode, tokens are still in the account, so we burn them directly
|
|
require(accountAddress != address(0), "SettlementOrchestrator: cannot resolve account");
|
|
IeMoneyToken(trigger.token).burn(accountAddress, trigger.amount, ReasonCodes.OK);
|
|
// Release lien
|
|
uint256 lienId = _triggerLiens[triggerId];
|
|
require(lienId > 0, "SettlementOrchestrator: no lien found");
|
|
debtRegistry.releaseLien(lienId);
|
|
}
|
|
}
|
|
|
|
// Update trigger state
|
|
triggerRegistry.updateState(triggerId, RailTypes.State.SETTLED, ReasonCodes.OK);
|
|
|
|
emit Settled(triggerId, railTxRef, trigger.accountRefId, trigger.token, trigger.amount);
|
|
}
|
|
|
|
/**
|
|
* @notice Confirms a trigger as rejected
|
|
* @dev Requires RAIL_ADAPTER_ROLE. Releases escrow/lien.
|
|
* @param triggerId The trigger ID
|
|
* @param reason The rejection reason
|
|
*/
|
|
function confirmRejected(uint256 triggerId, bytes32 reason) external override onlyRole(RAIL_ADAPTER_ROLE) {
|
|
IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId);
|
|
require(
|
|
trigger.state == RailTypes.State.PENDING ||
|
|
trigger.state == RailTypes.State.SUBMITTED_TO_RAIL ||
|
|
trigger.state == RailTypes.State.VALIDATED,
|
|
"SettlementOrchestrator: invalid state"
|
|
);
|
|
|
|
// Release escrow/lien
|
|
_releaseEscrow(triggerId, trigger);
|
|
|
|
// Update trigger state
|
|
triggerRegistry.updateState(triggerId, RailTypes.State.REJECTED, reason);
|
|
|
|
emit Rejected(triggerId, reason);
|
|
}
|
|
|
|
/**
|
|
* @notice Confirms a trigger as cancelled
|
|
* @dev Requires RAIL_ADAPTER_ROLE. Releases escrow/lien.
|
|
* @param triggerId The trigger ID
|
|
* @param reason The cancellation reason
|
|
*/
|
|
function confirmCancelled(uint256 triggerId, bytes32 reason) external override onlyRole(RAIL_ADAPTER_ROLE) {
|
|
IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId);
|
|
require(
|
|
trigger.state == RailTypes.State.CREATED ||
|
|
trigger.state == RailTypes.State.VALIDATED ||
|
|
trigger.state == RailTypes.State.SUBMITTED_TO_RAIL,
|
|
"SettlementOrchestrator: invalid state"
|
|
);
|
|
|
|
// Release escrow/lien if locked
|
|
if (trigger.state != RailTypes.State.CREATED) {
|
|
_releaseEscrow(triggerId, trigger);
|
|
}
|
|
|
|
// Update trigger state
|
|
triggerRegistry.updateState(triggerId, RailTypes.State.CANCELLED, reason);
|
|
|
|
emit Cancelled(triggerId, reason);
|
|
}
|
|
|
|
/**
|
|
* @notice Confirms a trigger as recalled
|
|
* @dev Requires RAIL_ADAPTER_ROLE. Releases escrow/lien.
|
|
* @param triggerId The trigger ID
|
|
* @param reason The recall reason
|
|
*/
|
|
function confirmRecalled(uint256 triggerId, bytes32 reason) external override onlyRole(RAIL_ADAPTER_ROLE) {
|
|
IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId);
|
|
require(
|
|
trigger.state == RailTypes.State.PENDING || trigger.state == RailTypes.State.SUBMITTED_TO_RAIL,
|
|
"SettlementOrchestrator: invalid state"
|
|
);
|
|
|
|
// Release escrow/lien
|
|
_releaseEscrow(triggerId, trigger);
|
|
|
|
// Update trigger state
|
|
triggerRegistry.updateState(triggerId, RailTypes.State.RECALLED, reason);
|
|
|
|
emit Recalled(triggerId, reason);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the escrow mode for a trigger
|
|
* @param triggerId The trigger ID
|
|
* @return The escrow mode (1 = vault, 2 = lien)
|
|
*/
|
|
function getEscrowMode(uint256 triggerId) external view override returns (uint8) {
|
|
return _escrowModes[triggerId];
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the rail transaction reference for a trigger
|
|
* @param triggerId The trigger ID
|
|
* @return The rail transaction reference
|
|
*/
|
|
function getRailTxRef(uint256 triggerId) external view override returns (bytes32) {
|
|
return _railTxRefs[triggerId];
|
|
}
|
|
|
|
/**
|
|
* @notice Releases escrow for a trigger (internal helper)
|
|
* @param triggerId The trigger ID
|
|
* @param trigger The trigger struct
|
|
*/
|
|
function _releaseEscrow(uint256 triggerId, IRailTriggerRegistry.Trigger memory trigger) internal {
|
|
uint8 escrowMode = _escrowModes[triggerId];
|
|
address accountAddress = _lockedAccounts[triggerId] != address(0)
|
|
? _lockedAccounts[triggerId]
|
|
: _resolveAccountAddress(trigger.accountRefId);
|
|
|
|
if (escrowMode == RailTypes.ESCROW_MODE_VAULT) {
|
|
// Release from vault back to account
|
|
escrowVault.release(trigger.token, accountAddress, trigger.amount, triggerId);
|
|
} else if (escrowMode == RailTypes.ESCROW_MODE_LIEN) {
|
|
// Release lien
|
|
uint256 lienId = _triggerLiens[triggerId];
|
|
if (lienId > 0) {
|
|
debtRegistry.releaseLien(lienId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Resolves account address from accountRefId
|
|
* @dev Uses AccountWalletRegistry to find the first active wallet for an account
|
|
* @param accountRefId The account reference ID
|
|
* @return The account address (or zero if not resolvable)
|
|
*/
|
|
function _resolveAccountAddress(bytes32 accountRefId) internal view returns (address) {
|
|
// Get wallets linked to this account
|
|
IAccountWalletRegistry.WalletLink[] memory wallets = accountWalletRegistry.getWallets(accountRefId);
|
|
|
|
// Find first active wallet and extract address (simplified - in production, you'd need to decode walletRefId)
|
|
// For now, we'll need the walletRefId to be set in the trigger or passed separately
|
|
// This is a limitation that should be addressed in production
|
|
return address(0);
|
|
}
|
|
|
|
/**
|
|
* @notice Checks if a message type is inbound
|
|
* @param msgType The message type
|
|
* @return true if inbound
|
|
*/
|
|
function _isInboundMessage(bytes32 msgType) internal pure returns (bool) {
|
|
return ISO20022Types.isInboundNotification(msgType);
|
|
}
|
|
}
|
|
|