// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "../vendor/openzeppelin/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "../registry/UniversalAssetRegistry.sol"; /** * @title TokenlistGovernanceSync * @notice Automatically syncs tokenlist.json changes to on-chain governance * @dev Monitors tokenlist versions and creates proposals for changes */ contract TokenlistGovernanceSync is Initializable, AccessControlUpgradeable, UUPSUpgradeable { bytes32 public constant TOKENLIST_MANAGER_ROLE = keccak256("TOKENLIST_MANAGER_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); struct TokenlistVersion { uint256 major; uint256 minor; uint256 patch; string ipfsHash; uint256 timestamp; bool synced; } struct AssetMetadata { address tokenAddress; UniversalAssetRegistry.AssetType assetType; UniversalAssetRegistry.ComplianceLevel complianceLevel; string name; string symbol; uint8 decimals; string jurisdiction; uint8 volatilityScore; uint256 minBridgeAmount; uint256 maxBridgeAmount; } struct TokenChange { address tokenAddress; ChangeType changeType; AssetMetadata metadata; } enum ChangeType { Added, Removed, Modified } // Storage UniversalAssetRegistry public assetRegistry; mapping(bytes32 => TokenlistVersion) public versions; bytes32 public currentVersion; bytes32[] public versionHistory; // Events event TokenlistUpdated( bytes32 indexed versionHash, uint256 major, uint256 minor, uint256 patch, string ipfsHash ); event AutoProposalCreated( bytes32 indexed proposalId, address indexed token, ChangeType changeType ); event VersionSynced(bytes32 indexed versionHash); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize( address _assetRegistry, address admin ) external initializer { __AccessControl_init(); __UUPSUpgradeable_init(); require(_assetRegistry != address(0), "Zero registry"); assetRegistry = UniversalAssetRegistry(_assetRegistry); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(TOKENLIST_MANAGER_ROLE, admin); _grantRole(UPGRADER_ROLE, admin); } function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} /** * @notice Submit new tokenlist version and auto-create proposals */ function submitTokenlistVersion( uint256 major, uint256 minor, uint256 patch, string calldata ipfsHash, address[] calldata newTokens, AssetMetadata[] calldata metadata ) external onlyRole(TOKENLIST_MANAGER_ROLE) returns (bytes32[] memory proposalIds) { require(newTokens.length == metadata.length, "Length mismatch"); bytes32 versionHash = keccak256(abi.encode(major, minor, patch)); // Store version TokenlistVersion storage version = versions[versionHash]; version.major = major; version.minor = minor; version.patch = patch; version.ipfsHash = ipfsHash; version.timestamp = block.timestamp; version.synced = false; versionHistory.push(versionHash); currentVersion = versionHash; emit TokenlistUpdated(versionHash, major, minor, patch, ipfsHash); // Auto-create proposals for new tokens proposalIds = new bytes32[](newTokens.length); for (uint256 i = 0; i < newTokens.length; i++) { proposalIds[i] = _createAssetProposal(newTokens[i], metadata[i]); emit AutoProposalCreated( proposalIds[i], newTokens[i], ChangeType.Added ); } return proposalIds; } /** * @notice Create asset proposal in registry */ function _createAssetProposal( address token, AssetMetadata memory metadata ) internal returns (bytes32) { return assetRegistry.proposeAsset( token, metadata.assetType, metadata.complianceLevel, metadata.name, metadata.symbol, metadata.decimals, metadata.jurisdiction, metadata.volatilityScore, metadata.minBridgeAmount, metadata.maxBridgeAmount ); } /** * @notice Detect changes between versions */ function detectChanges( bytes32 oldVersionHash, bytes32 newVersionHash, address[] calldata oldTokens, address[] calldata newTokens ) external pure returns (TokenChange[] memory changes) { // Simple diff: tokens in new but not old = added // tokens in old but not new = removed uint256 maxChanges = oldTokens.length + newTokens.length; TokenChange[] memory tempChanges = new TokenChange[](maxChanges); uint256 changeCount = 0; // Find added tokens for (uint256 i = 0; i < newTokens.length; i++) { bool found = false; for (uint256 j = 0; j < oldTokens.length; j++) { if (newTokens[i] == oldTokens[j]) { found = true; break; } } if (!found) { tempChanges[changeCount].tokenAddress = newTokens[i]; tempChanges[changeCount].changeType = ChangeType.Added; changeCount++; } } // Find removed tokens for (uint256 i = 0; i < oldTokens.length; i++) { bool found = false; for (uint256 j = 0; j < newTokens.length; j++) { if (oldTokens[i] == newTokens[j]) { found = true; break; } } if (!found) { tempChanges[changeCount].tokenAddress = oldTokens[i]; tempChanges[changeCount].changeType = ChangeType.Removed; changeCount++; } } // Resize array to actual size changes = new TokenChange[](changeCount); for (uint256 i = 0; i < changeCount; i++) { changes[i] = tempChanges[i]; } return changes; } /** * @notice Mark version as synced */ function markVersionSynced(bytes32 versionHash) external onlyRole(TOKENLIST_MANAGER_ROLE) { require(versions[versionHash].timestamp > 0, "Version not found"); versions[versionHash].synced = true; emit VersionSynced(versionHash); } /** * @notice Batch create proposals for multiple tokens */ function batchCreateProposals( address[] calldata tokens, AssetMetadata[] calldata metadata ) external onlyRole(TOKENLIST_MANAGER_ROLE) returns (bytes32[] memory proposalIds) { require(tokens.length == metadata.length, "Length mismatch"); proposalIds = new bytes32[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { proposalIds[i] = _createAssetProposal(tokens[i], metadata[i]); emit AutoProposalCreated( proposalIds[i], tokens[i], ChangeType.Added ); } return proposalIds; } // View functions function getVersion(bytes32 versionHash) external view returns (TokenlistVersion memory) { return versions[versionHash]; } function getCurrentVersion() external view returns (TokenlistVersion memory) { return versions[currentVersion]; } function getVersionHistory() external view returns (bytes32[] memory) { return versionHistory; } function isVersionSynced(bytes32 versionHash) external view returns (bool) { return versions[versionHash].synced; } function getVersionCount() external view returns (uint256) { return versionHistory.length; } }