Enforce asset-scoped governance metadata controls

This commit is contained in:
defiQUG
2026-04-01 01:59:10 -07:00
parent 162a7d5b03
commit 4a641475cd
12 changed files with 2497 additions and 6 deletions

View File

@@ -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);
}
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}

View File

@@ -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)));
}
}

View File

@@ -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_
);
}
}

View File

@@ -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_
);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 {}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}