diff --git a/contracts/compliance/LegallyCompliantBaseV2.sol b/contracts/compliance/LegallyCompliantBaseV2.sol new file mode 100644 index 0000000..5abe62e --- /dev/null +++ b/contracts/compliance/LegallyCompliantBaseV2.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * @title LegallyCompliantBaseV2 + * @notice Explicit-actor legal and audit metadata base for GRU c* V2 tokens. + * @dev Replaces tx.origin-based attribution with explicit initiator/authorizer/executor fields. + */ +abstract contract LegallyCompliantBaseV2 is AccessControl { + bytes32 public constant COMPLIANCE_ADMIN_ROLE = keccak256("COMPLIANCE_ADMIN_ROLE"); + + string public constant LEGAL_FRAMEWORK_VERSION = "2.0.0"; + string public constant LEGAL_JURISDICTION = "International Private Law"; + string public constant DISPUTE_RESOLUTION_MECHANISM = "ICC Arbitration (Paris)"; + string public constant SERVICE_OF_PROCESS_ADDRESS = "0x0000000000000000000000000000000000000000"; + string public constant ISO_20022_COMPLIANCE = "ISO 20022 (Financial Messaging) - Supported via ISO20022Router"; + string public constant ISO_27001_COMPLIANCE = "ISO 27001 (Information Security) - Architectural Compliance"; + string public constant ISO_3166_COMPLIANCE = "ISO 3166 (Country Codes) - Supported"; + string public constant ISO_8601_COMPLIANCE = "ISO 8601 (Timestamps) - Supported"; + string public constant ISO_4217_COMPLIANCE = "ISO 4217 (Currency Codes) - Supported"; + string public constant ICC_UNIFORM_RULES_COMPLIANCE = + "ICC Uniform Rules for Demand Guarantees (URDG 758) - Applicable by reference"; + string public constant ICC_ARBITRATION_SUPPORT = "ICC Arbitration Rules - Supported for dispute resolution"; + string public constant ICC_TRADE_TERMS_COMPLIANCE = "Incoterms 2020 - Applicable by reference"; + string public constant INSTRUMENT_CLASSIFICATION = "Private Digital Instrument of Value Transfer"; + string public constant TRANSFERABILITY_STATEMENT = + "This instrument represents a transferable claim or value, subject to contract terms."; + string public constant LEGAL_ENFORCEABILITY_STATEMENT = + "Enforceable under private international law principles and ICC arbitration."; + string public constant TRAVEL_RULE_EXEMPTION_STATEMENT = + "This contract is designed for private, peer-to-peer value transfer and is not intended to facilitate transactions subject to FATF Travel Rule (e.g., not a VASP, no originator/beneficiary data collection, no transaction monitoring, no reporting)."; + string public constant REGULATORY_EXEMPTION_STATEMENT = + "This contract is not intended to be a money transmission service, payment service, or regulated financial instrument. It operates as a private instrument of value transfer between consenting parties."; + + event LegalNotice(bytes32 indexed noticeHash, string message, uint256 timestamp); + event ValueTransferDeclared( + address indexed from, + address indexed to, + uint256 value, + bytes32 legalReferenceHash + ); + event JurisdictionDeclared(string jurisdiction, uint256 timestamp); + event DisputeResolutionMechanismSet(string mechanism, uint256 timestamp); + event CompliantOperationDeclared( + bytes32 indexed operationType, + address indexed initiator, + address indexed authorizer, + address executor, + address from, + address to, + uint256 value, + bytes32 reasonHash, + bytes32 accountingRef, + bytes32 messageCorrelationId, + bytes32 legalReferenceHash + ); + + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(COMPLIANCE_ADMIN_ROLE, admin); + emit JurisdictionDeclared(LEGAL_JURISDICTION, block.timestamp); + emit DisputeResolutionMechanismSet(DISPUTE_RESOLUTION_MECHANISM, block.timestamp); + } + + function recordLegalNotice(string calldata message) external onlyRole(COMPLIANCE_ADMIN_ROLE) { + emit LegalNotice(keccak256(abi.encodePacked(message, block.timestamp, address(this))), message, block.timestamp); + } + + function _generateLegalReferenceHashV2( + bytes32 operationType, + address initiator, + address authorizer, + address executor, + address from, + address to, + uint256 value, + bytes32 reasonHash, + bytes32 accountingRef, + bytes32 messageCorrelationId, + bytes32 additionalDataHash + ) internal view returns (bytes32) { + return keccak256( + abi.encode( + operationType, + initiator, + authorizer, + executor, + from, + to, + value, + reasonHash, + accountingRef, + messageCorrelationId, + additionalDataHash, + block.chainid, + address(this), + LEGAL_FRAMEWORK_VERSION, + LEGAL_JURISDICTION + ) + ); + } + + function _recordCompliantOperation( + bytes32 operationType, + address initiator, + address authorizer, + address executor, + address from, + address to, + uint256 value, + bytes32 reasonHash, + bytes32 accountingRef, + bytes32 messageCorrelationId, + bytes32 additionalDataHash + ) internal returns (bytes32 legalReferenceHash) { + legalReferenceHash = _generateLegalReferenceHashV2( + operationType, + initiator, + authorizer, + executor, + from, + to, + value, + reasonHash, + accountingRef, + messageCorrelationId, + additionalDataHash + ); + + emit CompliantOperationDeclared( + operationType, + initiator, + authorizer, + executor, + from, + to, + value, + reasonHash, + accountingRef, + messageCorrelationId, + legalReferenceHash + ); + + if (from != address(0) && to != address(0)) { + emit ValueTransferDeclared(from, to, value, legalReferenceHash); + } + } +} diff --git a/contracts/governance/GovernanceController.sol b/contracts/governance/GovernanceController.sol index 6df19c3..2367410 100644 --- a/contracts/governance/GovernanceController.sol +++ b/contracts/governance/GovernanceController.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "../vendor/openzeppelin/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "../vendor/openzeppelin/ReentrancyGuardUpgradeable.sol"; import "../registry/UniversalAssetRegistry.sol"; /** @@ -22,6 +22,8 @@ contract GovernanceController is bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + // Deprecated compatibility role. Jurisdiction review is derived from asset-scoped proposals. + bytes32 public constant JURISDICTION_TAGGER_ROLE = keccak256("JURISDICTION_TAGGER_ROLE"); // Governance modes enum GovernanceMode { @@ -64,12 +66,19 @@ contract GovernanceController is // Storage UniversalAssetRegistry public assetRegistry; mapping(uint256 => Proposal) public proposals; + mapping(uint256 => address) public proposalAssets; uint256 public proposalCount; // Governance parameters uint256 public votingDelay; // Blocks to wait before voting starts uint256 public votingPeriod; // Blocks voting is open uint256 public quorumNumerator; // Percentage required for quorum + mapping(uint256 => bytes32) public proposalJurisdictionIds; + mapping(uint256 => bool) public proposalJurisdictionReviewRequired; + mapping(uint256 => uint256) public proposalMinimumNoticePeriod; + mapping(uint256 => uint256) public proposalJurisdictionApprovalCount; + mapping(uint256 => address) public proposalLastJurisdictionApprover; + mapping(uint256 => mapping(address => bool)) private _proposalJurisdictionApprovals; uint256 public constant TIMELOCK_SHORT = 1 days; uint256 public constant TIMELOCK_MODERATE = 3 days; @@ -100,6 +109,25 @@ contract GovernanceController is event ProposalQueued(uint256 indexed proposalId, uint256 eta); event ProposalExecuted(uint256 indexed proposalId); event ProposalCanceled(uint256 indexed proposalId); + event ProposalJurisdictionTagged( + uint256 indexed proposalId, + bytes32 indexed jurisdictionId, + bool reviewRequired, + uint256 minimumNoticePeriod + ); + event ProposalJurisdictionApproved( + uint256 indexed proposalId, + bytes32 indexed jurisdictionId, + address indexed authority, + uint256 approvalCount + ); + event ProposalAssetScoped( + uint256 indexed proposalId, + address indexed asset, + bytes32 indexed jurisdictionId, + bool reviewRequired, + uint256 minimumNoticePeriod + ); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -123,7 +151,6 @@ contract GovernanceController is _grantRole(EXECUTOR_ROLE, admin); _grantRole(CANCELLER_ROLE, admin); _grantRole(UPGRADER_ROLE, admin); - votingDelay = 1; // 1 block votingPeriod = 50400; // ~7 days quorumNumerator = 4; // 4% quorum @@ -141,10 +168,41 @@ contract GovernanceController is bytes[] memory calldatas, string memory description, GovernanceMode mode + ) external pure returns (uint256) { + targets; + values; + calldatas; + description; + mode; + revert("Use proposeForAsset"); + } + + function proposeForAsset( + address asset, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + GovernanceMode mode ) external onlyRole(PROPOSER_ROLE) returns (uint256) { require(targets.length == values.length, "Length mismatch"); require(targets.length == calldatas.length, "Length mismatch"); require(targets.length > 0, "Empty proposal"); + require(assetRegistry.isAssetActive(asset), "Asset inactive"); + _validateProposalScope(asset, targets, calldatas); + + UniversalAssetRegistry.UniversalAsset memory assetRecord = assetRegistry.getAsset(asset); + bytes32 jurisdictionId = assetRegistry.getAssetJurisdictionId(asset); + require(jurisdictionId != bytes32(0), "Asset jurisdiction missing"); + bool reviewRequired = + assetRecord.governmentApprovalRequired || + assetRecord.supervisionRequired || + assetRecord.requiresGovernance; + uint256 minimumNoticePeriod = assetRecord.minimumUpgradeNoticePeriod; + uint256 jurisdictionNoticePeriod = assetRegistry.getJurisdictionMinimumUpgradeNotice(jurisdictionId); + if (jurisdictionNoticePeriod > minimumNoticePeriod) { + minimumNoticePeriod = jurisdictionNoticePeriod; + } proposalCount++; uint256 proposalId = proposalCount; @@ -160,6 +218,10 @@ contract GovernanceController is proposal.endBlock = proposal.startBlock + votingPeriod; proposal.mode = mode; proposal.state = ProposalState.Pending; + proposalAssets[proposalId] = asset; + proposalJurisdictionIds[proposalId] = jurisdictionId; + proposalJurisdictionReviewRequired[proposalId] = reviewRequired; + proposalMinimumNoticePeriod[proposalId] = minimumNoticePeriod; emit ProposalCreated( proposalId, @@ -172,6 +234,13 @@ contract GovernanceController is proposal.endBlock, description ); + emit ProposalAssetScoped( + proposalId, + asset, + jurisdictionId, + reviewRequired, + minimumNoticePeriod + ); return proposalId; } @@ -229,6 +298,34 @@ contract GovernanceController is return weight; } + function _validateProposalScope( + address asset, + address[] memory targets, + bytes[] memory calldatas + ) internal view { + for (uint256 i = 0; i < targets.length; i++) { + address target = targets[i]; + if (target == asset) { + continue; + } + + require(target == address(assetRegistry), "Unsupported asset-scoped target"); + _requireRegistryCallScopedToAsset(asset, calldatas[i]); + } + } + + function _requireRegistryCallScopedToAsset(address asset, bytes memory data) internal pure { + require(data.length >= 36, "Registry call not asset-scoped"); + address scopedAsset = _readAddressArgument(data); + require(scopedAsset == asset, "Registry asset mismatch"); + } + + function _readAddressArgument(bytes memory data) internal pure returns (address scopedAsset) { + assembly { + scopedAsset := shr(96, mload(add(data, 36))) + } + } + /** * @notice Queue a successful proposal */ @@ -236,7 +333,11 @@ contract GovernanceController is require(state(proposalId) == ProposalState.Succeeded, "Not succeeded"); Proposal storage proposal = proposals[proposalId]; + _requireJurisdictionApprovalIfNeeded(proposalId); uint256 delay = _getTimelockDelay(proposal.mode); + if (proposalMinimumNoticePeriod[proposalId] > delay) { + delay = proposalMinimumNoticePeriod[proposalId]; + } uint256 eta = block.timestamp + delay; proposal.eta = eta; @@ -388,4 +489,53 @@ contract GovernanceController is require(newQuorumNumerator <= 100, "Invalid quorum"); quorumNumerator = newQuorumNumerator; } + + function tagProposalJurisdiction( + uint256 proposalId, + string calldata jurisdiction, + bool reviewRequired + ) external pure { + proposalId; + jurisdiction; + reviewRequired; + revert("Jurisdiction derived from asset"); + } + + function approveProposalJurisdiction(uint256 proposalId) external { + bytes32 jurisdictionId = proposalJurisdictionIds[proposalId]; + require(jurisdictionId != bytes32(0), "Jurisdiction not tagged"); + require(proposalJurisdictionReviewRequired[proposalId], "Review not required"); + require( + assetRegistry.isGovernanceAuthority(jurisdictionId, msg.sender), + "Not jurisdiction authority" + ); + require(!_proposalJurisdictionApprovals[proposalId][msg.sender], "Already approved"); + + _proposalJurisdictionApprovals[proposalId][msg.sender] = true; + proposalJurisdictionApprovalCount[proposalId] += 1; + proposalLastJurisdictionApprover[proposalId] = msg.sender; + + emit ProposalJurisdictionApproved( + proposalId, + jurisdictionId, + msg.sender, + proposalJurisdictionApprovalCount[proposalId] + ); + } + + function hasJurisdictionApproval(uint256 proposalId, address authority) external view returns (bool) { + return _proposalJurisdictionApprovals[proposalId][authority]; + } + + function getProposalEta(uint256 proposalId) external view returns (uint256) { + return proposals[proposalId].eta; + } + + function _requireJurisdictionApprovalIfNeeded(uint256 proposalId) internal view { + if (!proposalJurisdictionReviewRequired[proposalId]) { + return; + } + + require(proposalJurisdictionApprovalCount[proposalId] > 0, "Jurisdiction approval required"); + } } diff --git a/contracts/interfaces/IRegulatedAssetMetadata.sol b/contracts/interfaces/IRegulatedAssetMetadata.sol new file mode 100644 index 0000000..8c8ac49 --- /dev/null +++ b/contracts/interfaces/IRegulatedAssetMetadata.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/** + * @title IRegulatedAssetMetadata + * @notice Shared governance, supervision, and storage metadata surface for GRU monetary assets. + */ +interface IRegulatedAssetMetadata { + function assetId() external view returns (bytes32); + function assetVersionId() external view returns (bytes32); + function governanceProfileId() external view returns (bytes32); + function supervisionProfileId() external view returns (bytes32); + function storageNamespace() external view returns (bytes32); + function primaryJurisdiction() external view returns (string memory); + function regulatoryDisclosureURI() external view returns (string memory); + function reportingURI() external view returns (string memory); + function canonicalUnderlyingAsset() external view returns (address); + function supervisionRequired() external view returns (bool); + function governmentApprovalRequired() external view returns (bool); + function minimumUpgradeNoticePeriod() external view returns (uint256); + function wrappedTransport() external view returns (bool); +} diff --git a/contracts/registry/UniversalAssetRegistry.sol b/contracts/registry/UniversalAssetRegistry.sol index 8abc5cc..dbaf48d 100644 --- a/contracts/registry/UniversalAssetRegistry.sol +++ b/contracts/registry/UniversalAssetRegistry.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "../vendor/openzeppelin/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "../vendor/openzeppelin/ReentrancyGuardUpgradeable.sol"; +import "../interfaces/IRegulatedAssetMetadata.sol"; /** * @title UniversalAssetRegistry @@ -21,6 +22,10 @@ contract UniversalAssetRegistry is bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE"); bytes32 public constant VALIDATOR_ROLE = keccak256("VALIDATOR_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + bytes32 public constant JURISDICTION_MANAGER_ROLE = keccak256("JURISDICTION_MANAGER_ROLE"); + bytes32 public constant REGULATOR_ROLE = keccak256("REGULATOR_ROLE"); + bytes32 public constant SUPERVISOR_ROLE = keccak256("SUPERVISOR_ROLE"); + bytes32 public constant EMERGENCY_ADMIN_ROLE = keccak256("EMERGENCY_ADMIN_ROLE"); // Asset classification types enum AssetType { @@ -85,6 +90,20 @@ contract UniversalAssetRegistry is bool isActive; uint256 registeredAt; uint256 lastUpdated; + + // Governance, storage, and supervision + bytes32 assetId; + bytes32 assetVersionId; + bytes32 governanceProfileId; + bytes32 supervisionProfileId; + bytes32 storageNamespace; + address canonicalUnderlyingAsset; + bool isWrappedTransport; + bool supervisionRequired; + bool governmentApprovalRequired; + uint256 minimumUpgradeNoticePeriod; + string regulatoryDisclosureURI; + string reportingURI; } struct PendingAssetProposal { @@ -111,6 +130,23 @@ contract UniversalAssetRegistry is uint256 maxBridgeAmount; } + struct JurisdictionProfile { + bool active; + bool requiresGovernmentApproval; + bool requiresPeriodicReports; + uint256 minimumUpgradeNoticePeriod; + string supervisionURI; + bytes32 policyHash; + } + + struct JurisdictionAuthority { + bool active; + bool canApproveGovernance; + bool canApproveUpgrades; + bool canPause; + bool canReceiveReports; + } + // Storage mapping(address => UniversalAsset) public assets; mapping(AssetType => address[]) public assetsByType; @@ -126,6 +162,10 @@ contract UniversalAssetRegistry is // Validator set address[] public validators; mapping(address => bool) public isValidator; + address public governanceController; + mapping(bytes32 => JurisdictionProfile) private _jurisdictionProfiles; + mapping(bytes32 => mapping(address => JurisdictionAuthority)) private _jurisdictionAuthorities; + mapping(address => bytes32) private _assetJurisdictionIds; // Events event AssetProposed( @@ -154,6 +194,50 @@ contract UniversalAssetRegistry is event ProposalExecuted(bytes32 indexed proposalId); event ProposalCancelled(bytes32 indexed proposalId); + event JurisdictionProfileUpdated( + bytes32 indexed jurisdictionId, + string jurisdiction, + bool active, + bool requiresGovernmentApproval, + bool requiresPeriodicReports, + uint256 minimumUpgradeNoticePeriod, + string supervisionURI, + bytes32 policyHash + ); + event JurisdictionAuthorityUpdated( + bytes32 indexed jurisdictionId, + string jurisdiction, + address indexed authority, + bool active, + bool canApproveGovernance, + bool canApproveUpgrades, + bool canPause, + bool canReceiveReports + ); + event AssetGovernanceProfileUpdated( + address indexed token, + bytes32 governanceProfileId, + bytes32 supervisionProfileId, + bytes32 storageNamespace + ); + event AssetSupervisionProfileUpdated( + address indexed token, + address canonicalUnderlyingAsset, + bool isWrappedTransport, + bool supervisionRequired, + bool governmentApprovalRequired, + uint256 minimumUpgradeNoticePeriod + ); + + error GovernanceControllerOnly(address caller); + error GovernanceControllerNotConfigured(); + error ZeroGovernanceController(); + + modifier onlyGovernanceExecution() { + if (governanceController == address(0)) revert GovernanceControllerNotConfigured(); + if (_msgSender() != governanceController) revert GovernanceControllerOnly(_msgSender()); + _; + } /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -170,6 +254,10 @@ contract UniversalAssetRegistry is _grantRole(PROPOSER_ROLE, admin); _grantRole(VALIDATOR_ROLE, admin); _grantRole(UPGRADER_ROLE, admin); + _grantRole(JURISDICTION_MANAGER_ROLE, admin); + _grantRole(REGULATOR_ROLE, admin); + _grantRole(SUPERVISOR_ROLE, admin); + _grantRole(EMERGENCY_ADMIN_ROLE, admin); quorumPercentage = 51; // 51% quorum } @@ -352,6 +440,9 @@ contract UniversalAssetRegistry is asset.isActive = true; asset.registeredAt = block.timestamp; asset.lastUpdated = block.timestamp; + _assetJurisdictionIds[tokenAddress] = jurisdictionIdFor(jurisdiction); + _syncAssetMetadata(tokenAddress, asset); + _applyJurisdictionDefaults(asset, _assetJurisdictionIds[tokenAddress]); assetsByType[assetType].push(tokenAddress); emit AssetApproved(tokenAddress, assetType, complianceLevel); } @@ -405,6 +496,259 @@ contract UniversalAssetRegistry is assets[token].lastUpdated = block.timestamp; } + function setGovernanceController(address governanceController_) external onlyRole(EMERGENCY_ADMIN_ROLE) { + if (governanceController_ == address(0)) revert ZeroGovernanceController(); + governanceController = governanceController_; + } + + function setJurisdictionProfile( + string calldata jurisdiction, + bool active, + bool requiresGovernmentApproval, + bool requiresPeriodicReports, + uint256 minimumUpgradeNoticePeriod, + string calldata supervisionURI, + bytes32 policyHash + ) external onlyGovernanceExecution { + _setJurisdictionProfileValue( + jurisdiction, + active, + requiresGovernmentApproval, + requiresPeriodicReports, + minimumUpgradeNoticePeriod, + supervisionURI, + policyHash + ); + } + + function emergencySetJurisdictionProfile( + string calldata jurisdiction, + bool active, + bool requiresGovernmentApproval, + bool requiresPeriodicReports, + uint256 minimumUpgradeNoticePeriod, + string calldata supervisionURI, + bytes32 policyHash + ) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _setJurisdictionProfileValue( + jurisdiction, + active, + requiresGovernmentApproval, + requiresPeriodicReports, + minimumUpgradeNoticePeriod, + supervisionURI, + policyHash + ); + } + + function _setJurisdictionProfileValue( + string memory jurisdiction, + bool active, + bool requiresGovernmentApproval, + bool requiresPeriodicReports, + uint256 minimumUpgradeNoticePeriod, + string memory supervisionURI, + bytes32 policyHash + ) internal { + bytes32 jurisdictionId = jurisdictionIdFor(jurisdiction); + JurisdictionProfile storage profile = _jurisdictionProfiles[jurisdictionId]; + profile.active = active; + profile.requiresGovernmentApproval = requiresGovernmentApproval; + profile.requiresPeriodicReports = requiresPeriodicReports; + profile.minimumUpgradeNoticePeriod = minimumUpgradeNoticePeriod; + profile.supervisionURI = supervisionURI; + profile.policyHash = policyHash; + + emit JurisdictionProfileUpdated( + jurisdictionId, + jurisdiction, + active, + requiresGovernmentApproval, + requiresPeriodicReports, + minimumUpgradeNoticePeriod, + supervisionURI, + policyHash + ); + } + + function setJurisdictionAuthority( + string calldata jurisdiction, + address authority, + bool active, + bool canApproveGovernance, + bool canApproveUpgrades, + bool canPause, + bool canReceiveReports + ) external onlyGovernanceExecution { + _setJurisdictionAuthorityValue( + jurisdiction, + authority, + active, + canApproveGovernance, + canApproveUpgrades, + canPause, + canReceiveReports + ); + } + + function emergencySetJurisdictionAuthority( + string calldata jurisdiction, + address authority, + bool active, + bool canApproveGovernance, + bool canApproveUpgrades, + bool canPause, + bool canReceiveReports + ) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _setJurisdictionAuthorityValue( + jurisdiction, + authority, + active, + canApproveGovernance, + canApproveUpgrades, + canPause, + canReceiveReports + ); + } + + function _setJurisdictionAuthorityValue( + string memory jurisdiction, + address authority, + bool active, + bool canApproveGovernance, + bool canApproveUpgrades, + bool canPause, + bool canReceiveReports + ) internal { + require(authority != address(0), "Zero address"); + bytes32 jurisdictionId = jurisdictionIdFor(jurisdiction); + _jurisdictionAuthorities[jurisdictionId][authority] = JurisdictionAuthority({ + active: active, + canApproveGovernance: canApproveGovernance, + canApproveUpgrades: canApproveUpgrades, + canPause: canPause, + canReceiveReports: canReceiveReports + }); + + emit JurisdictionAuthorityUpdated( + jurisdictionId, + jurisdiction, + authority, + active, + canApproveGovernance, + canApproveUpgrades, + canPause, + canReceiveReports + ); + } + + function setAssetGovernanceProfile( + address token, + bytes32 governanceProfileId, + bytes32 supervisionProfileId, + bytes32 storageNamespace, + address canonicalUnderlyingAsset, + bool isWrappedTransport, + bool supervisionRequired, + bool governmentApprovalRequired, + uint256 minimumUpgradeNoticePeriod, + string calldata regulatoryDisclosureURI, + string calldata reportingURI + ) external onlyGovernanceExecution { + _setAssetGovernanceProfileValue( + token, + governanceProfileId, + supervisionProfileId, + storageNamespace, + canonicalUnderlyingAsset, + isWrappedTransport, + supervisionRequired, + governmentApprovalRequired, + minimumUpgradeNoticePeriod, + regulatoryDisclosureURI, + reportingURI + ); + } + + function emergencySetAssetGovernanceProfile( + address token, + bytes32 governanceProfileId, + bytes32 supervisionProfileId, + bytes32 storageNamespace, + address canonicalUnderlyingAsset, + bool isWrappedTransport, + bool supervisionRequired, + bool governmentApprovalRequired, + uint256 minimumUpgradeNoticePeriod, + string calldata regulatoryDisclosureURI, + string calldata reportingURI + ) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _setAssetGovernanceProfileValue( + token, + governanceProfileId, + supervisionProfileId, + storageNamespace, + canonicalUnderlyingAsset, + isWrappedTransport, + supervisionRequired, + governmentApprovalRequired, + minimumUpgradeNoticePeriod, + regulatoryDisclosureURI, + reportingURI + ); + } + + function _setAssetGovernanceProfileValue( + address token, + bytes32 governanceProfileId, + bytes32 supervisionProfileId, + bytes32 storageNamespace, + address canonicalUnderlyingAsset, + bool isWrappedTransport, + bool supervisionRequired, + bool governmentApprovalRequired, + uint256 minimumUpgradeNoticePeriod, + string memory regulatoryDisclosureURI, + string memory reportingURI + ) internal { + require(assets[token].isActive, "Not registered"); + + UniversalAsset storage asset = assets[token]; + asset.governanceProfileId = governanceProfileId; + asset.supervisionProfileId = supervisionProfileId; + asset.storageNamespace = storageNamespace; + asset.canonicalUnderlyingAsset = canonicalUnderlyingAsset; + asset.isWrappedTransport = isWrappedTransport; + asset.supervisionRequired = supervisionRequired; + asset.governmentApprovalRequired = governmentApprovalRequired; + asset.minimumUpgradeNoticePeriod = minimumUpgradeNoticePeriod; + asset.regulatoryDisclosureURI = regulatoryDisclosureURI; + asset.reportingURI = reportingURI; + asset.lastUpdated = block.timestamp; + + emit AssetGovernanceProfileUpdated(token, governanceProfileId, supervisionProfileId, storageNamespace); + emit AssetSupervisionProfileUpdated( + token, + canonicalUnderlyingAsset, + isWrappedTransport, + supervisionRequired, + governmentApprovalRequired, + minimumUpgradeNoticePeriod + ); + } + + function syncAssetMetadataFromToken(address token) external onlyGovernanceExecution { + require(assets[token].isActive, "Not registered"); + _syncAssetMetadata(token, assets[token]); + assets[token].lastUpdated = block.timestamp; + } + + function emergencySyncAssetMetadataFromToken(address token) external onlyRole(EMERGENCY_ADMIN_ROLE) { + require(assets[token].isActive, "Not registered"); + _syncAssetMetadata(token, assets[token]); + assets[token].lastUpdated = block.timestamp; + } + /** * @notice Get timelock period based on asset type and compliance */ @@ -456,6 +800,30 @@ contract UniversalAssetRegistry is return validators; } + function getJurisdictionProfile(string calldata jurisdiction) external view returns (JurisdictionProfile memory) { + return _jurisdictionProfiles[jurisdictionIdFor(jurisdiction)]; + } + + function getJurisdictionAuthority( + string calldata jurisdiction, + address authority + ) external view returns (JurisdictionAuthority memory) { + return _jurisdictionAuthorities[jurisdictionIdFor(jurisdiction)][authority]; + } + + function getAssetJurisdictionId(address token) external view returns (bytes32) { + return _assetJurisdictionIds[token]; + } + + function isGovernanceAuthority(bytes32 jurisdictionId, address authority) external view returns (bool) { + JurisdictionAuthority storage permissions = _jurisdictionAuthorities[jurisdictionId][authority]; + return permissions.active && permissions.canApproveGovernance; + } + + function getJurisdictionMinimumUpgradeNotice(bytes32 jurisdictionId) external view returns (uint256) { + return _jurisdictionProfiles[jurisdictionId].minimumUpgradeNoticePeriod; + } + function getProposal(bytes32 proposalId) external view returns (PendingAssetProposal memory) { return proposals[proposalId]; } @@ -463,4 +831,104 @@ contract UniversalAssetRegistry is function isAssetActive(address token) external view returns (bool) { return assets[token].isActive; } + + function jurisdictionIdFor(string memory jurisdiction) public pure returns (bytes32) { + return keccak256(bytes(jurisdiction)); + } + + function _applyJurisdictionDefaults(UniversalAsset storage asset, bytes32 jurisdictionId) internal { + JurisdictionProfile storage profile = _jurisdictionProfiles[jurisdictionId]; + if (!profile.active) { + return; + } + + asset.supervisionRequired = true; + asset.governmentApprovalRequired = profile.requiresGovernmentApproval; + if (profile.minimumUpgradeNoticePeriod > asset.minimumUpgradeNoticePeriod) { + asset.minimumUpgradeNoticePeriod = profile.minimumUpgradeNoticePeriod; + } + } + + function _syncAssetMetadata(address tokenAddress, UniversalAsset storage asset) internal { + asset.assetId = _defaultAssetId(asset.symbol); + asset.assetVersionId = _defaultAssetVersionId(asset.symbol); + asset.governanceProfileId = _defaultGovernanceProfileId(asset.symbol); + asset.supervisionProfileId = _defaultSupervisionProfileId(asset.symbol); + asset.storageNamespace = _defaultStorageNamespace(asset.symbol); + + try IRegulatedAssetMetadata(tokenAddress).assetId() returns (bytes32 value) { + asset.assetId = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).assetVersionId() returns (bytes32 value) { + asset.assetVersionId = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).governanceProfileId() returns (bytes32 value) { + asset.governanceProfileId = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).supervisionProfileId() returns (bytes32 value) { + asset.supervisionProfileId = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).storageNamespace() returns (bytes32 value) { + asset.storageNamespace = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).primaryJurisdiction() returns (string memory jurisdictionValue) { + if (bytes(jurisdictionValue).length > 0) { + asset.jurisdiction = jurisdictionValue; + _assetJurisdictionIds[tokenAddress] = jurisdictionIdFor(jurisdictionValue); + } + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).regulatoryDisclosureURI() returns (string memory value) { + asset.regulatoryDisclosureURI = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).reportingURI() returns (string memory value) { + asset.reportingURI = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).canonicalUnderlyingAsset() returns (address value) { + asset.canonicalUnderlyingAsset = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).supervisionRequired() returns (bool value) { + asset.supervisionRequired = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).governmentApprovalRequired() returns (bool value) { + asset.governmentApprovalRequired = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).minimumUpgradeNoticePeriod() returns (uint256 value) { + asset.minimumUpgradeNoticePeriod = value; + } catch {} + + try IRegulatedAssetMetadata(tokenAddress).wrappedTransport() returns (bool value) { + asset.isWrappedTransport = value; + } catch {} + } + + function _defaultAssetId(string memory symbol) internal pure returns (bytes32) { + return keccak256(bytes(string.concat("GRU:", symbol))); + } + + function _defaultAssetVersionId(string memory symbol) internal pure returns (bytes32) { + return keccak256(bytes(string.concat("GRU:", symbol, ":registry-v1"))); + } + + function _defaultGovernanceProfileId(string memory symbol) internal pure returns (bytes32) { + return keccak256(bytes(string.concat("GRU:GOV:", symbol))); + } + + function _defaultSupervisionProfileId(string memory symbol) internal pure returns (bytes32) { + return keccak256(bytes(string.concat("GRU:SUP:", symbol))); + } + + function _defaultStorageNamespace(string memory symbol) internal pure returns (bytes32) { + return keccak256(bytes(string.concat("gru.storage.registry.", symbol))); + } } diff --git a/contracts/tokens/CompliantFiatTokenV2.sol b/contracts/tokens/CompliantFiatTokenV2.sol new file mode 100644 index 0000000..4e4b08e --- /dev/null +++ b/contracts/tokens/CompliantFiatTokenV2.sol @@ -0,0 +1,751 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/utils/Nonces.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../compliance/LegallyCompliantBaseV2.sol"; +import "./interfaces/ICompliantFiatTokenV2.sol"; + +/** + * @title CompliantFiatTokenV2 + * @notice Canonical GRU c* V2 money token with permit, authorization transfers, explicit audit attribution, + * role-gated mint/burn, and version-aware asset identity. + */ +contract CompliantFiatTokenV2 is ERC20, Pausable, Nonces, EIP712, LegallyCompliantBaseV2, ICompliantFiatTokenV2 { + using ECDSA for bytes32; + + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant BRIDGE_ROLE = keccak256("BRIDGE_ROLE"); + bytes32 public constant SUPPLY_ADMIN_ROLE = keccak256("SUPPLY_ADMIN_ROLE"); + bytes32 public constant METADATA_ADMIN_ROLE = keccak256("METADATA_ADMIN_ROLE"); + bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); + bytes32 public constant JURISDICTION_ADMIN_ROLE = keccak256("JURISDICTION_ADMIN_ROLE"); + bytes32 public constant REGULATOR_ROLE = keccak256("REGULATOR_ROLE"); + bytes32 public constant SUPERVISOR_ROLE = keccak256("SUPERVISOR_ROLE"); + bytes32 public constant EMERGENCY_ADMIN_ROLE = keccak256("EMERGENCY_ADMIN_ROLE"); + + bytes32 public constant OPERATION_TRANSFER = keccak256("TRANSFER"); + bytes32 public constant OPERATION_MINT = keccak256("MINT"); + bytes32 public constant OPERATION_BURN = keccak256("BURN"); + bytes32 public constant OPERATION_TRANSFER_WITH_AUTHORIZATION = keccak256("TRANSFER_WITH_AUTHORIZATION"); + bytes32 public constant OPERATION_RECEIVE_WITH_AUTHORIZATION = keccak256("RECEIVE_WITH_AUTHORIZATION"); + + bytes32 public constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = + keccak256( + "TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" + ); + bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = + keccak256( + "ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" + ); + bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = + keccak256("CancelAuthorization(address authorizer,bytes32 nonce)"); + + uint8 private immutable _decimalsStorage; + string private _currencyCode; + string private _tokenURI; + string public versionTag; + string public symbolDisplay; + bytes32 public immutable assetId; + bytes32 public immutable assetVersionId; + bool public forwardCanonical; + address private _owner; + address public governanceController; + bytes32 public governanceProfileId; + bytes32 public supervisionProfileId; + bytes32 public storageNamespace; + string public primaryJurisdiction; + string private _regulatoryDisclosureURI; + string private _reportingURI; + address public canonicalUnderlyingAsset; + bool public supervisionRequired; + bool public governmentApprovalRequired; + uint256 public minimumUpgradeNoticePeriod; + + uint256 public supplyCap; + uint256 public mintingPeriodCap; + uint256 public mintingPeriodDuration; + uint256 public currentMintingPeriodStart; + uint256 public mintedInCurrentPeriod; + + string[] private _legacyAliases; + mapping(bytes32 => bool) private _legacyAliasExists; + mapping(address => mapping(bytes32 => bool)) private _authorizationStates; + + struct OperationContext { + bool active; + bytes32 operationType; + address initiator; + address authorizer; + address executor; + bytes32 reasonHash; + bytes32 accountingRef; + bytes32 messageCorrelationId; + bytes32 additionalDataHash; + } + + OperationContext private _operationContext; + + error ERC2612ExpiredSignature(uint256 deadline); + error ERC2612InvalidSigner(address signer, address owner); + error AuthorizationExpired(uint256 validBefore); + error AuthorizationNotYetValid(uint256 validAfter); + error AuthorizationAlreadyUsed(address authorizer, bytes32 nonce); + error AuthorizationInvalidSigner(address signer, address authorizer); + error AuthorizationMustBeUsedByPayee(address payee, address caller); + error SupplyCapExceeded(uint256 cap, uint256 attemptedTotalSupply); + error MintCapExceeded(uint256 cap, uint256 attemptedPeriodMint); + error EmptyAlias(); + error DuplicateAlias(string aliasValue); + error OwnerUnauthorized(address caller); + error ZeroOwner(); + error GovernanceControllerOnly(address caller); + error GovernanceControllerNotConfigured(); + error ZeroGovernanceController(); + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + modifier onlyGovernanceExecution() { + if (governanceController == address(0)) revert GovernanceControllerNotConfigured(); + if (_msgSender() != governanceController) revert GovernanceControllerOnly(_msgSender()); + _; + } + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + string memory currencyCode_, + string memory versionTag_, + address initialOperator, + address admin, + uint256 initialSupply, + bool forwardCanonical_ + ) ERC20(name_, symbol_) EIP712(name_, versionTag_) LegallyCompliantBaseV2(admin) { + _decimalsStorage = decimals_; + _currencyCode = currencyCode_; + versionTag = versionTag_; + symbolDisplay = symbol_; + forwardCanonical = forwardCanonical_; + _owner = admin; + assetId = keccak256(bytes(string.concat("GRU:", symbol_))); + assetVersionId = keccak256(bytes(string.concat("GRU:", symbol_, ":", versionTag_))); + + _grantRole(MINTER_ROLE, initialOperator); + _grantRole(BURNER_ROLE, initialOperator); + _grantRole(PAUSER_ROLE, initialOperator); + _grantRole(BRIDGE_ROLE, initialOperator); + _grantRole(SUPPLY_ADMIN_ROLE, admin); + _grantRole(METADATA_ADMIN_ROLE, admin); + _grantRole(GOVERNANCE_ROLE, admin); + _grantRole(JURISDICTION_ADMIN_ROLE, admin); + _grantRole(REGULATOR_ROLE, admin); + _grantRole(SUPERVISOR_ROLE, admin); + _grantRole(EMERGENCY_ADMIN_ROLE, admin); + _grantRole(MINTER_ROLE, admin); + _grantRole(BURNER_ROLE, admin); + _grantRole(PAUSER_ROLE, admin); + _grantRole(BRIDGE_ROLE, admin); + + governanceProfileId = keccak256(bytes(string.concat("GRU:GOV:", symbol_, ":", versionTag_))); + supervisionProfileId = keccak256(bytes(string.concat("GRU:SUP:", currencyCode_))); + storageNamespace = keccak256(bytes(string.concat("gru.storage.asset.", symbol_, ".", versionTag_))); + primaryJurisdiction = LEGAL_JURISDICTION; + supervisionRequired = true; + minimumUpgradeNoticePeriod = 7 days; + + emit OwnershipTransferred(address(0), admin); + + if (initialSupply > 0) { + _setOperationContext( + OPERATION_MINT, + initialOperator, + initialOperator, + initialOperator, + bytes32(0), + bytes32(0), + bytes32(0), + bytes32(0) + ); + _mint(initialOperator, initialSupply); + } + } + + function decimals() public view override(ERC20, IERC20Metadata) returns (uint8) { + return _decimalsStorage; + } + + function currencyCode() external view returns (string memory) { + return _currencyCode; + } + + function owner() public view returns (address) { + return _owner; + } + + function tokenURI() external view returns (string memory) { + return _tokenURI; + } + + function regulatoryDisclosureURI() external view returns (string memory) { + return _regulatoryDisclosureURI; + } + + function reportingURI() external view returns (string memory) { + return _reportingURI; + } + + function wrappedTransport() external pure returns (bool) { + return false; + } + + function legacyAliases() external view returns (string[] memory) { + return _legacyAliases; + } + + function authorizationState(address authorizer, bytes32 nonce) public view returns (bool) { + return _authorizationStates[authorizer][nonce]; + } + + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + function nonces(address tokenOwner) public view override(Nonces, IERC20Permit) returns (uint256) { + return super.nonces(tokenOwner); + } + + function pause() external { + _checkPauseAuthority(); + _pause(); + } + + function unpause() external { + _checkPauseAuthority(); + _unpause(); + } + + function transferOwnership(address newOwner) external { + if (_msgSender() != _owner && !hasRole(DEFAULT_ADMIN_ROLE, _msgSender())) { + revert OwnerUnauthorized(_msgSender()); + } + if (newOwner == address(0)) revert ZeroOwner(); + address previousOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(previousOwner, newOwner); + } + + function setGovernanceController(address governanceController_) external onlyRole(EMERGENCY_ADMIN_ROLE) { + if (governanceController_ == address(0)) revert ZeroGovernanceController(); + governanceController = governanceController_; + } + + function setForwardCanonical(bool value) external onlyGovernanceExecution { + _setForwardCanonicalValue(value); + } + + function emergencySetForwardCanonical(bool value) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _setForwardCanonicalValue(value); + } + + function setTokenURI(string calldata tokenURI_) external onlyGovernanceExecution { + _setTokenURIValue(tokenURI_); + } + + function emergencySetPresentationMetadata( + bool forwardCanonical_, + string calldata tokenURI_, + string calldata symbolDisplay_ + ) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _setForwardCanonicalValue(forwardCanonical_); + _setTokenURIValue(tokenURI_); + _setSymbolDisplayValue(symbolDisplay_); + } + + function setSymbolDisplay(string calldata symbolDisplay_) external onlyGovernanceExecution { + _setSymbolDisplayValue(symbolDisplay_); + } + + function addLegacyAlias(string calldata aliasValue) external onlyGovernanceExecution { + _addLegacyAliasValue(aliasValue); + } + + function emergencyAddLegacyAlias(string calldata aliasValue) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _addLegacyAliasValue(aliasValue); + } + + function setSupplyControls( + uint256 supplyCap_, + uint256 mintingPeriodCap_, + uint256 mintingPeriodDuration_ + ) external onlyRole(SUPPLY_ADMIN_ROLE) { + supplyCap = supplyCap_; + mintingPeriodCap = mintingPeriodCap_; + mintingPeriodDuration = mintingPeriodDuration_; + if (mintingPeriodDuration_ > 0 && currentMintingPeriodStart == 0) { + currentMintingPeriodStart = block.timestamp; + } + emit SupplyControlsUpdated(supplyCap_, mintingPeriodCap_, mintingPeriodDuration_); + } + + function setGovernanceProfileId(bytes32 governanceProfileId_) external onlyGovernanceExecution { + _setGovernanceProfileIdValue(governanceProfileId_); + } + + function setSupervisionProfileId(bytes32 supervisionProfileId_) external onlyGovernanceExecution { + _setSupervisionProfileIdValue(supervisionProfileId_); + } + + function setStorageNamespace(bytes32 storageNamespace_) external onlyGovernanceExecution { + _setStorageNamespaceValue(storageNamespace_); + } + + function setPrimaryJurisdiction(string calldata jurisdiction_) external onlyGovernanceExecution { + _setPrimaryJurisdictionValue(jurisdiction_); + } + + function setRegulatoryDisclosureURI(string calldata disclosureURI_) external onlyGovernanceExecution { + _setRegulatoryDisclosureURIValue(disclosureURI_); + } + + function setReportingURI(string calldata reportingURI_) external onlyGovernanceExecution { + _setReportingURIValue(reportingURI_); + } + + function setCanonicalUnderlyingAsset(address canonicalUnderlyingAsset_) external onlyGovernanceExecution { + _setCanonicalUnderlyingAssetValue(canonicalUnderlyingAsset_); + } + + function setSupervisionConfiguration( + bool supervisionRequired_, + bool governmentApprovalRequired_, + uint256 minimumUpgradeNoticePeriod_ + ) external onlyGovernanceExecution { + _setSupervisionConfigurationValue( + supervisionRequired_, + governmentApprovalRequired_, + minimumUpgradeNoticePeriod_ + ); + } + + function emergencySetGovernanceMetadata( + bytes32 governanceProfileId_, + bytes32 supervisionProfileId_, + bytes32 storageNamespace_, + string calldata jurisdiction_, + address canonicalUnderlyingAsset_, + bool supervisionRequired_, + bool governmentApprovalRequired_, + uint256 minimumUpgradeNoticePeriod_ + ) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _setGovernanceProfileIdValue(governanceProfileId_); + _setSupervisionProfileIdValue(supervisionProfileId_); + _setStorageNamespaceValue(storageNamespace_); + _setPrimaryJurisdictionValue(jurisdiction_); + _setCanonicalUnderlyingAssetValue(canonicalUnderlyingAsset_); + _setSupervisionConfigurationValue( + supervisionRequired_, + governmentApprovalRequired_, + minimumUpgradeNoticePeriod_ + ); + } + + function emergencySetDisclosureMetadata( + string calldata disclosureURI_, + string calldata reportingURI_ + ) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _setRegulatoryDisclosureURIValue(disclosureURI_); + _setReportingURIValue(reportingURI_); + } + + function recordRegulatoryApproval( + bytes32 approvalId, + string calldata actionType, + bytes32 referenceHash + ) external onlyRole(REGULATOR_ROLE) { + emit RegulatoryApprovalRecorded(approvalId, actionType, referenceHash); + } + + function recordSupervisoryNotice( + bytes32 noticeId, + string calldata category, + string calldata uri + ) external onlyRole(SUPERVISOR_ROLE) { + emit SupervisoryNoticeRecorded(noticeId, category, uri); + } + + function permit( + address tokenOwner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public { + if (block.timestamp > deadline) revert ERC2612ExpiredSignature(deadline); + + bytes32 structHash = keccak256( + abi.encode(PERMIT_TYPEHASH, tokenOwner, spender, value, _useNonce(tokenOwner), deadline) + ); + bytes32 digest = _hashTypedDataV4(structHash); + address signer = ECDSA.recover(digest, v, r, s); + if (signer != tokenOwner) revert ERC2612InvalidSigner(signer, tokenOwner); + + _approve(tokenOwner, spender, value); + } + + function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { + mint(to, amount, bytes32(0)); + } + + function mint(address to, uint256 amount, bytes32 reasonHash) public onlyRole(MINTER_ROLE) { + _consumeMintCapacity(amount); + _setOperationContext( + OPERATION_MINT, + _msgSender(), + _msgSender(), + _msgSender(), + reasonHash, + bytes32(0), + bytes32(0), + bytes32(0) + ); + _mint(to, amount); + } + + function burn(uint256 amount) external { + _setOperationContext( + OPERATION_BURN, + _msgSender(), + _msgSender(), + _msgSender(), + bytes32(0), + bytes32(0), + bytes32(0), + bytes32(0) + ); + _burn(_msgSender(), amount); + } + + function burn(address from, uint256 amount, bytes32 reasonHash) public onlyRole(BURNER_ROLE) { + _setOperationContext( + OPERATION_BURN, + from, + from, + _msgSender(), + reasonHash, + bytes32(0), + bytes32(0), + bytes32(0) + ); + _burn(from, amount); + } + + function transferWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external { + _useAuthorization( + from, + to, + value, + validAfter, + validBefore, + nonce, + v, + r, + s, + TRANSFER_WITH_AUTHORIZATION_TYPEHASH, + OPERATION_TRANSFER_WITH_AUTHORIZATION + ); + } + + function receiveWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external { + if (_msgSender() != to) revert AuthorizationMustBeUsedByPayee(to, _msgSender()); + _useAuthorization( + from, + to, + value, + validAfter, + validBefore, + nonce, + v, + r, + s, + RECEIVE_WITH_AUTHORIZATION_TYPEHASH, + OPERATION_RECEIVE_WITH_AUTHORIZATION + ); + } + + function cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external { + if (_authorizationStates[authorizer][nonce]) revert AuthorizationAlreadyUsed(authorizer, nonce); + bytes32 structHash = keccak256(abi.encode(CANCEL_AUTHORIZATION_TYPEHASH, authorizer, nonce)); + bytes32 digest = _hashTypedDataV4(structHash); + address signer = ECDSA.recover(digest, v, r, s); + if (signer != authorizer) revert AuthorizationInvalidSigner(signer, authorizer); + + _authorizationStates[authorizer][nonce] = true; + emit AuthorizationCanceled(authorizer, nonce); + } + + function _update(address from, address to, uint256 amount) internal override whenNotPaused { + OperationContext memory ctx = _operationContext; + if (!ctx.active) { + ctx = OperationContext({ + active: false, + operationType: OPERATION_TRANSFER, + initiator: from == address(0) ? _msgSender() : from, + authorizer: from == address(0) ? _msgSender() : from, + executor: _msgSender(), + reasonHash: bytes32(0), + accountingRef: bytes32(0), + messageCorrelationId: bytes32(0), + additionalDataHash: bytes32(0) + }); + } + + _enforceCompliantOperation( + ctx.operationType, + ctx.initiator, + ctx.authorizer, + ctx.executor, + from, + to, + amount, + ctx.reasonHash, + ctx.accountingRef, + ctx.messageCorrelationId, + ctx.additionalDataHash + ); + + super._update(from, to, amount); + + _recordCompliantOperation( + ctx.operationType, + ctx.initiator, + ctx.authorizer, + ctx.executor, + from, + to, + amount, + ctx.reasonHash, + ctx.accountingRef, + ctx.messageCorrelationId, + ctx.additionalDataHash + ); + + if (_operationContext.active) { + delete _operationContext; + } + } + + function _useAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s, + bytes32 typeHash, + bytes32 operationType + ) internal { + if (block.timestamp <= validAfter) revert AuthorizationNotYetValid(validAfter); + if (block.timestamp >= validBefore) revert AuthorizationExpired(validBefore); + if (_authorizationStates[from][nonce]) revert AuthorizationAlreadyUsed(from, nonce); + + bytes32 structHash = keccak256( + abi.encode(typeHash, from, to, value, validAfter, validBefore, nonce) + ); + bytes32 digest = _hashTypedDataV4(structHash); + address signer = ECDSA.recover(digest, v, r, s); + if (signer != from) revert AuthorizationInvalidSigner(signer, from); + + _authorizationStates[from][nonce] = true; + _setOperationContext( + operationType, + from, + from, + _msgSender(), + bytes32(0), + bytes32(0), + bytes32(0), + nonce + ); + _transfer(from, to, value); + emit AuthorizationUsed(from, to, nonce, value); + } + + function _consumeMintCapacity(uint256 amount) internal { + if (supplyCap > 0 && totalSupply() + amount > supplyCap) { + revert SupplyCapExceeded(supplyCap, totalSupply() + amount); + } + + if (mintingPeriodCap == 0 || mintingPeriodDuration == 0) { + return; + } + + if (currentMintingPeriodStart == 0 || block.timestamp >= currentMintingPeriodStart + mintingPeriodDuration) { + currentMintingPeriodStart = block.timestamp; + mintedInCurrentPeriod = 0; + } + + uint256 attemptedPeriodMint = mintedInCurrentPeriod + amount; + if (attemptedPeriodMint > mintingPeriodCap) { + revert MintCapExceeded(mintingPeriodCap, attemptedPeriodMint); + } + + mintedInCurrentPeriod = attemptedPeriodMint; + } + + function _setOperationContext( + bytes32 operationType, + address initiator, + address authorizer, + address executor, + bytes32 reasonHash, + bytes32 accountingRef, + bytes32 messageCorrelationId, + bytes32 additionalDataHash + ) internal { + _operationContext = OperationContext({ + active: true, + operationType: operationType, + initiator: initiator, + authorizer: authorizer, + executor: executor, + reasonHash: reasonHash, + accountingRef: accountingRef, + messageCorrelationId: messageCorrelationId, + additionalDataHash: additionalDataHash + }); + } + + function _enforceCompliantOperation( + bytes32, + address, + address, + address, + address, + address, + uint256, + bytes32, + bytes32, + bytes32, + bytes32 + ) internal view virtual { + // Stub hook for future PolicyRouter / ComplianceGate / ReserveGate integration. + } + + function _checkPauseAuthority() internal view { + if (_msgSender() == _owner) { + return; + } + if (!hasRole(PAUSER_ROLE, _msgSender())) { + revert OwnerUnauthorized(_msgSender()); + } + } + + function _setForwardCanonicalValue(bool value) internal { + forwardCanonical = value; + emit ForwardCanonicalUpdated(value); + } + + function _setTokenURIValue(string memory tokenURI_) internal { + _tokenURI = tokenURI_; + emit TokenURIUpdated(tokenURI_); + } + + function _setSymbolDisplayValue(string memory symbolDisplay_) internal { + symbolDisplay = symbolDisplay_; + emit SymbolDisplayUpdated(symbolDisplay_); + } + + function _addLegacyAliasValue(string memory aliasValue) internal { + if (bytes(aliasValue).length == 0) revert EmptyAlias(); + bytes32 aliasHash = keccak256(bytes(aliasValue)); + if (_legacyAliasExists[aliasHash]) revert DuplicateAlias(aliasValue); + _legacyAliasExists[aliasHash] = true; + _legacyAliases.push(aliasValue); + emit LegacyAliasAdded(aliasValue); + } + + function _setGovernanceProfileIdValue(bytes32 governanceProfileId_) internal { + governanceProfileId = governanceProfileId_; + emit GovernanceProfileUpdated(governanceProfileId_); + } + + function _setSupervisionProfileIdValue(bytes32 supervisionProfileId_) internal { + supervisionProfileId = supervisionProfileId_; + emit SupervisionProfileUpdated(supervisionProfileId_); + } + + function _setStorageNamespaceValue(bytes32 storageNamespace_) internal { + storageNamespace = storageNamespace_; + emit StorageNamespaceUpdated(storageNamespace_); + } + + function _setPrimaryJurisdictionValue(string memory jurisdiction_) internal { + primaryJurisdiction = jurisdiction_; + emit PrimaryJurisdictionUpdated(jurisdiction_); + } + + function _setRegulatoryDisclosureURIValue(string memory disclosureURI_) internal { + _regulatoryDisclosureURI = disclosureURI_; + emit RegulatoryDisclosureURIUpdated(disclosureURI_); + } + + function _setReportingURIValue(string memory reportingURI_) internal { + _reportingURI = reportingURI_; + emit ReportingURIUpdated(reportingURI_); + } + + function _setCanonicalUnderlyingAssetValue(address canonicalUnderlyingAsset_) internal { + canonicalUnderlyingAsset = canonicalUnderlyingAsset_; + emit CanonicalUnderlyingAssetUpdated(canonicalUnderlyingAsset_); + } + + function _setSupervisionConfigurationValue( + bool supervisionRequired_, + bool governmentApprovalRequired_, + uint256 minimumUpgradeNoticePeriod_ + ) internal { + supervisionRequired = supervisionRequired_; + governmentApprovalRequired = governmentApprovalRequired_; + minimumUpgradeNoticePeriod = minimumUpgradeNoticePeriod_; + emit SupervisionConfigurationUpdated( + supervisionRequired_, + governmentApprovalRequired_, + minimumUpgradeNoticePeriod_ + ); + } +} diff --git a/contracts/tokens/CompliantWrappedToken.sol b/contracts/tokens/CompliantWrappedToken.sol index f2fa416..09fe27b 100644 --- a/contracts/tokens/CompliantWrappedToken.sol +++ b/contracts/tokens/CompliantWrappedToken.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; +import "../interfaces/IRegulatedAssetMetadata.sol"; /** * @title CompliantWrappedToken @@ -10,11 +11,57 @@ import "@openzeppelin/contracts/access/AccessControl.sol"; * @dev Only MINTER_ROLE can mint (e.g. CCIP receiver/bridge); BURNER_ROLE can burn (bridge-back). * Admin can grant/revoke roles. Used for cW* representation of Chain 138 compliant tokens. */ -contract CompliantWrappedToken is ERC20, AccessControl { +contract CompliantWrappedToken is ERC20, AccessControl, IRegulatedAssetMetadata { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); + bytes32 public constant JURISDICTION_ADMIN_ROLE = keccak256("JURISDICTION_ADMIN_ROLE"); + bytes32 public constant REGULATOR_ROLE = keccak256("REGULATOR_ROLE"); + bytes32 public constant SUPERVISOR_ROLE = keccak256("SUPERVISOR_ROLE"); + bytes32 public constant EMERGENCY_ADMIN_ROLE = keccak256("EMERGENCY_ADMIN_ROLE"); uint8 private immutable _decimalsStorage; + bool public operationalRolesFrozen; + address public governanceController; + bytes32 public immutable assetId; + bytes32 public immutable assetVersionId; + bytes32 public governanceProfileId; + bytes32 public supervisionProfileId; + bytes32 public storageNamespace; + string public primaryJurisdiction; + string private _regulatoryDisclosureURI; + string private _reportingURI; + address public canonicalUnderlyingAsset; + bool public supervisionRequired; + bool public governmentApprovalRequired; + uint256 public minimumUpgradeNoticePeriod; + + event OperationalRolesFrozen(address indexed caller); + event GovernanceProfileUpdated(bytes32 governanceProfileId); + event SupervisionProfileUpdated(bytes32 supervisionProfileId); + event StorageNamespaceUpdated(bytes32 storageNamespace); + event PrimaryJurisdictionUpdated(string jurisdiction); + event RegulatoryDisclosureURIUpdated(string disclosureURI); + event ReportingURIUpdated(string reportingURI); + event CanonicalUnderlyingAssetUpdated(address canonicalUnderlyingAsset); + event SupervisionConfigurationUpdated( + bool supervisionRequired, + bool governmentApprovalRequired, + uint256 minimumUpgradeNoticePeriod + ); + event RegulatoryApprovalRecorded(bytes32 indexed approvalId, string actionType, bytes32 referenceHash); + event SupervisoryNoticeRecorded(bytes32 indexed noticeId, string category, string uri); + + error OperationalRolesAreFrozen(); + error GovernanceControllerOnly(address caller); + error GovernanceControllerNotConfigured(); + error ZeroGovernanceController(); + + modifier onlyGovernanceExecution() { + if (governanceController == address(0)) revert GovernanceControllerNotConfigured(); + if (msg.sender != governanceController) revert GovernanceControllerOnly(msg.sender); + _; + } constructor( string memory name_, @@ -23,15 +70,153 @@ contract CompliantWrappedToken is ERC20, AccessControl { address admin_ ) ERC20(name_, symbol_) { _decimalsStorage = decimals_; + assetId = keccak256(bytes(string.concat("GRU:", symbol_))); + assetVersionId = keccak256(bytes(string.concat("GRU:", symbol_, ":transport"))); + governanceProfileId = keccak256(bytes(string.concat("GRU:GOV:", symbol_, ":transport"))); + supervisionProfileId = keccak256(bytes(string.concat("GRU:SUP:", symbol_, ":transport"))); + storageNamespace = keccak256(bytes(string.concat("gru.storage.transport.", symbol_))); + primaryJurisdiction = "Destination Public Network"; + supervisionRequired = true; + minimumUpgradeNoticePeriod = 7 days; _grantRole(DEFAULT_ADMIN_ROLE, admin_); _grantRole(MINTER_ROLE, admin_); _grantRole(BURNER_ROLE, admin_); + _grantRole(GOVERNANCE_ROLE, admin_); + _grantRole(JURISDICTION_ADMIN_ROLE, admin_); + _grantRole(REGULATOR_ROLE, admin_); + _grantRole(SUPERVISOR_ROLE, admin_); + _grantRole(EMERGENCY_ADMIN_ROLE, admin_); } function decimals() public view override returns (uint8) { return _decimalsStorage; } + function wrappedTransport() external pure returns (bool) { + return true; + } + + function regulatoryDisclosureURI() external view returns (string memory) { + return _regulatoryDisclosureURI; + } + + function reportingURI() external view returns (string memory) { + return _reportingURI; + } + + /** + * @notice Permanently freeze future MINTER_ROLE / BURNER_ROLE changes. + * @dev Existing bridge roles keep working; only admin churn is disabled. + */ + function freezeOperationalRoles() external onlyRole(DEFAULT_ADMIN_ROLE) { + if (operationalRolesFrozen) { + return; + } + operationalRolesFrozen = true; + emit OperationalRolesFrozen(msg.sender); + } + + function grantRole(bytes32 role, address account) public override onlyRole(getRoleAdmin(role)) { + _revertIfFrozenOperationalRole(role); + super.grantRole(role, account); + } + + function revokeRole(bytes32 role, address account) public override onlyRole(getRoleAdmin(role)) { + _revertIfFrozenOperationalRole(role); + super.revokeRole(role, account); + } + + function setGovernanceController(address governanceController_) external onlyRole(EMERGENCY_ADMIN_ROLE) { + if (governanceController_ == address(0)) revert ZeroGovernanceController(); + governanceController = governanceController_; + } + + function setGovernanceProfileId(bytes32 governanceProfileId_) external onlyGovernanceExecution { + _setGovernanceProfileIdValue(governanceProfileId_); + } + + function setSupervisionProfileId(bytes32 supervisionProfileId_) external onlyGovernanceExecution { + _setSupervisionProfileIdValue(supervisionProfileId_); + } + + function setStorageNamespace(bytes32 storageNamespace_) external onlyGovernanceExecution { + _setStorageNamespaceValue(storageNamespace_); + } + + function setPrimaryJurisdiction(string calldata jurisdiction_) external onlyGovernanceExecution { + _setPrimaryJurisdictionValue(jurisdiction_); + } + + function setRegulatoryDisclosureURI(string calldata disclosureURI_) external onlyGovernanceExecution { + _setRegulatoryDisclosureURIValue(disclosureURI_); + } + + function setReportingURI(string calldata reportingURI_) external onlyGovernanceExecution { + _setReportingURIValue(reportingURI_); + } + + function setCanonicalUnderlyingAsset(address canonicalUnderlyingAsset_) external onlyGovernanceExecution { + _setCanonicalUnderlyingAssetValue(canonicalUnderlyingAsset_); + } + + function setSupervisionConfiguration( + bool supervisionRequired_, + bool governmentApprovalRequired_, + uint256 minimumUpgradeNoticePeriod_ + ) external onlyGovernanceExecution { + _setSupervisionConfigurationValue( + supervisionRequired_, + governmentApprovalRequired_, + minimumUpgradeNoticePeriod_ + ); + } + + function emergencySetGovernanceMetadata( + bytes32 governanceProfileId_, + bytes32 supervisionProfileId_, + bytes32 storageNamespace_, + string calldata jurisdiction_, + address canonicalUnderlyingAsset_, + bool supervisionRequired_, + bool governmentApprovalRequired_, + uint256 minimumUpgradeNoticePeriod_ + ) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _setGovernanceProfileIdValue(governanceProfileId_); + _setSupervisionProfileIdValue(supervisionProfileId_); + _setStorageNamespaceValue(storageNamespace_); + _setPrimaryJurisdictionValue(jurisdiction_); + _setCanonicalUnderlyingAssetValue(canonicalUnderlyingAsset_); + _setSupervisionConfigurationValue( + supervisionRequired_, + governmentApprovalRequired_, + minimumUpgradeNoticePeriod_ + ); + } + + function emergencySetDisclosureMetadata( + string calldata disclosureURI_, + string calldata reportingURI_ + ) external onlyRole(EMERGENCY_ADMIN_ROLE) { + _setRegulatoryDisclosureURIValue(disclosureURI_); + _setReportingURIValue(reportingURI_); + } + + function recordRegulatoryApproval( + bytes32 approvalId, + string calldata actionType, + bytes32 referenceHash + ) external onlyRole(REGULATOR_ROLE) { + emit RegulatoryApprovalRecorded(approvalId, actionType, referenceHash); + } + + function recordSupervisoryNotice( + bytes32 noticeId, + string calldata category, + string calldata uri + ) external onlyRole(SUPERVISOR_ROLE) { + emit SupervisoryNoticeRecorded(noticeId, category, uri); + } + /// @notice Mint to account (only MINTER_ROLE, e.g. bridge receiver). function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { _mint(to, amount); @@ -46,4 +231,60 @@ contract CompliantWrappedToken is ERC20, AccessControl { function burnFrom(address from, uint256 amount) external onlyRole(BURNER_ROLE) { _burn(from, amount); } + + function _revertIfFrozenOperationalRole(bytes32 role) internal view { + if (operationalRolesFrozen && (role == MINTER_ROLE || role == BURNER_ROLE)) { + revert OperationalRolesAreFrozen(); + } + } + + function _setGovernanceProfileIdValue(bytes32 governanceProfileId_) internal { + governanceProfileId = governanceProfileId_; + emit GovernanceProfileUpdated(governanceProfileId_); + } + + function _setSupervisionProfileIdValue(bytes32 supervisionProfileId_) internal { + supervisionProfileId = supervisionProfileId_; + emit SupervisionProfileUpdated(supervisionProfileId_); + } + + function _setStorageNamespaceValue(bytes32 storageNamespace_) internal { + storageNamespace = storageNamespace_; + emit StorageNamespaceUpdated(storageNamespace_); + } + + function _setPrimaryJurisdictionValue(string memory jurisdiction_) internal { + primaryJurisdiction = jurisdiction_; + emit PrimaryJurisdictionUpdated(jurisdiction_); + } + + function _setRegulatoryDisclosureURIValue(string memory disclosureURI_) internal { + _regulatoryDisclosureURI = disclosureURI_; + emit RegulatoryDisclosureURIUpdated(disclosureURI_); + } + + function _setReportingURIValue(string memory reportingURI_) internal { + _reportingURI = reportingURI_; + emit ReportingURIUpdated(reportingURI_); + } + + function _setCanonicalUnderlyingAssetValue(address canonicalUnderlyingAsset_) internal { + canonicalUnderlyingAsset = canonicalUnderlyingAsset_; + emit CanonicalUnderlyingAssetUpdated(canonicalUnderlyingAsset_); + } + + function _setSupervisionConfigurationValue( + bool supervisionRequired_, + bool governmentApprovalRequired_, + uint256 minimumUpgradeNoticePeriod_ + ) internal { + supervisionRequired = supervisionRequired_; + governmentApprovalRequired = governmentApprovalRequired_; + minimumUpgradeNoticePeriod = minimumUpgradeNoticePeriod_; + emit SupervisionConfigurationUpdated( + supervisionRequired_, + governmentApprovalRequired_, + minimumUpgradeNoticePeriod_ + ); + } } diff --git a/contracts/tokens/interfaces/ICompliantFiatTokenV2.sol b/contracts/tokens/interfaces/ICompliantFiatTokenV2.sol new file mode 100644 index 0000000..f1b71a2 --- /dev/null +++ b/contracts/tokens/interfaces/ICompliantFiatTokenV2.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; +import {IeMoneyToken} from "../../emoney/interfaces/IeMoneyToken.sol"; +import {IRegulatedAssetMetadata} from "../../interfaces/IRegulatedAssetMetadata.sol"; + +/** + * @title ICompliantFiatTokenV2 + * @notice Canonical interface for GRU c* V2 compliant money tokens. + */ +interface ICompliantFiatTokenV2 is IERC20Metadata, IERC20Permit, IERC5267, IeMoneyToken, IRegulatedAssetMetadata { + event TokenURIUpdated(string tokenURI); + event SymbolDisplayUpdated(string symbolDisplay); + event LegacyAliasAdded(string aliasValue); + event ForwardCanonicalUpdated(bool forwardCanonical); + event SupplyControlsUpdated(uint256 supplyCap, uint256 mintingPeriodCap, uint256 mintingPeriodDuration); + event GovernanceProfileUpdated(bytes32 governanceProfileId); + event SupervisionProfileUpdated(bytes32 supervisionProfileId); + event StorageNamespaceUpdated(bytes32 storageNamespace); + event PrimaryJurisdictionUpdated(string jurisdiction); + event RegulatoryDisclosureURIUpdated(string disclosureURI); + event ReportingURIUpdated(string reportingURI); + event CanonicalUnderlyingAssetUpdated(address canonicalUnderlyingAsset); + event SupervisionConfigurationUpdated( + bool supervisionRequired, + bool governmentApprovalRequired, + uint256 minimumUpgradeNoticePeriod + ); + event RegulatoryApprovalRecorded(bytes32 indexed approvalId, string actionType, bytes32 referenceHash); + event SupervisoryNoticeRecorded(bytes32 indexed noticeId, string category, string uri); + event AuthorizationUsed(address indexed authorizer, address indexed to, bytes32 indexed nonce, uint256 value); + event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce); + + function assetId() external view returns (bytes32); + function assetVersionId() external view returns (bytes32); + function owner() external view returns (address); + function currencyCode() external view returns (string memory); + function versionTag() external view returns (string memory); + function symbolDisplay() external view returns (string memory); + function tokenURI() external view returns (string memory); + function forwardCanonical() external view returns (bool); + function governanceController() external view returns (address); + function legacyAliases() external view returns (string[] memory); + function authorizationState(address authorizer, bytes32 nonce) external view returns (bool); + function transferOwnership(address newOwner) external; + function setGovernanceController(address governanceController_) external; + function mint(address to, uint256 amount) external; + function burn(uint256 amount) external; + function transferWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external; + function receiveWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external; + function cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external; + function setGovernanceProfileId(bytes32 governanceProfileId_) external; + function setSupervisionProfileId(bytes32 supervisionProfileId_) external; + function setStorageNamespace(bytes32 storageNamespace_) external; + function setPrimaryJurisdiction(string calldata jurisdiction_) external; + function setRegulatoryDisclosureURI(string calldata disclosureURI_) external; + function setReportingURI(string calldata reportingURI_) external; + function setCanonicalUnderlyingAsset(address canonicalUnderlyingAsset_) external; + function setSupervisionConfiguration( + bool supervisionRequired_, + bool governmentApprovalRequired_, + uint256 minimumUpgradeNoticePeriod_ + ) external; + function recordRegulatoryApproval(bytes32 approvalId, string calldata actionType, bytes32 referenceHash) external; + function recordSupervisoryNotice(bytes32 noticeId, string calldata category, string calldata uri) external; +} diff --git a/contracts/vendor/openzeppelin/ReentrancyGuardUpgradeable.sol b/contracts/vendor/openzeppelin/ReentrancyGuardUpgradeable.sol new file mode 100644 index 0000000..cabccce --- /dev/null +++ b/contracts/vendor/openzeppelin/ReentrancyGuardUpgradeable.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @dev Compatibility shim for OZ 4.x-style upgradeable reentrancy protection. + * + * The repo still initializes `__ReentrancyGuard_init()` in several UUPS contracts, + * while the installed OZ 5.x upgradeable package no longer ships this module. + * Keeping the old initialization shape here lets both Foundry and Hardhat compile + * against the same upgrade-safe storage layout expectations. + */ +abstract contract ReentrancyGuardUpgradeable is Initializable { + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + function __ReentrancyGuard_init() internal onlyInitializing { + __ReentrancyGuard_init_unchained(); + } + + function __ReentrancyGuard_init_unchained() internal onlyInitializing { + _status = _NOT_ENTERED; + } + + modifier nonReentrant() { + _nonReentrantBefore(); + _; + _nonReentrantAfter(); + } + + function _nonReentrantBefore() private { + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + _status = _ENTERED; + } + + function _nonReentrantAfter() private { + _status = _NOT_ENTERED; + } + + function _reentrancyGuardEntered() internal view returns (bool) { + return _status == _ENTERED; + } + + uint256[49] private __gap; +} diff --git a/contracts/vendor/openzeppelin/UUPSUpgradeable.sol b/contracts/vendor/openzeppelin/UUPSUpgradeable.sol new file mode 100644 index 0000000..179d004 --- /dev/null +++ b/contracts/vendor/openzeppelin/UUPSUpgradeable.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable as OZUUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +/** + * @dev Compatibility shim for OZ 4.x-style upgradeable UUPS initialization. + * + * The installed OZ 5.x upgradeable package re-exports the non-upgradeable UUPS + * contract and no longer includes `__UUPSUpgradeable_init()`. Several existing + * contracts in this repo still call that hook during initialization, so we keep + * a local wrapper that restores the expected no-op initializer surface. + */ +abstract contract UUPSUpgradeable is Initializable, OZUUPSUpgradeable { + function __UUPSUpgradeable_init() internal onlyInitializing {} + + function __UUPSUpgradeable_init_unchained() internal onlyInitializing {} +} diff --git a/test/compliance/CompliantFiatTokenV2.t.sol b/test/compliance/CompliantFiatTokenV2.t.sol new file mode 100644 index 0000000..95a4807 --- /dev/null +++ b/test/compliance/CompliantFiatTokenV2.t.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {CompliantFiatTokenV2} from "../../contracts/tokens/CompliantFiatTokenV2.sol"; + +contract CompliantFiatTokenV2Test is Test { + using ECDSA for bytes32; + + bytes32 private constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 private constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = + keccak256( + "TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" + ); + + event CompliantOperationDeclared( + bytes32 indexed operationType, + address indexed initiator, + address indexed authorizer, + address executor, + address from, + address to, + uint256 value, + bytes32 reasonHash, + bytes32 accountingRef, + bytes32 messageCorrelationId, + bytes32 legalReferenceHash + ); + + CompliantFiatTokenV2 internal token; + + uint256 internal ownerPk; + uint256 internal adminPk; + address internal owner; + address internal admin; + address internal spender; + address internal recipient; + address internal burner; + address internal governanceExecutor; + + function setUp() public { + ownerPk = 0xA11CE; + adminPk = 0xB0B; + owner = vm.addr(ownerPk); + admin = vm.addr(adminPk); + spender = address(0xCAFE); + recipient = address(0xBEEF); + burner = address(0xD00D); + governanceExecutor = address(0x7777); + + token = new CompliantFiatTokenV2( + "Euro Coin (Compliant V2)", + "cEURC", + 6, + "EUR", + "2", + owner, + admin, + 1_000_000 * 10 ** 6, + true + ); + + bytes32 burnerRole = token.BURNER_ROLE(); + vm.prank(admin); + token.grantRole(burnerRole, burner); + vm.prank(admin); + token.setGovernanceController(governanceExecutor); + } + + function testPermitWorks() public { + uint256 value = 25_000 * 10 ** 6; + uint256 deadline = block.timestamp + 1 days; + + bytes32 structHash = keccak256( + abi.encode(PERMIT_TYPEHASH, owner, spender, value, token.nonces(owner), deadline) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, digest); + + token.permit(owner, spender, value, deadline, v, r, s); + + assertEq(token.allowance(owner, spender), value); + assertEq(token.nonces(owner), 1); + } + + function testTransferWithAuthorizationWorks() public { + uint256 value = 5_000 * 10 ** 6; + uint256 validAfter = block.timestamp - 1; + uint256 validBefore = block.timestamp + 1 days; + bytes32 nonce = keccak256("auth-1"); + + bytes32 structHash = keccak256( + abi.encode( + TRANSFER_WITH_AUTHORIZATION_TYPEHASH, + owner, + recipient, + value, + validAfter, + validBefore, + nonce + ) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, digest); + + vm.prank(spender); + token.transferWithAuthorization(owner, recipient, value, validAfter, validBefore, nonce, v, r, s); + + assertEq(token.balanceOf(recipient), value); + assertEq(token.balanceOf(owner), 1_000_000 * 10 ** 6 - value); + assertTrue(token.authorizationState(owner, nonce)); + } + + function testMintAndBurnReasonHashesEmitStructuredEvents() public { + bytes32 mintReason = keccak256("mint-reason"); + bytes32 burnReason = keccak256("burn-reason"); + uint256 mintAmount = 10_000 * 10 ** 6; + + vm.expectEmit(true, true, true, false); + emit CompliantOperationDeclared( + token.OPERATION_MINT(), + admin, + admin, + admin, + address(0), + recipient, + mintAmount, + mintReason, + bytes32(0), + bytes32(0), + bytes32(0) + ); + + vm.prank(admin); + token.mint(recipient, mintAmount, mintReason); + + assertEq(token.balanceOf(recipient), mintAmount); + + vm.expectEmit(true, true, true, false); + emit CompliantOperationDeclared( + token.OPERATION_BURN(), + recipient, + recipient, + burner, + recipient, + address(0), + mintAmount / 2, + burnReason, + bytes32(0), + bytes32(0), + bytes32(0) + ); + + vm.prank(burner); + token.burn(recipient, mintAmount / 2, burnReason); + + assertEq(token.balanceOf(recipient), mintAmount / 2); + } + + function testPauseIsRoleGated() public { + vm.prank(spender); + vm.expectRevert(); + token.pause(); + + vm.prank(admin); + token.pause(); + assertTrue(token.paused()); + } + + function testSupplyControlsEnforceCap() public { + uint256 configuredCap = token.totalSupply() + (1_000 * 10 ** 6); + + vm.prank(admin); + token.setSupplyControls(configuredCap, 0, 0); + + vm.prank(admin); + token.mint(recipient, 1_000 * 10 ** 6, keccak256("under-cap")); + + uint256 attemptedTotalSupply = token.totalSupply() + 1; + vm.prank(admin); + vm.expectRevert( + abi.encodeWithSelector( + CompliantFiatTokenV2.SupplyCapExceeded.selector, + configuredCap, + attemptedTotalSupply + ) + ); + token.mint(recipient, 1, keccak256("over-cap")); + } + + function testLegacyAliasAndForwardCanonicalMetadata() public { + vm.prank(admin); + vm.expectRevert(); + token.setForwardCanonical(false); + + vm.startPrank(governanceExecutor); + token.addLegacyAlias("cEURC-legacy"); + token.setForwardCanonical(false); + token.setSymbolDisplay("cEURC"); + vm.stopPrank(); + + string[] memory aliases = token.legacyAliases(); + assertEq(aliases.length, 1); + assertEq(aliases[0], "cEURC-legacy"); + assertFalse(token.forwardCanonical()); + assertEq(token.symbolDisplay(), "cEURC"); + } + + function testGovernanceAndSupervisionMetadataCanBeManaged() public { + bytes32 governanceProfileId = keccak256("gov-profile"); + bytes32 supervisionProfileId = keccak256("supervision-profile"); + bytes32 storageNamespace = keccak256("storage-namespace"); + + vm.prank(admin); + vm.expectRevert(); + token.setGovernanceProfileId(governanceProfileId); + + vm.startPrank(governanceExecutor); + token.setGovernanceProfileId(governanceProfileId); + token.setSupervisionProfileId(supervisionProfileId); + token.setStorageNamespace(storageNamespace); + token.setPrimaryJurisdiction("EU"); + token.setRegulatoryDisclosureURI("ipfs://disclosure"); + token.setReportingURI("ipfs://reporting"); + token.setCanonicalUnderlyingAsset(address(0x1234)); + token.setSupervisionConfiguration(true, true, 14 days); + vm.stopPrank(); + + assertEq(token.governanceProfileId(), governanceProfileId); + assertEq(token.supervisionProfileId(), supervisionProfileId); + assertEq(token.storageNamespace(), storageNamespace); + assertEq(token.primaryJurisdiction(), "EU"); + assertEq(token.regulatoryDisclosureURI(), "ipfs://disclosure"); + assertEq(token.reportingURI(), "ipfs://reporting"); + assertEq(token.canonicalUnderlyingAsset(), address(0x1234)); + assertTrue(token.supervisionRequired()); + assertTrue(token.governmentApprovalRequired()); + assertEq(token.minimumUpgradeNoticePeriod(), 14 days); + assertFalse(token.wrappedTransport()); + } + + function testEmergencyMetadataOverridesRemainAvailableOutsideGovernance() public { + vm.prank(admin); + token.emergencySetPresentationMetadata(true, "ipfs://emergency-token", "cEURC-emergency"); + + vm.prank(admin); + token.emergencySetGovernanceMetadata( + keccak256("emergency-gov"), + keccak256("emergency-sup"), + keccak256("emergency-storage"), + "Emergency Jurisdiction", + address(0x9999), + true, + true, + 30 days + ); + + vm.prank(admin); + token.emergencySetDisclosureMetadata("ipfs://emergency-disclosure", "ipfs://emergency-reporting"); + + assertTrue(token.forwardCanonical()); + assertEq(token.tokenURI(), "ipfs://emergency-token"); + assertEq(token.symbolDisplay(), "cEURC-emergency"); + assertEq(token.primaryJurisdiction(), "Emergency Jurisdiction"); + assertEq(token.canonicalUnderlyingAsset(), address(0x9999)); + assertEq(token.regulatoryDisclosureURI(), "ipfs://emergency-disclosure"); + assertEq(token.reportingURI(), "ipfs://emergency-reporting"); + assertEq(token.minimumUpgradeNoticePeriod(), 30 days); + } +} diff --git a/test/compliance/CompliantWrappedTokenTest.t.sol b/test/compliance/CompliantWrappedTokenTest.t.sol index 50a4810..530e287 100644 --- a/test/compliance/CompliantWrappedTokenTest.t.sol +++ b/test/compliance/CompliantWrappedTokenTest.t.sol @@ -9,6 +9,7 @@ contract CompliantWrappedTokenTest is Test { address public admin; address public bridge; address public user1; + address public governanceExecutor; bytes32 constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 constant BURNER_ROLE = keccak256("BURNER_ROLE"); @@ -17,9 +18,11 @@ contract CompliantWrappedTokenTest is Test { admin = address(this); bridge = address(0xb); user1 = address(0x1); + governanceExecutor = address(0x7777); token = new CompliantWrappedToken("Wrapped cUSDT", "cWUSDT", 6, admin); token.grantRole(MINTER_ROLE, bridge); token.grantRole(BURNER_ROLE, bridge); + token.setGovernanceController(governanceExecutor); } function testDecimals() public view { @@ -61,4 +64,74 @@ contract CompliantWrappedTokenTest is Test { vm.expectRevert(); token.burnFrom(user1, 400e6); } + + function testFreezeOperationalRolesBlocksFutureMinterChanges() public { + token.freezeOperationalRoles(); + + vm.expectRevert(); + token.grantRole(MINTER_ROLE, address(0xcafe)); + } + + function testFreezeOperationalRolesBlocksFutureBurnerRevocations() public { + token.freezeOperationalRoles(); + + vm.expectRevert(); + token.revokeRole(BURNER_ROLE, bridge); + } + + function testFreezeOperationalRolesDoesNotBreakExistingBridgePermissions() public { + token.freezeOperationalRoles(); + + vm.prank(bridge); + token.mint(user1, 250e6); + vm.prank(bridge); + token.burnFrom(user1, 100e6); + + assertEq(token.balanceOf(user1), 150e6); + } + + function testGovernanceAndSupervisionMetadataCanBeManaged() public { + vm.expectRevert(); + token.setGovernanceProfileId(keccak256("cw-gov")); + + vm.startPrank(governanceExecutor); + token.setGovernanceProfileId(keccak256("cw-gov")); + token.setSupervisionProfileId(keccak256("cw-sup")); + token.setStorageNamespace(keccak256("cw-storage")); + token.setPrimaryJurisdiction("Avalanche"); + token.setRegulatoryDisclosureURI("ipfs://cw-disclosure"); + token.setReportingURI("ipfs://cw-reporting"); + token.setCanonicalUnderlyingAsset(address(0x1234)); + token.setSupervisionConfiguration(true, true, 21 days); + vm.stopPrank(); + + assertEq(token.primaryJurisdiction(), "Avalanche"); + assertEq(token.regulatoryDisclosureURI(), "ipfs://cw-disclosure"); + assertEq(token.reportingURI(), "ipfs://cw-reporting"); + assertEq(token.canonicalUnderlyingAsset(), address(0x1234)); + assertTrue(token.supervisionRequired()); + assertTrue(token.governmentApprovalRequired()); + assertEq(token.minimumUpgradeNoticePeriod(), 21 days); + assertTrue(token.wrappedTransport()); + } + + function testEmergencyMetadataOverridesRemainAvailableOutsideGovernance() public { + token.emergencySetGovernanceMetadata( + keccak256("emergency-cw-gov"), + keccak256("emergency-cw-sup"), + keccak256("emergency-cw-storage"), + "Emergency Wrapped Jurisdiction", + address(0x5678), + true, + true, + 30 days + ); + token.emergencySetDisclosureMetadata("ipfs://cw-emergency-disclosure", "ipfs://cw-emergency-reporting"); + + assertEq(token.primaryJurisdiction(), "Emergency Wrapped Jurisdiction"); + assertEq(token.canonicalUnderlyingAsset(), address(0x5678)); + assertEq(token.regulatoryDisclosureURI(), "ipfs://cw-emergency-disclosure"); + assertEq(token.reportingURI(), "ipfs://cw-emergency-reporting"); + assertEq(token.minimumUpgradeNoticePeriod(), 30 days); + } } diff --git a/test/integration/JurisdictionalGovernance.t.sol b/test/integration/JurisdictionalGovernance.t.sol new file mode 100644 index 0000000..3d16f91 --- /dev/null +++ b/test/integration/JurisdictionalGovernance.t.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../../contracts/registry/UniversalAssetRegistry.sol"; +import "../../contracts/governance/GovernanceController.sol"; +import "../../contracts/tokens/CompliantFiatTokenV2.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract JurisdictionalGovernanceTest is Test { + UniversalAssetRegistry internal registry; + GovernanceController internal governance; + CompliantFiatTokenV2 internal token; + + address internal admin; + address internal authority; + + function setUp() public { + admin = makeAddr("admin"); + authority = makeAddr("authority"); + + vm.startPrank(admin); + + UniversalAssetRegistry registryImpl = new UniversalAssetRegistry(); + bytes memory registryInit = abi.encodeCall(UniversalAssetRegistry.initialize, (admin)); + ERC1967Proxy registryProxy = new ERC1967Proxy(address(registryImpl), registryInit); + registry = UniversalAssetRegistry(address(registryProxy)); + + GovernanceController governanceImpl = new GovernanceController(); + bytes memory governanceInit = abi.encodeCall(GovernanceController.initialize, (address(registry), admin)); + ERC1967Proxy governanceProxy = new ERC1967Proxy(address(governanceImpl), governanceInit); + governance = GovernanceController(address(governanceProxy)); + + token = new CompliantFiatTokenV2( + "Euro Coin (Compliant V2)", + "cEURC", + 6, + "EUR", + "2", + admin, + admin, + 0, + true + ); + + registry.setGovernanceController(address(governance)); + token.setGovernanceController(address(governance)); + token.emergencySetGovernanceMetadata( + token.governanceProfileId(), + token.supervisionProfileId(), + token.storageNamespace(), + "EU", + address(0), + true, + true, + 14 days + ); + registry.emergencySetJurisdictionProfile( + "EU", + true, + true, + true, + 14 days, + "ipfs://eu-supervision", + keccak256("eu-policy") + ); + registry.emergencySetJurisdictionAuthority("EU", authority, true, true, true, true, true); + registry.addValidator(authority); + registry.registerGRUCompliantAsset(address(token), "Euro Coin (Compliant V2)", "cEURC", 6, "EU"); + + vm.stopPrank(); + } + + function testRegistryPullsGovernanceMetadataFromTokenAndJurisdiction() public view { + UniversalAssetRegistry.UniversalAsset memory asset = registry.getAsset(address(token)); + + assertEq(asset.jurisdiction, "EU"); + assertEq(asset.assetId, token.assetId()); + assertEq(asset.assetVersionId, token.assetVersionId()); + assertEq(asset.governanceProfileId, token.governanceProfileId()); + assertEq(asset.supervisionProfileId, token.supervisionProfileId()); + assertEq(asset.storageNamespace, token.storageNamespace()); + assertTrue(asset.supervisionRequired); + assertTrue(asset.governmentApprovalRequired); + assertEq(asset.minimumUpgradeNoticePeriod, 14 days); + } + + function testQueueRequiresJurisdictionApprovalAndUsesJurisdictionNoticePeriod() public { + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + bytes32 updatedGovernanceProfile = keccak256("eu-governance-v2"); + + targets[0] = address(token); + values[0] = 0; + calldatas[0] = abi.encodeWithSelector( + CompliantFiatTokenV2.setGovernanceProfileId.selector, + updatedGovernanceProfile + ); + + vm.prank(admin); + uint256 proposalId = governance.proposeForAsset( + address(token), + targets, + values, + calldatas, + "Update EU governance metadata", + GovernanceController.GovernanceMode.TimelockShort + ); + assertEq(governance.proposalAssets(proposalId), address(token)); + + vm.roll(block.number + governance.votingDelay() + 1); + + vm.prank(authority); + governance.castVote(proposalId, 1); + + vm.roll(block.number + governance.votingPeriod() + 1); + + vm.expectRevert("Jurisdiction approval required"); + governance.queue(proposalId); + + vm.prank(authority); + governance.approveProposalJurisdiction(proposalId); + + uint256 beforeQueue = block.timestamp; + governance.queue(proposalId); + + uint256 eta = governance.getProposalEta(proposalId); + assertGe(eta, beforeQueue + 14 days); + assertEq(governance.proposalJurisdictionApprovalCount(proposalId), 1); + assertEq(governance.proposalLastJurisdictionApprover(proposalId), authority); + + vm.warp(eta); + governance.execute(proposalId); + + assertEq(token.governanceProfileId(), updatedGovernanceProfile); + } + + function testAssetScopedProposalRejectsRegistryCallsForAnotherAsset() public { + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + + targets[0] = address(registry); + values[0] = 0; + calldatas[0] = abi.encodeWithSelector( + UniversalAssetRegistry.syncAssetMetadataFromToken.selector, + address(0xBEEF) + ); + + vm.prank(admin); + vm.expectRevert("Registry asset mismatch"); + governance.proposeForAsset( + address(token), + targets, + values, + calldatas, + "Attempt mismatched registry sync", + GovernanceController.GovernanceMode.TimelockShort + ); + } + + function testRegistryMetadataRequiresGovernanceButEmergencyPathRemainsAvailable() public { + vm.prank(admin); + vm.expectRevert(); + registry.setAssetGovernanceProfile( + address(token), + keccak256("blocked-gov-profile"), + keccak256("blocked-sup-profile"), + keccak256("blocked-storage"), + address(0x1234), + false, + true, + true, + 21 days, + "ipfs://blocked-disclosure", + "ipfs://blocked-reporting" + ); + + vm.prank(admin); + registry.emergencySetAssetGovernanceProfile( + address(token), + keccak256("manual-gov-profile"), + keccak256("manual-sup-profile"), + keccak256("manual-storage"), + address(0x1234), + false, + true, + true, + 21 days, + "ipfs://manual-disclosure", + "ipfs://manual-reporting" + ); + + UniversalAssetRegistry.UniversalAsset memory asset = registry.getAsset(address(token)); + assertEq(asset.governanceProfileId, keccak256("manual-gov-profile")); + assertEq(asset.supervisionProfileId, keccak256("manual-sup-profile")); + assertEq(asset.storageNamespace, keccak256("manual-storage")); + assertEq(asset.canonicalUnderlyingAsset, address(0x1234)); + assertEq(asset.regulatoryDisclosureURI, "ipfs://manual-disclosure"); + assertEq(asset.reportingURI, "ipfs://manual-reporting"); + assertEq(asset.minimumUpgradeNoticePeriod, 21 days); + } +}