Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m18s
CI/CD Pipeline / Security Scanning (push) Successful in 2m27s
CI/CD Pipeline / Lint and Format (push) Failing after 46s
CI/CD Pipeline / Terraform Validation (push) Failing after 28s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 29s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 1m0s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 29s
Validation / validate-genesis (push) Successful in 30s
Validation / validate-terraform (push) Failing after 32s
Validation / validate-kubernetes (push) Failing after 10s
Validation / validate-smart-contracts (push) Failing after 13s
Validation / validate-security (push) Failing after 1m23s
Validation / validate-documentation (push) Failing after 22s
Verify Deployment / Verify Deployment (push) Failing after 11m53s
- CWTokenMarketRegistry + USD feeds for mainnet cW* market anchors - HYBX cross-chain treasury line registry expansion - PMM seed default → live DODOPMMIntegration (0x86ADA6Ef…) - mint-xau-chain138: troy-ounce mint helper with multisig path - relay: prefer CCIP_MAINNET_LINK138 relayer key on default profile Co-authored-by: Cursor <cursoragent@cursor.com>
217 lines
6.9 KiB
Solidity
217 lines
6.9 KiB
Solidity
// 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");
|
|
}
|
|
}
|