// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {IPairAssetRegistry} from "./interfaces/IPairAssetRegistry.sol"; import {OwnedTwoStep} from "./utils/OwnedTwoStep.sol"; /// @title STANK Pair Asset Registry /// @notice Admin-curated allowlist of bStocks and BNB Chain assets eligible for new launches. /// @dev Deactivation prevents new launches but never alters or disables an existing pool. contract PairAssetRegistry is IPairAssetRegistry, OwnedTwoStep { enum AssetClass { Other, BStock, BnbToken } struct PairAsset { bool listed; bool active; AssetClass assetClass; string label; string symbol; } struct PairAssetInput { address asset; AssetClass assetClass; string label; string symbol; } mapping(address asset => PairAsset details) private _pairAssets; address[] private _assetList; error InvalidAsset(address asset); error AssetAlreadyListed(address asset); error AssetNotListed(address asset); event PairAssetAdded( address indexed asset, AssetClass indexed assetClass, string label, string symbol ); event PairAssetStatusChanged(address indexed asset, bool active); event PairAssetMetadataChanged( address indexed asset, AssetClass indexed assetClass, string label, string symbol ); constructor(address initialOwner) OwnedTwoStep(initialOwner) {} /// @notice Adds a contract address as an active pairing option. function addPairAsset(address asset, AssetClass assetClass, string calldata label, string calldata symbol) external onlyOwner { _addPairAsset(asset, assetClass, label, symbol); } /// @notice Adds multiple active pairing options in one administration transaction. function addPairAssets(PairAssetInput[] calldata inputs) external onlyOwner { uint256 length = inputs.length; for (uint256 i; i < length; ++i) { PairAssetInput calldata input = inputs[i]; _addPairAsset(input.asset, input.assetClass, input.label, input.symbol); } } /// @notice Activates or deactivates an existing pairing option. /// @dev Use `active = false` as the non-destructive remove operation in an admin UI. function setPairAssetActive(address asset, bool active) external onlyOwner { PairAsset storage details = _pairAssets[asset]; if (!details.listed) revert AssetNotListed(asset); if (details.active == active) return; details.active = active; emit PairAssetStatusChanged(asset, active); } /// @notice Updates display metadata without changing the asset address or active state. function updatePairAssetMetadata( address asset, AssetClass assetClass, string calldata label, string calldata symbol ) external onlyOwner { PairAsset storage details = _pairAssets[asset]; if (!details.listed) revert AssetNotListed(asset); details.assetClass = assetClass; details.label = label; details.symbol = symbol; emit PairAssetMetadataChanged(asset, assetClass, label, symbol); } /// @notice Returns whether an asset may be used for a new launch. function isPairAssetActive(address asset) external view returns (bool) { return _pairAssets[asset].active; } /// @notice Returns registry metadata for an asset. function pairAsset(address asset) external view returns (PairAsset memory) { return _pairAssets[asset]; } /// @notice Number of assets ever added. Deactivated entries remain enumerable. function pairAssetCount() external view returns (uint256) { return _assetList.length; } /// @notice Returns the asset address stored at an enumeration index. function pairAssetAt(uint256 index) external view returns (address) { return _assetList[index]; } function _addPairAsset(address asset, AssetClass assetClass, string calldata label, string calldata symbol) private { if (asset == address(0) || asset.code.length == 0) revert InvalidAsset(asset); if (_pairAssets[asset].listed) revert AssetAlreadyListed(asset); _pairAssets[asset] = PairAsset({ listed: true, active: true, assetClass: assetClass, label: label, symbol: symbol }); _assetList.push(asset); emit PairAssetAdded(asset, assetClass, label, symbol); } }