// 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 "./DBIS_RootRegistry.sol"; import "./DBIS_SignerRegistry.sol"; import "./StablecoinReferenceRegistry.sol"; interface IBlocklist { function isBlocked(address account) external view returns (bool); } struct SwapAuth { bytes32 messageId; bytes32 lpaId; bytes32 venue; address tokenIn; address tokenOut; uint256 amountIn; uint256 minAmountOut; uint256 deadline; bytes32 quoteHash; address quoteIssuer; uint256 chainId; address verifyingContract; } contract DBIS_ConversionRouter is AccessControl, Pausable, ReentrancyGuard { bytes32 public constant ROUTER_ADMIN_ROLE = keccak256("ROUTER_ADMIN"); uint256 public constant CHAIN_ID = 138; bytes32 public constant SIGNER_REGISTRY_KEY = keccak256("SignerRegistry"); bytes32 public constant STABLECOIN_REGISTRY_KEY = keccak256("StablecoinReferenceRegistry"); bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" ); bytes32 private constant SWAPAUTH_TYPEHASH = keccak256( "SwapAuth(bytes32 messageId,bytes32 lpaId,bytes32 venue,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 deadline,bytes32 quoteHash,address quoteIssuer,uint256 chainId,address verifyingContract)" ); DBIS_RootRegistry public rootRegistry; mapping(bytes32 => bool) public usedSwapMessageIds; mapping(bytes32 => bool) public venueAllowlist; mapping(address => bool) public quoteIssuerAllowlist; address public blocklistContract; event ConversionExecuted( bytes32 indexed messageId, bytes32 indexed quoteHash, bytes32 venue, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, address quoteIssuer ); constructor(address admin, address _rootRegistry) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(ROUTER_ADMIN_ROLE, admin); rootRegistry = DBIS_RootRegistry(_rootRegistry); } function addVenue(bytes32 venue) external onlyRole(ROUTER_ADMIN_ROLE) { venueAllowlist[venue] = true; } function removeVenue(bytes32 venue) external onlyRole(ROUTER_ADMIN_ROLE) { venueAllowlist[venue] = false; } function addQuoteIssuer(address issuer) external onlyRole(ROUTER_ADMIN_ROLE) { quoteIssuerAllowlist[issuer] = true; } function removeQuoteIssuer(address issuer) external onlyRole(ROUTER_ADMIN_ROLE) { quoteIssuerAllowlist[issuer] = false; } function setBlocklist(address _blocklist) external onlyRole(ROUTER_ADMIN_ROLE) { blocklistContract = _blocklist; } function pause() external onlyRole(ROUTER_ADMIN_ROLE) { _pause(); } function unpause() external onlyRole(ROUTER_ADMIN_ROLE) { _unpause(); } function _domainSeparator() private view returns (bytes32) { return keccak256( abi.encode( EIP712_DOMAIN_TYPEHASH, keccak256("DBISConversionRouter"), keccak256("1"), CHAIN_ID, address(this) ) ); } function _hashSwapAuth(SwapAuth calldata auth) private pure returns (bytes32) { return keccak256( abi.encode( SWAPAUTH_TYPEHASH, auth.messageId, auth.lpaId, auth.venue, auth.tokenIn, auth.tokenOut, auth.amountIn, auth.minAmountOut, auth.deadline, auth.quoteHash, auth.quoteIssuer, auth.chainId, auth.verifyingContract ) ); } function getSwapAuthDigest(SwapAuth calldata auth) external view returns (bytes32) { return keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), _hashSwapAuth(auth))); } function submitSwapAuth(SwapAuth calldata auth, bytes[] calldata signatures, uint256 amountOut) external nonReentrant whenNotPaused { require(auth.chainId == CHAIN_ID, "DBIS: wrong chain"); require(auth.verifyingContract == address(this), "DBIS: wrong contract"); require(block.timestamp <= auth.deadline, "DBIS: expired"); require(!usedSwapMessageIds[auth.messageId], "DBIS: replay"); require(venueAllowlist[auth.venue], "DBIS: venue not allowed"); require(quoteIssuerAllowlist[auth.quoteIssuer], "DBIS: quote issuer not allowed"); require(amountOut >= auth.minAmountOut, "DBIS: slippage"); _requireStablecoinActive(auth.tokenOut); _requireNotBlocked(); address[] memory signers = _recoverSwapSigners(auth, signatures); DBIS_SignerRegistry signerReg = DBIS_SignerRegistry(rootRegistry.getComponent(SIGNER_REGISTRY_KEY)); (bool ok, ) = signerReg.validateSignersForSwap(signers, auth.amountIn); require(ok, "DBIS: quorum not met"); usedSwapMessageIds[auth.messageId] = true; emit ConversionExecuted( auth.messageId, auth.quoteHash, auth.venue, auth.tokenIn, auth.tokenOut, auth.amountIn, amountOut, auth.quoteIssuer ); } function _requireStablecoinActive(address tokenOut) private view { StablecoinReferenceRegistry stableReg = StablecoinReferenceRegistry(rootRegistry.getComponent(STABLECOIN_REGISTRY_KEY)); if (address(stableReg) != address(0)) require(stableReg.isActive(tokenOut), "DBIS: tokenOut not active"); } function _requireNotBlocked() private view { if (blocklistContract != address(0)) require(!IBlocklist(blocklistContract).isBlocked(msg.sender), "DBIS: blocked"); } function _recoverSwapSigners(SwapAuth 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"); bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), _hashSwapAuth(auth))); signers = new address[](signatures.length); for (uint256 i = 0; i < signatures.length; i++) { require(signatures[i].length == 65, "DBIS: bad sig len"); address signer = _recover(digest, signatures[i]); require(signer != address(0), "DBIS: invalid sig"); require(signerReg.isSignerActiveAtBlock(signer, block.number), "DBIS: signer not active"); for (uint256 j = 0; j < i; j++) require(signers[j] != signer, "DBIS: duplicate signer"); signers[i] = signer; } } function _recover(bytes32 digest, bytes calldata signature) private pure returns (address) { require(signature.length == 65, "DBIS: sig length"); bytes32 r; bytes32 s; uint8 v; assembly { r := calldataload(signature.offset) s := calldataload(add(signature.offset, 32)) v := byte(0, calldataload(add(signature.offset, 64))) } if (v < 27) v += 27; require(v == 27 || v == 28, "DBIS: invalid v"); return ecrecover(digest, v, r, s); } }