// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./IDBISTypes.sol"; import "./IDBIS_EIP712Helper.sol"; import "./DBIS_RootRegistry.sol"; import "./DBIS_ParticipantRegistry.sol"; import "./DBIS_SignerRegistry.sol"; import "./DBIS_GRU_MintController.sol"; contract DBIS_SettlementRouter is AccessControl, Pausable, ReentrancyGuard { bytes32 public constant ROUTER_ADMIN_ROLE = keccak256("ROUTER_ADMIN"); uint256 public constant CHAIN_ID = 138; bytes32 public constant PARTICIPANT_REGISTRY_KEY = keccak256("ParticipantRegistry"); bytes32 public constant SIGNER_REGISTRY_KEY = keccak256("SignerRegistry"); bytes32 public constant GRU_MINT_CONTROLLER_KEY = keccak256("GRUMintController"); bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" ); bytes32 private constant MINTAUTH_TYPEHASH = keccak256( "MintAuth(bytes32 messageId,bytes32 isoType,bytes32 isoHash,bytes32 accountingRef,uint8 fundsStatus,bytes32 corridor,uint8 assetClass,bytes32 recipientsHash,bytes32 amountsHash,uint64 notBefore,uint64 expiresAt,uint256 chainId,address verifyingContract)" ); DBIS_RootRegistry public rootRegistry; address public eip712Lib; mapping(bytes32 => bool) public usedMessageIds; uint256 public maxAmountPerMessage; mapping(bytes32 => uint256) public corridorDailyCap; mapping(bytes32 => mapping(uint256 => uint256)) public corridorUsedToday; event SettlementRecorded( bytes32 indexed messageId, bytes32 indexed isoType, bytes32 isoHash, bytes32 accountingRef, uint8 fundsStatus, bytes32 corridor, uint8 assetClass, uint256 totalAmount ); event MintExecuted(bytes32 indexed messageId, address indexed recipient, uint256 amount, uint8 assetClass); event MessageIdConsumed(bytes32 indexed messageId); event RouterPaused(bool paused); constructor(address admin, address _rootRegistry, address _eip712Lib) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(ROUTER_ADMIN_ROLE, admin); rootRegistry = DBIS_RootRegistry(_rootRegistry); eip712Lib = _eip712Lib; maxAmountPerMessage = type(uint256).max; } function setMaxAmountPerMessage(uint256 cap) external onlyRole(ROUTER_ADMIN_ROLE) { maxAmountPerMessage = cap; } function setCorridorDailyCap(bytes32 corridor, uint256 cap) external onlyRole(ROUTER_ADMIN_ROLE) { corridorDailyCap[corridor] = cap; } function pause() external onlyRole(ROUTER_ADMIN_ROLE) { _pause(); emit RouterPaused(true); } function unpause() external onlyRole(ROUTER_ADMIN_ROLE) { _unpause(); emit RouterPaused(false); } function _domainSeparator() private view returns (bytes32) { return keccak256( abi.encode( EIP712_DOMAIN_TYPEHASH, keccak256("DBISSettlementRouter"), keccak256("1"), CHAIN_ID, address(this) ) ); } function _hashMintAuth(IDBISTypes.MintAuth calldata auth) private view returns (bytes32) { IDBIS_EIP712Helper helper = IDBIS_EIP712Helper(eip712Lib); bytes32 rh = helper.hashAddressArray(auth.recipients); bytes32 ah = helper.hashUint256Array(auth.amounts); return helper.getMintAuthStructHash( MINTAUTH_TYPEHASH, auth.messageId, auth.isoType, auth.isoHash, auth.accountingRef, uint8(auth.fundsStatus), auth.corridor, uint8(auth.assetClass), rh, ah, auth.notBefore, auth.expiresAt, auth.chainId, auth.verifyingContract ); } function getMintAuthDigest(IDBISTypes.MintAuth calldata auth) external view returns (bytes32) { return IDBIS_EIP712Helper(eip712Lib).getDigest(_domainSeparator(), _hashMintAuth(auth)); } function submitMintAuth(IDBISTypes.MintAuth calldata auth, bytes[] calldata signatures) external nonReentrant whenNotPaused { require(auth.chainId == CHAIN_ID, "DBIS: wrong chain"); require(auth.verifyingContract == address(this), "DBIS: wrong contract"); require(block.timestamp >= auth.notBefore && block.timestamp <= auth.expiresAt, "DBIS: time window"); require(!usedMessageIds[auth.messageId], "DBIS: replay"); require(auth.recipients.length == auth.amounts.length, "DBIS: length mismatch"); require(auth.recipients.length > 0, "DBIS: no recipients"); uint256 totalAmount = _sumAmounts(auth.amounts); require(totalAmount <= maxAmountPerMessage, "DBIS: cap exceeded"); uint256 day = block.timestamp / 1 days; require(_checkCorridorCap(auth.corridor, day, totalAmount), "DBIS: corridor cap"); _requireRecipientsOperational(auth.recipients); _recoverSigners(auth, signatures); usedMessageIds[auth.messageId] = true; corridorUsedToday[auth.corridor][day] = corridorUsedToday[auth.corridor][day] + totalAmount; _emitSettlementEvents(auth, totalAmount); DBIS_GRU_MintController mintController = DBIS_GRU_MintController(rootRegistry.getComponent(GRU_MINT_CONTROLLER_KEY)); require(address(mintController) != address(0), "DBIS: no mint controller"); mintController.mintFromAuthorization(auth); } function _sumAmounts(uint256[] calldata amounts) private pure returns (uint256) { uint256 total = 0; for (uint256 i = 0; i < amounts.length; i++) total += amounts[i]; return total; } function _checkCorridorCap(bytes32 corridor, uint256 day, uint256 totalAmount) private view returns (bool) { uint256 cap = corridorDailyCap[corridor]; if (cap == 0) return true; return corridorUsedToday[corridor][day] + totalAmount <= cap; } function _requireRecipientsOperational(address[] calldata recipients) private view { DBIS_ParticipantRegistry participantReg = DBIS_ParticipantRegistry(rootRegistry.getComponent(PARTICIPANT_REGISTRY_KEY)); require(address(participantReg) != address(0), "DBIS: no participant registry"); for (uint256 i = 0; i < recipients.length; i++) { require(participantReg.isOperationalWallet(recipients[i]), "DBIS: not operational wallet"); } } function _recoverSigners(IDBISTypes.MintAuth calldata auth, bytes[] calldata signatures) private view returns (address[] memory signers) { DBIS_SignerRegistry signerReg = DBIS_SignerRegistry(rootRegistry.getComponent(SIGNER_REGISTRY_KEY)); require(address(signerReg) != address(0), "DBIS: no signer registry"); IDBIS_EIP712Helper helper = IDBIS_EIP712Helper(eip712Lib); bytes32 digest = helper.getDigest(_domainSeparator(), _hashMintAuth(auth)); signers = helper.recoverSigners(digest, signatures); require(!signerReg.hasDuplicateSigners(signers), "DBIS: duplicate signer"); require(signerReg.areSignersActiveAtBlock(signers, block.number), "DBIS: signer not active at block"); (bool ok, ) = signerReg.validateSigners(signers); require(ok, "DBIS: quorum not met"); } function _emitSettlementEvents(IDBISTypes.MintAuth calldata auth, uint256 totalAmount) private { emit SettlementRecorded( auth.messageId, auth.isoType, auth.isoHash, auth.accountingRef, uint8(auth.fundsStatus), auth.corridor, uint8(auth.assetClass), totalAmount ); for (uint256 i = 0; i < auth.recipients.length; i++) { emit MintExecuted(auth.messageId, auth.recipients[i], auth.amounts[i], uint8(auth.assetClass)); } emit MessageIdConsumed(auth.messageId); } }