Enforce asset-scoped governance metadata controls
This commit is contained in:
150
contracts/compliance/LegallyCompliantBaseV2.sol
Normal file
150
contracts/compliance/LegallyCompliantBaseV2.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
22
contracts/interfaces/IRegulatedAssetMetadata.sol
Normal file
22
contracts/interfaces/IRegulatedAssetMetadata.sol
Normal 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);
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
751
contracts/tokens/CompliantFiatTokenV2.sol
Normal file
751
contracts/tokens/CompliantFiatTokenV2.sol
Normal 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_
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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_
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
89
contracts/tokens/interfaces/ICompliantFiatTokenV2.sol
Normal file
89
contracts/tokens/interfaces/ICompliantFiatTokenV2.sol
Normal 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;
|
||||
}
|
||||
48
contracts/vendor/openzeppelin/ReentrancyGuardUpgradeable.sol
vendored
Normal file
48
contracts/vendor/openzeppelin/ReentrancyGuardUpgradeable.sol
vendored
Normal 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;
|
||||
}
|
||||
19
contracts/vendor/openzeppelin/UUPSUpgradeable.sol
vendored
Normal file
19
contracts/vendor/openzeppelin/UUPSUpgradeable.sol
vendored
Normal 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 {}
|
||||
}
|
||||
276
test/compliance/CompliantFiatTokenV2.t.sol
Normal file
276
test/compliance/CompliantFiatTokenV2.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
204
test/integration/JurisdictionalGovernance.t.sol
Normal file
204
test/integration/JurisdictionalGovernance.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user