- Updated DBIS_ConversionRouter and DBIS_SettlementRouter to utilize IDBIS_EIP712Helper for EIP-712 hashing and signature recovery, improving stack depth management. - Refactored minting logic in DBIS_GRU_MintController to streamline recipient processing. - Enhanced BUILD_NOTES.md with updated build instructions and test coverage details. - Added new functions in DBIS_SignerRegistry for duplicate signer checks and active signer validation. - Introduced a new submodule, DBIS_EIP712Helper, to encapsulate EIP-712 related functionalities. Made-with: Cursor
177 lines
7.5 KiB
Solidity
177 lines
7.5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
|
|
contract DBIS_SignerRegistry is AccessControl {
|
|
bytes32 public constant SIGNER_ADMIN_ROLE = keccak256("SIGNER_ADMIN");
|
|
|
|
uint8 public constant CATEGORY_OPS = 0;
|
|
uint8 public constant CATEGORY_COMPLIANCE = 1;
|
|
uint8 public constant CATEGORY_CUSTODY = 2;
|
|
uint8 public constant CATEGORY_RISK = 3;
|
|
uint8 public constant CATEGORY_AUDITOR = 4;
|
|
|
|
uint256 private constant NEVER_REVOKED = type(uint256).max;
|
|
|
|
struct SignerInfo {
|
|
uint8 category;
|
|
uint256 effectiveFromBlock;
|
|
uint256 revokedAtBlock;
|
|
bool exists;
|
|
}
|
|
|
|
mapping(address => SignerInfo) private _signers;
|
|
address[] private _signerList;
|
|
|
|
uint8 public requiredSignatures = 3;
|
|
uint256 public categoryMaskRequired = 1 << CATEGORY_COMPLIANCE;
|
|
uint256 public categoryMaskAllowed = (1 << CATEGORY_OPS) | (1 << CATEGORY_COMPLIANCE) | (1 << CATEGORY_CUSTODY) | (1 << CATEGORY_RISK) | (1 << CATEGORY_AUDITOR);
|
|
|
|
event SignerAdded(address indexed signer, uint8 category);
|
|
event SignerRemoved(address indexed signer);
|
|
event QuorumUpdated(uint8 requiredSigs, uint256 requiredMask, uint256 allowedMask);
|
|
|
|
constructor(address admin) {
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
_grantRole(SIGNER_ADMIN_ROLE, admin);
|
|
}
|
|
|
|
function addSigner(address signer, uint8 category) external onlyRole(SIGNER_ADMIN_ROLE) {
|
|
require(signer != address(0), "DBIS: zero signer");
|
|
require(!_signers[signer].exists, "DBIS: already signer");
|
|
require(category <= CATEGORY_AUDITOR, "DBIS: invalid category");
|
|
_signers[signer] = SignerInfo({
|
|
category: category,
|
|
effectiveFromBlock: block.number,
|
|
revokedAtBlock: NEVER_REVOKED,
|
|
exists: true
|
|
});
|
|
_signerList.push(signer);
|
|
emit SignerAdded(signer, category);
|
|
}
|
|
|
|
function removeSigner(address signer) external onlyRole(SIGNER_ADMIN_ROLE) {
|
|
require(_signers[signer].exists, "DBIS: not signer");
|
|
if (_signers[signer].revokedAtBlock == NEVER_REVOKED) {
|
|
_signers[signer].revokedAtBlock = block.number;
|
|
}
|
|
_signers[signer].exists = false;
|
|
for (uint256 i = 0; i < _signerList.length; i++) {
|
|
if (_signerList[i] == signer) {
|
|
_signerList[i] = _signerList[_signerList.length - 1];
|
|
_signerList.pop();
|
|
break;
|
|
}
|
|
}
|
|
emit SignerRemoved(signer);
|
|
}
|
|
|
|
function revokeSignerAtBlock(address signer) external onlyRole(SIGNER_ADMIN_ROLE) {
|
|
require(_signers[signer].exists, "DBIS: not signer");
|
|
_signers[signer].revokedAtBlock = block.number;
|
|
}
|
|
|
|
function setQuorum(uint8 requiredSigs, uint256 requiredMask, uint256 allowedMask) external onlyRole(SIGNER_ADMIN_ROLE) {
|
|
requiredSignatures = requiredSigs;
|
|
categoryMaskRequired = requiredMask;
|
|
categoryMaskAllowed = allowedMask;
|
|
emit QuorumUpdated(requiredSigs, requiredMask, allowedMask);
|
|
}
|
|
|
|
function isSigner(address signer) external view returns (bool) {
|
|
return _signers[signer].exists && _signers[signer].revokedAtBlock == NEVER_REVOKED;
|
|
}
|
|
|
|
function isSignerActiveAtBlock(address signer, uint256 blockNum) external view returns (bool) {
|
|
SignerInfo memory info = _signers[signer];
|
|
if (!info.exists) return false;
|
|
if (blockNum < info.effectiveFromBlock) return false;
|
|
if (info.revokedAtBlock != NEVER_REVOKED && blockNum >= info.revokedAtBlock) return false;
|
|
return true;
|
|
}
|
|
|
|
function areSignersActiveAtBlock(address[] calldata signers, uint256 blockNum) external view returns (bool) {
|
|
for (uint256 i = 0; i < signers.length; i++) {
|
|
if (!this.isSignerActiveAtBlock(signers[i], blockNum)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function hasDuplicateSigners(address[] calldata signers) external pure returns (bool) {
|
|
for (uint256 i = 0; i < signers.length; i++) {
|
|
for (uint256 j = i + 1; j < signers.length; j++) {
|
|
if (signers[i] == signers[j]) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getSignerInfo(address signer) external view returns (uint8 category, uint256 effectiveFromBlock, uint256 revokedAtBlock) {
|
|
SignerInfo memory info = _signers[signer];
|
|
require(info.exists, "DBIS: not signer");
|
|
return (info.category, info.effectiveFromBlock, info.revokedAtBlock);
|
|
}
|
|
|
|
function validateSigners(address[] calldata signers) external view returns (bool ok, string memory reason) {
|
|
if (signers.length < requiredSignatures) return (false, "insufficient count");
|
|
uint256 categoryMask = 0;
|
|
for (uint256 i = 0; i < signers.length; i++) {
|
|
address s = signers[i];
|
|
(bool exists, uint8 cat, uint256 revoked) = _getSignerInfo(s);
|
|
if (!exists) return (false, "not signer");
|
|
if (revoked != NEVER_REVOKED) return (false, "signer revoked");
|
|
if ((categoryMaskAllowed & (1 << cat)) == 0) return (false, "category not allowed");
|
|
if (_hasDuplicate(signers, i, s)) return (false, "duplicate signer");
|
|
categoryMask |= (1 << cat);
|
|
}
|
|
if ((categoryMask & categoryMaskRequired) != categoryMaskRequired) return (false, "required category missing");
|
|
return (true, "");
|
|
}
|
|
|
|
function _getSignerInfo(address signer) private view returns (bool exists, uint8 category, uint256 revokedAtBlock) {
|
|
SignerInfo memory info = _signers[signer];
|
|
return (info.exists, info.category, info.revokedAtBlock);
|
|
}
|
|
|
|
function _hasDuplicate(address[] calldata signers, uint256 currentIndex, address signer) private pure returns (bool) {
|
|
for (uint256 j = currentIndex + 1; j < signers.length; j++) {
|
|
if (signers[j] == signer) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getSignerCount() external view returns (uint256) {
|
|
return _signerList.length;
|
|
}
|
|
|
|
uint256 public largeSwapAmountThreshold;
|
|
uint8 public requiredSignaturesSmall = 2;
|
|
uint8 public requiredSignaturesLarge = 3;
|
|
|
|
function setSwapQuorum(uint256 largeThreshold, uint8 smallSigs, uint8 largeSigs) external onlyRole(SIGNER_ADMIN_ROLE) {
|
|
largeSwapAmountThreshold = largeThreshold;
|
|
requiredSignaturesSmall = smallSigs;
|
|
requiredSignaturesLarge = largeSigs;
|
|
}
|
|
|
|
function validateSignersForSwap(address[] calldata signers, uint256 amountIn) external view returns (bool ok, string memory reason) {
|
|
uint8 required = amountIn > largeSwapAmountThreshold ? requiredSignaturesLarge : requiredSignaturesSmall;
|
|
if (signers.length < required) return (false, "insufficient count");
|
|
uint256 categoryMask = 0;
|
|
for (uint256 i = 0; i < signers.length; i++) {
|
|
address s = signers[i];
|
|
(bool exists, uint8 cat, uint256 revoked) = _getSignerInfo(s);
|
|
if (!exists) return (false, "not signer");
|
|
if (revoked != NEVER_REVOKED) return (false, "signer revoked");
|
|
if ((categoryMaskAllowed & (1 << cat)) == 0) return (false, "category not allowed");
|
|
if (_hasDuplicate(signers, i, s)) return (false, "duplicate signer");
|
|
categoryMask |= (1 << cat);
|
|
}
|
|
if (amountIn > largeSwapAmountThreshold && (categoryMask & categoryMaskRequired) != categoryMaskRequired) {
|
|
return (false, "required category missing");
|
|
}
|
|
return (true, "");
|
|
}
|
|
}
|