diff --git a/config/hybx-omnl-cross-chain-lines.json b/config/hybx-omnl-cross-chain-lines.json index 10d0d1f..05c6333 100644 --- a/config/hybx-omnl-cross-chain-lines.json +++ b/config/hybx-omnl-cross-chain-lines.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "updated": "2026-06-17", + "updated": "2026-06-20", "description": "OMNL office settlement lines \u2014 treasury EVM wallets + canonical M1 token anchors per chain.", "lines": [ { @@ -20,7 +20,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x63246fbf065dc14fcd18890be3b3274a90faebb4defa602cfeaaf56254d5c3eb", @@ -39,7 +41,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xf3b667aa86df7021f12238da02013ea06691654a19e14820a8e1f0810ea8584b", @@ -58,7 +62,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x329d143e0dd4209e5dadc159dc39a1b2121f9f0e1daa7f6d4ef7fdd685017e86", @@ -77,7 +83,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x00709b1e16c8ea744c115bbcf28c588bceefe2ee79b8ccf3ec723bafee375b0a", @@ -96,7 +104,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x98179d4cf92f0703702f0f98eda788932cb5d3bf823df275b6ca826d40df941a", @@ -115,7 +125,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x7e796548c9367f06628f65d7cbb8dbb104c08b0559e3079d3e4ba2868aab1a02", @@ -134,7 +146,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x78287281703fd64c3bd014a5eb61130324075ea91e286def362ba3cbb06237ac", @@ -153,7 +167,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xafe717d3727d08ef51307b96d4c090f69cd3f8b32622c6d6a9068d16b13da4f6", @@ -172,7 +188,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x7eee9b83385d0220cac4fdae36222ed0364d445baf60e8a1fbd24d8100819e7e", @@ -191,7 +209,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x751d9a27b003b5b8ec98f086003238f1ac16f1db5083817f1d8e8729bd3dbda8", @@ -210,7 +230,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x2065d9d64806c8946d4d59e78cd709cadb0275f2962bc986e9bdd76d5b2bdde3", @@ -229,7 +251,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x981d13dd8853a67cb3d865e9b77400c2e848a753be12c1cf693621288b2ba253", @@ -248,7 +272,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x4b492f8afa40d6191fe1319190345b32fdc1918b727de2b54f86f9e890834f05", @@ -267,7 +293,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xa86f3acebcecb46454e091fcf24a35f9efaa939421983f69563b53f55bb7ae94", @@ -286,7 +314,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xc236020f755fe76acaf5eb5fd8a53114d1f49e8e58d985beb4f4410d52abd2a6", @@ -305,7 +335,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xc820d6d2cf4b054b08435a1ea3318a88ffa849b51fc61ab12ef15b248423be96", @@ -324,7 +356,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xf3c45215b31095db55af3e8a65668f86de60f689c4fdeb8d54e89f3504752c43", @@ -343,7 +377,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xc96c3bb01849a9f3d8fd62f1114b5606a9235d53f771b424b299207952175f7d", @@ -362,7 +398,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x5d44d97f66dcb6591d36b70375b84c73b73cfc8f3788228b5977632beb995b1d", @@ -381,7 +419,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xa1fd57654dc5fef77117da02995c584a19a4b6949a03cd91ccb16551d7d3d01d", @@ -400,7 +440,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xd5666338b8da738cf82c681e4a39ae5f817861061accfd98464a30470bdfcc58", @@ -419,7 +461,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0x3d83b0f803d19de0cd096a4c29fc27c8ef02248d8e5978a3a2e16bbe714754dc", @@ -438,7 +482,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xaede6a63b3f49d0d18b13267855274e250516a97c84dcf0d31e17f359ceca4ff", @@ -457,7 +503,9 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" }, { "lineId": "0xcdd67c94cd265ef8dd383a06b7ac4733e83a8c7cdf5030aed9ec36ab2999004a", @@ -476,7 +524,10 @@ "tokenM0": "0x0000000000000000000000000000000000000000", "tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } - } + }, + "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8", + "inboundSettlementType": "gnosis-safe" } - ] + ], + "globalInboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8" } diff --git a/contracts/mainnet-market/CWTokenMarketRegistry.sol b/contracts/mainnet-market/CWTokenMarketRegistry.sol new file mode 100644 index 0000000..31e3eee --- /dev/null +++ b/contracts/mainnet-market/CWTokenMarketRegistry.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20Supply} from "./interfaces/IERC20Supply.sol"; +import {ICWTokenUsdFeedView, IUniV3PoolTokens} from "./interfaces/IMarketAnchor.sol"; + +/** + * @title CWTokenMarketRegistry + * @notice On-chain market anchor for mainnet cW* — face USD, published TWAP, x402 settlement mirror, + * and Chainlink-compatible per-token feeds for indexers that ingest custom oracles. + * @dev Does not unilaterally control Etherscan UI; pairs with UniV3 liquidity + operator sync. + */ +contract CWTokenMarketRegistry { + uint8 public constant PRICE_DECIMALS = 8; + /// @dev Max deviation from faceValueUsdE8 on publishPrice (1500 = 15%) for USD-face cW* stables. + uint16 public constant MAX_PEG_DEVIATION_BPS = 1500; + + address public admin; + bool public paused; + + struct TokenConfig { + bool registered; + uint8 tokenDecimals; + uint64 faceValueUsdE8; + address uniV3Pool; + bool cwIsToken0; + uint64 publishedPriceUsdE8; + uint256 publishedAt; + uint80 roundId; + uint256 settlementCount; + } + + mapping(address => TokenConfig) public tokens; + mapping(address => address) public tokenFeed; + address[] public tokenList; + + event AdminChanged(address indexed newAdmin); + event Paused(); + event Unpaused(); + event TokenRegistered( + address indexed token, + uint64 faceValueUsdE8, + address indexed uniV3Pool, + address indexed feed + ); + event MarketPricePublished( + address indexed token, + uint80 roundId, + uint64 priceUsdE8, + uint256 timestamp, + string source + ); + event X402SettlementRecorded( + address indexed token, + address indexed payer, + address indexed payTo, + uint256 amountRaw, + uint64 faceUsdE8, + uint256 settlementChainId, + bytes32 settlementTxHash, + bytes32 resourceHash + ); + + modifier onlyAdmin() { + require(msg.sender == admin, "only admin"); + _; + } + + modifier whenNotPaused() { + require(!paused, "paused"); + _; + } + + constructor(address _admin) { + require(_admin != address(0), "zero admin"); + admin = _admin; + } + + function setAdmin(address newAdmin) external onlyAdmin { + require(newAdmin != address(0), "zero admin"); + admin = newAdmin; + emit AdminChanged(newAdmin); + } + + function pause() external onlyAdmin { + paused = true; + emit Paused(); + } + + function unpause() external onlyAdmin { + paused = false; + emit Unpaused(); + } + + function registerToken( + address token, + uint8 tokenDecimals, + uint64 faceValueUsdE8, + address uniV3Pool, + bool cwIsToken0, + address feed + ) external onlyAdmin whenNotPaused { + require(token != address(0), "zero token"); + require(faceValueUsdE8 > 0, "zero face"); + require(feed != address(0), "zero feed"); + require( + ICWTokenUsdFeedView(feed).registry() == address(this) + && ICWTokenUsdFeedView(feed).token() == token, + "feed mismatch" + ); + if (uniV3Pool != address(0)) { + address t0 = IUniV3PoolTokens(uniV3Pool).token0(); + address t1 = IUniV3PoolTokens(uniV3Pool).token1(); + require(t0 == token || t1 == token, "pool token"); + } + + if (tokens[token].registered) { + require(tokens[token].tokenDecimals == tokenDecimals, "decimals locked"); + } else { + tokenList.push(token); + } + + tokens[token] = TokenConfig({ + registered: true, + tokenDecimals: tokenDecimals, + faceValueUsdE8: faceValueUsdE8, + uniV3Pool: uniV3Pool, + cwIsToken0: cwIsToken0, + publishedPriceUsdE8: faceValueUsdE8, + publishedAt: block.timestamp, + roundId: 1, + settlementCount: tokens[token].settlementCount + }); + tokenFeed[token] = feed; + + emit TokenRegistered(token, faceValueUsdE8, uniV3Pool, feed); + emit MarketPricePublished(token, 1, faceValueUsdE8, block.timestamp, "face_bootstrap"); + } + + function publishPrice(address token, uint64 priceUsdE8, string calldata source) + external + onlyAdmin + whenNotPaused + { + TokenConfig storage t = _requireToken(token); + require(priceUsdE8 > 0, "zero price"); + _enforcePegBand(t.faceValueUsdE8, priceUsdE8); + t.roundId += 1; + t.publishedPriceUsdE8 = priceUsdE8; + t.publishedAt = block.timestamp; + emit MarketPricePublished(token, t.roundId, priceUsdE8, block.timestamp, source); + } + + function recordX402Settlement( + address token, + address payer, + address payTo, + uint256 amountRaw, + uint64 faceUsdE8, + uint256 settlementChainId, + bytes32 settlementTxHash, + bytes32 resourceHash + ) external onlyAdmin whenNotPaused { + TokenConfig storage t = _requireToken(token); + require(payer != address(0) && payTo != address(0), "zero addr"); + require(amountRaw > 0, "zero amount"); + t.settlementCount += 1; + emit X402SettlementRecorded( + token, + payer, + payTo, + amountRaw, + faceUsdE8, + settlementChainId, + settlementTxHash, + resourceHash + ); + } + + function latestPriceUsdE8(address token) external view returns (uint64) { + TokenConfig storage t = _requireToken(token); + return t.publishedPriceUsdE8 != 0 ? t.publishedPriceUsdE8 : t.faceValueUsdE8; + } + + /// @notice USD market cap with 8 decimals: priceUsdE8 * totalSupply / 10^tokenDecimals + function onchainMarketCapUsdE8(address token) external view returns (uint256) { + TokenConfig storage t = _requireToken(token); + uint64 price = t.publishedPriceUsdE8 != 0 ? t.publishedPriceUsdE8 : t.faceValueUsdE8; + uint256 supply = IERC20Supply(token).totalSupply(); + return (uint256(price) * supply) / (10 ** uint256(t.tokenDecimals)); + } + + function tokenCount() external view returns (uint256) { + return tokenList.length; + } + + function latestRoundDataForToken(address token) + external + view + returns (uint80 roundId, int256 answer, uint256 updatedAt) + { + TokenConfig storage t = _requireToken(token); + uint64 price = t.publishedPriceUsdE8 != 0 ? t.publishedPriceUsdE8 : t.faceValueUsdE8; + return (t.roundId, int256(uint256(price)), t.publishedAt); + } + + function _requireToken(address token) internal view returns (TokenConfig storage t) { + t = tokens[token]; + require(t.registered, "unregistered"); + } + + function _enforcePegBand(uint64 faceUsdE8, uint64 priceUsdE8) internal pure { + uint256 maxDev = (uint256(faceUsdE8) * MAX_PEG_DEVIATION_BPS) / 10_000; + require(priceUsdE8 >= faceUsdE8 - maxDev && priceUsdE8 <= faceUsdE8 + maxDev, "peg band"); + } +} diff --git a/contracts/mainnet-market/CWTokenUsdFeed.sol b/contracts/mainnet-market/CWTokenUsdFeed.sol new file mode 100644 index 0000000..cbf1cd1 --- /dev/null +++ b/contracts/mainnet-market/CWTokenUsdFeed.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IAggregatorV3} from "./interfaces/IAggregatorV3.sol"; +import {CWTokenMarketRegistry} from "./CWTokenMarketRegistry.sol"; + +/** + * @title CWTokenUsdFeed + * @notice Per-token Chainlink AggregatorV3-compatible USD feed backed by CWTokenMarketRegistry. + */ +contract CWTokenUsdFeed is IAggregatorV3 { + CWTokenMarketRegistry public immutable registry; + address public immutable token; + string public description_; + + constructor(CWTokenMarketRegistry _registry, address _token, string memory _description) { + require(address(_registry) != address(0) && _token != address(0), "zero arg"); + registry = _registry; + token = _token; + description_ = _description; + } + + function decimals() external pure returns (uint8) { + return 8; + } + + function description() external view returns (string memory) { + return description_; + } + + function version() external pure returns (uint256) { + return 1; + } + + function getRoundData(uint80 _roundId) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + (roundId, answer, startedAt, updatedAt, answeredInRound) = latestRoundData(); + if (_roundId != 0) { + require(roundId == _roundId, "stale round"); + } + } + + function latestRoundData() + public + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + (roundId, answer, updatedAt) = registry.latestRoundDataForToken(token); + startedAt = updatedAt; + answeredInRound = roundId; + } +} diff --git a/contracts/mainnet-market/interfaces/IAggregatorV3.sol b/contracts/mainnet-market/interfaces/IAggregatorV3.sol new file mode 100644 index 0000000..4be0030 --- /dev/null +++ b/contracts/mainnet-market/interfaces/IAggregatorV3.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @notice Chainlink AggregatorV3-compatible surface for explorer / DeFi integrations. +interface IAggregatorV3 { + function decimals() external view returns (uint8); + function description() external view returns (string memory); + function version() external view returns (uint256); + + function getRoundData(uint80 _roundId) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} diff --git a/contracts/mainnet-market/interfaces/IERC20Supply.sol b/contracts/mainnet-market/interfaces/IERC20Supply.sol new file mode 100644 index 0000000..cf7d57e --- /dev/null +++ b/contracts/mainnet-market/interfaces/IERC20Supply.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IERC20Supply { + function totalSupply() external view returns (uint256); + function decimals() external view returns (uint8); +} diff --git a/contracts/mainnet-market/interfaces/IMarketAnchor.sol b/contracts/mainnet-market/interfaces/IMarketAnchor.sol new file mode 100644 index 0000000..55284fd --- /dev/null +++ b/contracts/mainnet-market/interfaces/IMarketAnchor.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface ICWTokenUsdFeedView { + function token() external view returns (address); + function registry() external view returns (address); +} + +interface IUniV3PoolTokens { + function token0() external view returns (address); + function token1() external view returns (address); +} diff --git a/script/mainnet-market/DeployCWTokenMarketRegistry.s.sol b/script/mainnet-market/DeployCWTokenMarketRegistry.s.sol new file mode 100644 index 0000000..37d9595 --- /dev/null +++ b/script/mainnet-market/DeployCWTokenMarketRegistry.s.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console2} from "forge-std/Script.sol"; +import {CWTokenMarketRegistry} from "../../contracts/mainnet-market/CWTokenMarketRegistry.sol"; +import {CWTokenUsdFeed} from "../../contracts/mainnet-market/CWTokenUsdFeed.sol"; + +contract DeployCWTokenMarketRegistry is Script { + struct TokenSeed { + address token; + uint8 decimals; + uint64 faceUsdE8; + address uniV3Pool; + bool cwIsToken0; + string feedDescription; + } + + function run() external { + vm.startBroadcast(); + address admin = msg.sender; + + CWTokenMarketRegistry registry = new CWTokenMarketRegistry(admin); + + TokenSeed[] memory seeds = _seeds(); + for (uint256 i = 0; i < seeds.length; i++) { + TokenSeed memory s = seeds[i]; + CWTokenUsdFeed feed = new CWTokenUsdFeed(registry, s.token, s.feedDescription); + registry.registerToken( + s.token, s.decimals, s.faceUsdE8, s.uniV3Pool, s.cwIsToken0, address(feed) + ); + console2.log("Registered", s.token); + console2.log(" feed", address(feed)); + } + + vm.stopBroadcast(); + console2.log("CWTokenMarketRegistry", address(registry)); + } + + function _seeds() internal pure returns (TokenSeed[] memory) { + TokenSeed[] memory seeds = new TokenSeed[](2); + seeds[0] = TokenSeed({ + token: 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a, + decimals: 6, + faceUsdE8: 100_000_000, + uniV3Pool: 0x1Cf2e685682C7F7beF508F0Af15Dfb5CDda01ee3, + cwIsToken0: true, + feedDescription: "cWUSDC / USD (GRU market anchor)" + }); + seeds[1] = TokenSeed({ + token: 0xaF5017d0163ecb99D9B5D94e3b4D7b09Af44D8AE, + decimals: 6, + faceUsdE8: 100_000_000, + uniV3Pool: 0x6eF5A392BcbE42F3D0a855595bEa60CFf9451784, + cwIsToken0: true, + feedDescription: "cWUSDT / USD (GRU market anchor)" + }); + return seeds; + } +} diff --git a/script/mainnet-market/RegisterAdditionalCWMarketTokens.s.sol b/script/mainnet-market/RegisterAdditionalCWMarketTokens.s.sol new file mode 100644 index 0000000..bc18043 --- /dev/null +++ b/script/mainnet-market/RegisterAdditionalCWMarketTokens.s.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console2} from "forge-std/Script.sol"; +import {CWTokenMarketRegistry} from "../../contracts/mainnet-market/CWTokenMarketRegistry.sol"; +import {CWTokenUsdFeed} from "../../contracts/mainnet-market/CWTokenUsdFeed.sol"; + +/// @notice Register remaining mainnet CompliantWrappedToken assets on deployed registry. +contract RegisterAdditionalCWMarketTokens is Script { + struct Seed { + address token; + uint8 decimals; + uint64 faceUsdE8; + address uniV3Pool; + bool cwIsToken0; + string symbol; + } + + function run() external { + address registryAddr = vm.envAddress("CW_MARKET_REGISTRY_MAINNET"); + CWTokenMarketRegistry registry = CWTokenMarketRegistry(registryAddr); + + Seed[] memory seeds = _seeds(); + vm.startBroadcast(); + for (uint256 i = 0; i < seeds.length; i++) { + Seed memory s = seeds[i]; + if (registry.tokenFeed(s.token) != address(0)) { + console2.log("skip already registered", s.symbol, s.token); + continue; + } + CWTokenUsdFeed feed = new CWTokenUsdFeed( + registry, s.token, string.concat(s.symbol, " / USD (GRU market anchor)") + ); + registry.registerToken( + s.token, s.decimals, s.faceUsdE8, s.uniV3Pool, s.cwIsToken0, address(feed) + ); + console2.log("Registered", s.symbol, s.token); + console2.log(" feed", address(feed)); + } + vm.stopBroadcast(); + console2.log("CWTokenMarketRegistry", registryAddr); + } + + function _seeds() internal pure returns (Seed[] memory) { + // face $1.00 e8 for ISO fiat stables; pool=0 when no live UniV3 peg pool (DODO/UniV2 only) + Seed[] memory seeds = new Seed[](10); + seeds[0] = Seed(0xD4aEAa8cD3fB41Dc8437FaC7639B6d91B60A5e8d, 6, 100_000_000, address(0), true, "cWEURC"); + seeds[1] = Seed(0x855d74FFB6CF75721a9bAbc8B2ed35c8119241dC, 6, 100_000_000, address(0), true, "cWEURT"); + seeds[2] = Seed(0xc074007dc0Bfb384B1cf6426a56287Ed23FE4D52, 6, 100_000_000, address(0), true, "cWGBPC"); + seeds[3] = Seed(0x1dDF9970F01c76A692Fdba2706203E6f16e0C46F, 6, 100_000_000, address(0), true, "cWGBPT"); + seeds[4] = Seed(0x5020Db641B3Fc0dAbBc0c688C845bc4E3699f35F, 6, 100_000_000, address(0), true, "cWAUDC"); + seeds[5] = Seed(0x07EEd0D7dD40984e47B9D3a3bdded1c536435582, 6, 100_000_000, address(0), true, "cWJPYC"); + seeds[6] = Seed(0x0F91C5E6Ddd46403746aAC970D05d70FFe404780, 6, 100_000_000, address(0), true, "cWCHFC"); + seeds[7] = Seed(0x209FE32fe7B541751D190ae4e50cd005DcF8EDb4, 6, 100_000_000, address(0), true, "cWCADC"); + seeds[8] = Seed(0x572Be0fa8CA0534d642A567CEDb398B771D8a715, 6, 3_300_000_000_00, address(0), true, "cWXAUC"); + seeds[9] = Seed(0xACE1DBF857549a11aF1322e1f91F2F64b029c906, 6, 3_300_000_000_00, address(0), true, "cWXAUT"); + // cWBTC: register separately with live BTC/USD face (volatile) + return seeds; + } +} diff --git a/script/mainnet-market/RegisterCWBTMarketToken.s.sol b/script/mainnet-market/RegisterCWBTMarketToken.s.sol new file mode 100644 index 0000000..9dd3b2c --- /dev/null +++ b/script/mainnet-market/RegisterCWBTMarketToken.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console2} from "forge-std/Script.sol"; +import {CWTokenMarketRegistry} from "../../contracts/mainnet-market/CWTokenMarketRegistry.sol"; +import {CWTokenUsdFeed} from "../../contracts/mainnet-market/CWTokenUsdFeed.sol"; +import {IAggregatorV3} from "../../contracts/mainnet-market/interfaces/IAggregatorV3.sol"; + +/// @notice Register mainnet cWBTC with live Chainlink BTC/USD face (8 decimals). +contract RegisterCWBTMarketToken is Script { + address internal constant CWBTC = 0x2BBe3c809386FA5349EB2da31E0848ee9f54EE59; + address internal constant CHAINLINK_BTC_USD = 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c; + + function run() external { + address registryAddr = vm.envAddress("CW_MARKET_REGISTRY_MAINNET"); + CWTokenMarketRegistry registry = CWTokenMarketRegistry(registryAddr); + + if (registry.tokenFeed(CWBTC) != address(0)) { + console2.log("skip already registered cWBTC", CWBTC); + console2.log(" feed", registry.tokenFeed(CWBTC)); + return; + } + + (, int256 answer,, uint256 updatedAt,) = IAggregatorV3(CHAINLINK_BTC_USD).latestRoundData(); + require(answer > 0, "chainlink btc/usd stale"); + require(updatedAt > 0, "chainlink btc/usd no timestamp"); + uint64 faceUsdE8 = uint64(uint256(answer)); + require(faceUsdE8 > 0, "zero btc usd"); + + console2.log("Chainlink BTC/USD faceUsdE8", faceUsdE8); + + vm.startBroadcast(); + CWTokenUsdFeed feed = new CWTokenUsdFeed(registry, CWBTC, "cWBTC / USD (GRU market anchor)"); + registry.registerToken(CWBTC, 8, faceUsdE8, address(0), true, address(feed)); + vm.stopBroadcast(); + + console2.log("Registered cWBTC", CWBTC); + console2.log(" feed", address(feed)); + console2.log("CWTokenMarketRegistry", registryAddr); + } +} diff --git a/scripts/deployment/seed-chain138-canonical-pmm-pools.sh b/scripts/deployment/seed-chain138-canonical-pmm-pools.sh index 82421dd..32c9e52 100755 --- a/scripts/deployment/seed-chain138-canonical-pmm-pools.sh +++ b/scripts/deployment/seed-chain138-canonical-pmm-pools.sh @@ -24,7 +24,7 @@ fi command -v cast >/dev/null 2>&1 || { echo "cast is required" >&2; exit 1; } RPC_URL_138="${RPC_URL_138:-${RPC_URL:-http://192.168.11.221:8545}}" -DODO_PMM_INTEGRATION_ADDRESS="${CHAIN_138_DODO_PMM_INTEGRATION:-${DODO_PMM_INTEGRATION_ADDRESS:-0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d}}" +DODO_PMM_INTEGRATION_ADDRESS="${CHAIN_138_DODO_PMM_INTEGRATION:-${DODO_PMM_INTEGRATION_ADDRESS:-0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895}}" PRIVATE_KEY="${PRIVATE_KEY:-${DEPLOYER_PRIVATE_KEY:-}}" CHAIN_GAS_PRICE="${CHAIN_GAS_PRICE:-1000000000}" APPROVE_GAS_LIMIT="${APPROVE_GAS_LIMIT:-200000}" diff --git a/scripts/mint-xau-chain138.sh b/scripts/mint-xau-chain138.sh index 4d3c862..8ddcba8 100755 --- a/scripts/mint-xau-chain138.sh +++ b/scripts/mint-xau-chain138.sh @@ -65,9 +65,65 @@ echo " Troy ounces: $OZ -> base units: $BASE_UNITS" echo " cXAUC: $DO_CXAUC cXAUT: $DO_CXAUT" echo "" +MULTISIG="${ADDR_MULTISIG:-0xb9E29cFa1f89d369671E640d0BB3aD94Cab43965}" +GAS_LIMIT_MULTISIG="${GAS_LIMIT_MULTISIG:-600000}" +GAS_PRICE="${GAS_PRICE_138:-1000000000}" + +extract_tx_hash() { + awk '/^transactionHash[[:space:]]/ {print $2; exit}' +} + +tx_status_ok() { + local txhash="$1" + [[ -n "$txhash" && "$txhash" == 0x* ]] || return 1 + local st + st=$(cast receipt "$txhash" status --rpc-url "$RPC" 2>/dev/null | awk '{print $1}' || echo "") + [[ "$st" == "1" || "$st" == "true" ]] +} + +mint_via_multisig() { + local addr="$1" label="$2" + local data txid exec_out txhash count + data=$(cast calldata "mint(address,uint256)" "$TO" "$BASE_UNITS") + echo -n " multisig mint $label... " + if [ -n "${DRY_RUN:-}" ]; then + echo "[dry-run]" + return 0 + fi + cast send "$MULTISIG" \ + "submitTransaction(address,uint256,bytes)" "$addr" 0 "$data" \ + --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy \ + --gas-limit "$GAS_LIMIT_MULTISIG" --gas-price "$GAS_PRICE" >/dev/null || { + echo "FAIL (submit)"; return 1 + } + count=$(cast call "$MULTISIG" "getTransactionCount()(uint256)" --rpc-url "$RPC" | awk '{print $1}') + txid=$((count - 1)) + exec_out=$(cast send "$MULTISIG" "executeTransaction(uint256)" "$txid" \ + --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy \ + --gas-limit "$GAS_LIMIT_MULTISIG" --gas-price "$GAS_PRICE" 2>&1) || { + echo "FAIL (execute)"; return 1 + } + txhash=$(echo "$exec_out" | extract_tx_hash) + if tx_status_ok "$txhash"; then + echo "OK (tx $txhash)" + return 0 + fi + echo "FAIL (receipt: $txhash)" + return 1 +} + run_mint() { local addr="$1" local label="$2" + local owner lc_to lc_own lc_ms + owner=$(cast call "$addr" "owner()(address)" --rpc-url "$RPC" 2>/dev/null || echo "") + lc_to=$(echo "$TO" | tr '[:upper:]' '[:lower:]') + lc_own=$(echo "$owner" | tr '[:upper:]' '[:lower:]') + lc_ms=$(echo "$MULTISIG" | tr '[:upper:]' '[:lower:]') + if [[ "$lc_own" == "$lc_ms" ]]; then + mint_via_multisig "$addr" "$label" || return 1 + return 0 + fi if [ -n "${DRY_RUN:-}" ]; then echo "[dry-run] cast send $label $addr mint $TO $BASE_UNITS" return 0 diff --git a/services/relay/start-relay.sh b/services/relay/start-relay.sh index 0d536e4..c12453e 100755 --- a/services/relay/start-relay.sh +++ b/services/relay/start-relay.sh @@ -90,13 +90,15 @@ fi # Ensure PRIVATE_KEY is exported if [ -n "$PRIVATE_KEY" ]; then export PRIVATE_KEY - if [ "$PROFILE" != "mainnet-link-138" ] || [ -z "${CCIP_MAINNET_LINK138_RELAYER_PRIVATE_KEY:-}" ]; then + if { [ "$PROFILE" = "mainnet-link-138" ] || [ -z "$PROFILE" ]; } && [ -n "${CCIP_MAINNET_LINK138_RELAYER_PRIVATE_KEY:-}" ]; then + : # dedicated relayer set below + else export RELAYER_PRIVATE_KEY="$PRIVATE_KEY" fi fi -# Dedicated relayer for mainnet-link-138 (wins over deployer PRIVATE_KEY). -if [ "$PROFILE" = "mainnet-link-138" ] && [ -n "${CCIP_MAINNET_LINK138_RELAYER_PRIVATE_KEY:-}" ]; then +# Dedicated relayer for mainnet-link-138 and default WETH lane (wins over deployer PRIVATE_KEY). +if { [ "$PROFILE" = "mainnet-link-138" ] || [ -z "$PROFILE" ]; } && [ -n "${CCIP_MAINNET_LINK138_RELAYER_PRIVATE_KEY:-}" ]; then export RELAYER_PRIVATE_KEY="$CCIP_MAINNET_LINK138_RELAYER_PRIVATE_KEY" if [ -n "${CCIP_MAINNET_LINK138_RELAYER_ADDRESS:-}" ]; then export RELAYER_ADDRESS="$CCIP_MAINNET_LINK138_RELAYER_ADDRESS" diff --git a/test/mainnet-market/CWTokenMarketRegistry.t.sol b/test/mainnet-market/CWTokenMarketRegistry.t.sol new file mode 100644 index 0000000..df8ec39 --- /dev/null +++ b/test/mainnet-market/CWTokenMarketRegistry.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {CWTokenMarketRegistry} from "../../contracts/mainnet-market/CWTokenMarketRegistry.sol"; +import {CWTokenUsdFeed} from "../../contracts/mainnet-market/CWTokenUsdFeed.sol"; + +contract CWTokenMarketRegistryTest is Test { + CWTokenMarketRegistry registry; + MockErc20 token; + CWTokenUsdFeed feed; + + function setUp() public { + registry = new CWTokenMarketRegistry(address(this)); + token = new MockErc20(6, 15_452_106_931_334_998_000_000); + feed = new CWTokenUsdFeed(registry, address(token), "test cWUSD / USD"); + registry.registerToken(address(token), 6, 100_000_000, address(0), true, address(feed)); + } + + function test_latestRoundData_faceBootstrap() public view { + (, int256 answer,,,) = feed.latestRoundData(); + assertEq(answer, 100_000_000); + } + + function test_onchainMarketCap() public view { + uint256 cap = registry.onchainMarketCapUsdE8(address(token)); + assertEq(cap, 15_452_106_931_334_998_000_000 * 100_000_000 / 1e6); + } + + function test_publishPrice_updatesFeed() public { + registry.publishPrice(address(token), 99_500_000, "univ3_twap"); + (, int256 answer,,,) = feed.latestRoundData(); + assertEq(answer, 99_500_000); + } + + function test_publishPrice_revertsOutsidePegBand() public { + vm.expectRevert(bytes("peg band")); + registry.publishPrice(address(token), 50_000_000, "bad"); + } + + function test_registerToken_revertsFeedMismatch() public { + CWTokenUsdFeed badFeed = new CWTokenUsdFeed(registry, address(0xBEEF), "bad"); + vm.expectRevert(bytes("feed mismatch")); + registry.registerToken(address(0xCAFE), 6, 100_000_000, address(0), true, address(badFeed)); + } + + function test_recordX402Settlement() public { + registry.recordX402Settlement( + address(token), + address(0x4A66), + address(0x6435), + 10_000, + 10_000, + 8453, + bytes32(uint256(1)), + keccak256("resource") + ); + (,,,,,,,, uint256 settlementCount) = registry.tokens(address(token)); + assertEq(settlementCount, 1); + } + + function test_pauseBlocksPublish() public { + registry.pause(); + vm.expectRevert(bytes("paused")); + registry.publishPrice(address(token), 100_000_000, "x"); + } +} + +contract MockErc20 { + uint8 public decimals; + uint256 public totalSupply; + + constructor(uint8 d, uint256 supply) { + decimals = d; + totalSupply = supply; + } +} diff --git a/test/mainnet-market/CWTokenMarketRegistryFork.t.sol b/test/mainnet-market/CWTokenMarketRegistryFork.t.sol new file mode 100644 index 0000000..1ca2ab9 --- /dev/null +++ b/test/mainnet-market/CWTokenMarketRegistryFork.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {CWTokenMarketRegistry} from "../../contracts/mainnet-market/CWTokenMarketRegistry.sol"; +import {CWTokenUsdFeed} from "../../contracts/mainnet-market/CWTokenUsdFeed.sol"; + +contract CWTokenMarketRegistryForkTest is Test { + address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a; + address constant CWUSDT = 0xaF5017d0163ecb99D9B5D94e3b4D7b09Af44D8AE; + address constant POOL_CWUSDC = 0x1Cf2e685682C7F7beF508F0Af15Dfb5CDda01ee3; + address constant POOL_CWUSDT = 0x6eF5A392BcbE42F3D0a855595bEa60CFf9451784; + address constant CWBTC = 0x2BBe3c809386FA5349EB2da31E0848ee9f54EE59; + address constant CHAINLINK_BTC_USD = 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c; + + function test_fork_registerMainnetTokensAndMarketCap() public { + string memory rpc = vm.envOr(string("ETHEREUM_MAINNET_RPC"), string("https://ethereum.publicnode.com")); + vm.createSelectFork(rpc); + + CWTokenMarketRegistry registry = new CWTokenMarketRegistry(address(this)); + + CWTokenUsdFeed feedUsdc = new CWTokenUsdFeed(registry, CWUSDC, "cWUSDC / USD"); + registry.registerToken(CWUSDC, 6, 100_000_000, POOL_CWUSDC, true, address(feedUsdc)); + + CWTokenUsdFeed feedUsdt = new CWTokenUsdFeed(registry, CWUSDT, "cWUSDT / USD"); + registry.registerToken(CWUSDT, 6, 100_000_000, POOL_CWUSDT, true, address(feedUsdt)); + + (, int256 usdcPx,,,) = feedUsdc.latestRoundData(); + assertEq(usdcPx, 100_000_000); + + uint256 mcap = registry.onchainMarketCapUsdE8(CWUSDC); + assertGt(mcap, 0); + + assertEq(registry.tokenFeed(CWUSDC), address(feedUsdc)); + assertEq(registry.tokenCount(), 2); + } + + function test_fork_registerCWBTWithChainlinkBtcUsd() public { + string memory rpc = vm.envOr(string("ETHEREUM_MAINNET_RPC"), string("https://ethereum.publicnode.com")); + vm.createSelectFork(rpc); + + (, int256 answer,,,) = AggregatorV3(CHAINLINK_BTC_USD).latestRoundData(); + assertGt(answer, 0); + + CWTokenMarketRegistry registry = new CWTokenMarketRegistry(address(this)); + uint64 faceUsdE8 = uint64(uint256(answer)); + + CWTokenUsdFeed feed = new CWTokenUsdFeed(registry, CWBTC, "cWBTC / USD"); + registry.registerToken(CWBTC, 8, faceUsdE8, address(0), true, address(feed)); + + (, int256 px,,,) = feed.latestRoundData(); + assertEq(px, answer); + assertGt(registry.onchainMarketCapUsdE8(CWBTC), 0); + } +} + +interface AggregatorV3 { + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +}