diff --git a/config/chain138-pmm-pools.json b/config/chain138-pmm-pools.json index 243a869..d646ffa 100644 --- a/config/chain138-pmm-pools.json +++ b/config/chain138-pmm-pools.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Desired-state pool spec for Chain 138 DODO PMM. Scripts should create and register only missing pools from this file, not redeploy contracts.", "version": "1.1.0", - "updated": "2026-04-19", + "updated": "2026-05-09", "chainId": 138, "defaults": { "lpFeeRate": 3, @@ -14,6 +14,10 @@ "cBTC": "0xe94260c555aC1d9D3CC9E1632883452ebDf0082E", "cUSDT": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", "cUSDC": "0xf22258f57794CC8E06237084b353Ab30fFfa640b", + "cUSDT_V2": "0x9FBfab33882Efe0038DAa608185718b772EE5660", + "cUSDC_V2": "0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d", + "cUSDW": "0xca6BfA614935F1aB71c9aB106baa6fbB6057095E", + "cAUSDT": "0x5fdDF65733e3d590463F68f93Cf16E8c04081271", "cEURC": "0x8085961F9cF02b4d800A3c6d386D31da4B34266a", "cEURT": "0xdf4b71c61E5912712C1Bdd451416B9aC26949d72", "cGBPC": "0x003960f16D9d34F2e98d62723B6721Fb92074aD2", @@ -32,6 +36,7 @@ "cStarSymbols": [ "cUSDT", "cUSDC", + "cAUSDT", "cEURC", "cEURT", "cGBPC", @@ -60,8 +65,12 @@ { "baseSymbol": "cBTC", "quoteSymbol": "cUSDC" }, { "baseSymbol": "cBTC", "quoteSymbol": "cXAUC" }, { "baseSymbol": "cUSDT", "quoteSymbol": "cUSDC" }, + { "baseSymbol": "cUSDC_V2", "quoteSymbol": "cUSDT_V2" }, + { "baseSymbol": "cUSDW", "quoteSymbol": "cUSDC" }, { "baseSymbol": "cUSDT", "quoteSymbol": "USDT" }, { "baseSymbol": "cUSDC", "quoteSymbol": "USDC" }, + { "baseSymbol": "cAUSDT", "quoteSymbol": "cUSDC" }, + { "baseSymbol": "cAUSDT", "quoteSymbol": "cUSDT" }, { "baseSymbol": "cEURC", "quoteSymbol": "cUSDC" }, { "baseSymbol": "cEURT", "quoteSymbol": "cUSDC" }, { "baseSymbol": "cGBPC", "quoteSymbol": "cUSDC" }, diff --git a/contracts/flash/DBISEngineXFlashProofBorrower.sol b/contracts/flash/DBISEngineXFlashProofBorrower.sol new file mode 100644 index 0000000..ad7f507 --- /dev/null +++ b/contracts/flash/DBISEngineXFlashProofBorrower.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; +import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @notice Minimal Engine X ERC-3156 borrower for proving same-transaction USDC working capital. +/// @dev Prefund this contract with at least the flash fee before calling `runFlashProof`. +contract DBISEngineXFlashProofBorrower is IERC3156FlashBorrower, Ownable { + using SafeERC20 for IERC20; + + bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + IERC3156FlashLender public immutable lender; + IERC20 public immutable usdc; + + mapping(bytes32 => bool) public usedProofIds; + + event EngineXFlashProof( + bytes32 indexed proofId, + address indexed initiator, + address indexed lender, + address token, + uint256 amount, + uint256 fee, + bytes32 iso20022DocumentHash, + bytes32 auditEnvelopeHash, + bytes32 pegProofHash + ); + + constructor(address lender_, address usdc_, address owner_) Ownable(owner_) { + require(lender_ != address(0) && usdc_ != address(0), "zero address"); + lender = IERC3156FlashLender(lender_); + usdc = IERC20(usdc_); + } + + function runFlashProof( + uint256 amount, + bytes32 proofId, + bytes32 iso20022DocumentHash, + bytes32 auditEnvelopeHash, + bytes32 pegProofHash + ) external onlyOwner returns (bool) { + require(proofId != bytes32(0), "zero proof"); + require(!usedProofIds[proofId], "proof used"); + require(iso20022DocumentHash != bytes32(0), "zero iso hash"); + require(auditEnvelopeHash != bytes32(0), "zero audit hash"); + require(pegProofHash != bytes32(0), "zero peg hash"); + + bytes memory data = abi.encode(msg.sender, proofId, iso20022DocumentHash, auditEnvelopeHash, pegProofHash); + return lender.flashLoan(this, address(usdc), amount, data); + } + + function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) + external + override + returns (bytes32) + { + require(msg.sender == address(lender), "bad lender"); + require(initiator == address(this), "bad initiator"); + require(token == address(usdc), "bad token"); + + ( + address operator, + bytes32 proofId, + bytes32 iso20022DocumentHash, + bytes32 auditEnvelopeHash, + bytes32 pegProofHash + ) = abi.decode(data, (address, bytes32, bytes32, bytes32, bytes32)); + require(!usedProofIds[proofId], "proof used"); + usedProofIds[proofId] = true; + + usdc.forceApprove(msg.sender, amount + fee); + emit EngineXFlashProof( + proofId, operator, msg.sender, token, amount, fee, iso20022DocumentHash, auditEnvelopeHash, pegProofHash + ); + return _RETURN_VALUE; + } + + function withdraw(address token, address to, uint256 amount) external onlyOwner { + require(to != address(0), "zero to"); + IERC20(token).safeTransfer(to, amount); + } +} diff --git a/contracts/flash/DBISEngineXIndexedLiquidityVault.sol b/contracts/flash/DBISEngineXIndexedLiquidityVault.sol new file mode 100644 index 0000000..1618624 --- /dev/null +++ b/contracts/flash/DBISEngineXIndexedLiquidityVault.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +interface IEngineXUniswapV3PoolLike { + function token0() external view returns (address); + function token1() external view returns (address); + function fee() external view returns (uint24); + function liquidity() external view returns (uint128); + + function slot0() + external + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ); +} + +/// @notice Public indexed-liquidity proof anchor for Engine X. +/// @dev This contract does not claim virtual DEX volume. It anchors Engine X proof IDs to a public UniV3 pool state. +contract DBISEngineXIndexedLiquidityVault is Ownable { + address public immutable cWUSDC; + address public immutable usdc; + IEngineXUniswapV3PoolLike public immutable pool; + uint24 public immutable fee; + + uint256 public positionTokenId; + int24 public maxAbsTick; + uint128 public minLiquidity; + uint256 public minToken0Balance; + uint256 public minToken1Balance; + uint256 public maxProofSwapAmount; + bool public paused; + bool public operatorAllowlistEnabled; + + mapping(address => bool) public approvedOperator; + mapping(bytes32 => bool) public usedProofIds; + + struct IndexedProof { + bytes32 proofId; + bytes32 publicSwapTxHash; + bytes32 liquidityTxHash; + address outputRecipient; + uint256 exactOutputAmount; + bytes32 iso20022DocumentHash; + bytes32 auditEnvelopeHash; + bytes32 pegProofHash; + } + + event RiskControlsUpdated( + int24 maxAbsTick, + uint128 minLiquidity, + uint256 minToken0Balance, + uint256 minToken1Balance, + uint256 maxProofSwapAmount + ); + event PositionTokenIdUpdated(uint256 positionTokenId); + event Paused(address indexed operator); + event Unpaused(address indexed operator); + event OperatorAllowlistEnabledUpdated(bool enabled); + event OperatorApprovalUpdated(address indexed operator, bool approved); + event IndexedLiquidityProof( + bytes32 indexed proofId, + address indexed operator, + address indexed outputRecipient, + address pool, + uint24 fee, + uint256 positionTokenId, + uint160 sqrtPriceX96, + int24 tick, + uint128 liquidity, + uint256 token0Balance, + uint256 token1Balance, + uint256 exactOutputAmount, + bytes32 publicSwapTxHash, + bytes32 liquidityTxHash, + bytes32 iso20022DocumentHash, + bytes32 auditEnvelopeHash, + bytes32 pegProofHash + ); + + modifier whenNotPaused() { + require(!paused, "paused"); + _; + } + + modifier onlyApprovedOperator() { + require(!operatorAllowlistEnabled || approvedOperator[msg.sender], "operator not approved"); + _; + } + + constructor( + address cWUSDC_, + address usdc_, + address pool_, + address owner_, + int24 maxAbsTick_, + uint128 minLiquidity_, + uint256 maxProofSwapAmount_ + ) Ownable(owner_) { + require(cWUSDC_ != address(0) && usdc_ != address(0) && pool_ != address(0), "zero address"); + require(owner_ != address(0), "zero owner"); + cWUSDC = cWUSDC_; + usdc = usdc_; + pool = IEngineXUniswapV3PoolLike(pool_); + + address token0 = pool.token0(); + address token1 = pool.token1(); + require((token0 == cWUSDC_ && token1 == usdc_) || (token0 == usdc_ && token1 == cWUSDC_), "pool token mismatch"); + + fee = pool.fee(); + _setRiskControls(maxAbsTick_, minLiquidity_, 0, 0, maxProofSwapAmount_); + } + + function pause() external onlyOwner { + paused = true; + emit Paused(msg.sender); + } + + function unpause() external onlyOwner { + paused = false; + emit Unpaused(msg.sender); + } + + function setOperatorAllowlistEnabled(bool enabled) external onlyOwner { + operatorAllowlistEnabled = enabled; + emit OperatorAllowlistEnabledUpdated(enabled); + } + + function setOperatorApproved(address operator, bool approved) external onlyOwner { + require(operator != address(0), "zero operator"); + approvedOperator[operator] = approved; + emit OperatorApprovalUpdated(operator, approved); + } + + function setPositionTokenId(uint256 positionTokenId_) external onlyOwner { + positionTokenId = positionTokenId_; + emit PositionTokenIdUpdated(positionTokenId_); + } + + function setRiskControls( + int24 maxAbsTick_, + uint128 minLiquidity_, + uint256 minToken0Balance_, + uint256 minToken1Balance_, + uint256 maxProofSwapAmount_ + ) external onlyOwner { + _setRiskControls(maxAbsTick_, minLiquidity_, minToken0Balance_, minToken1Balance_, maxProofSwapAmount_); + } + + function currentPoolState() + public + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint128 activeLiquidity, + uint256 token0Balance, + uint256 token1Balance + ) + { + (sqrtPriceX96, tick,,,,,) = pool.slot0(); + activeLiquidity = pool.liquidity(); + token0Balance = IERC20(pool.token0()).balanceOf(address(pool)); + token1Balance = IERC20(pool.token1()).balanceOf(address(pool)); + } + + function recordIndexedProof(IndexedProof calldata proof) + external + whenNotPaused + onlyApprovedOperator + returns (uint160 sqrtPriceX96, int24 tick, uint128 activeLiquidity) + { + require(proof.proofId != bytes32(0), "zero proof"); + require(!usedProofIds[proof.proofId], "proof used"); + require(proof.publicSwapTxHash != bytes32(0), "zero swap hash"); + require(proof.liquidityTxHash != bytes32(0), "zero liquidity hash"); + require(proof.outputRecipient != address(0), "zero recipient"); + require(proof.exactOutputAmount > 0, "zero output"); + require(maxProofSwapAmount == 0 || proof.exactOutputAmount <= maxProofSwapAmount, "proof amount too high"); + require(proof.iso20022DocumentHash != bytes32(0), "zero iso hash"); + require(proof.auditEnvelopeHash != bytes32(0), "zero audit hash"); + require(proof.pegProofHash != bytes32(0), "zero peg hash"); + + uint256 token0Balance; + uint256 token1Balance; + (sqrtPriceX96, tick, activeLiquidity, token0Balance, token1Balance) = currentPoolState(); + require(_absTick(tick) <= uint24(maxAbsTick), "tick drift too high"); + require(activeLiquidity >= minLiquidity, "insufficient liquidity"); + require(token0Balance >= minToken0Balance, "insufficient token0 balance"); + require(token1Balance >= minToken1Balance, "insufficient token1 balance"); + + usedProofIds[proof.proofId] = true; + emit IndexedLiquidityProof( + proof.proofId, + msg.sender, + proof.outputRecipient, + address(pool), + fee, + positionTokenId, + sqrtPriceX96, + tick, + activeLiquidity, + token0Balance, + token1Balance, + proof.exactOutputAmount, + proof.publicSwapTxHash, + proof.liquidityTxHash, + proof.iso20022DocumentHash, + proof.auditEnvelopeHash, + proof.pegProofHash + ); + } + + function _setRiskControls( + int24 maxAbsTick_, + uint128 minLiquidity_, + uint256 minToken0Balance_, + uint256 minToken1Balance_, + uint256 maxProofSwapAmount_ + ) internal { + require(maxAbsTick_ >= 0, "negative tick gate"); + maxAbsTick = maxAbsTick_; + minLiquidity = minLiquidity_; + minToken0Balance = minToken0Balance_; + minToken1Balance = minToken1Balance_; + maxProofSwapAmount = maxProofSwapAmount_; + emit RiskControlsUpdated(maxAbsTick_, minLiquidity_, minToken0Balance_, minToken1Balance_, maxProofSwapAmount_); + } + + function _absTick(int24 tick) internal pure returns (uint24) { + return tick >= 0 ? uint24(tick) : uint24(-tick); + } +} diff --git a/contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol b/contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol new file mode 100644 index 0000000..ede0fb0 --- /dev/null +++ b/contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +interface IEngineXDodoPoolLike { + function _BASE_TOKEN_() external view returns (address); + function _QUOTE_TOKEN_() external view returns (address); + function querySellBase(address trader, uint256 payBaseAmount) + external + view + returns (uint256 receiveQuoteAmount, uint256 mtFee); + function querySellQuote(address trader, uint256 payQuoteAmount) + external + view + returns (uint256 receiveBaseAmount, uint256 mtFee); + function getVaultReserve() external view returns (uint256 baseReserve, uint256 quoteReserve); +} + +interface IEngineXDodoIntegrationLike { + function addLiquidity(address pool, uint256 baseAmount, uint256 quoteAmount) + external + returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare); +} + +/// @notice Engine X single-sided cWUSDC inventory wrapper for later DODO PMM promotion. +/// @dev cWUSDC-only inventory is accounting/support inventory, not executable public DODO liquidity. +/// DODO promotion is allowed only with nonzero base and quote amounts and passing canary guards. +contract DBISEngineXSingleSidedDodoCwusdcVault is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + IERC20 public immutable cWUSDC; + IERC20 public immutable quoteToken; + IEngineXDodoIntegrationLike public immutable dodoIntegration; + + address public dodoPool; + bool public paused; + + uint256 public accountedCwusdcInventory; + uint256 public accountedQuoteInventory; + uint256 public totalCwusdcPromotedToDodo; + uint256 public totalQuotePromotedToDodo; + + uint256 public sampleBaseIn; + uint256 public minQuoteOut; + uint256 public sampleQuoteIn; + uint256 public minBaseOut; + + event Paused(address indexed operator); + event Unpaused(address indexed operator); + event DodoPoolUpdated(address indexed pool); + event CanaryUpdated(uint256 sampleBaseIn, uint256 minQuoteOut, uint256 sampleQuoteIn, uint256 minBaseOut); + event CwusdcInventoryDeposited(address indexed from, uint256 amount); + event QuoteInventoryDeposited(address indexed from, uint256 amount); + event InventoryWithdrawn(address indexed token, address indexed to, uint256 amount); + event DodoLiquidityPromoted( + address indexed pool, + uint256 baseAmount, + uint256 quoteAmount, + uint256 baseShare, + uint256 quoteShare, + uint256 lpShare + ); + event UnaccountedTokenWithdrawn(address indexed token, address indexed to, uint256 amount); + + modifier whenNotPaused() { + require(!paused, "paused"); + _; + } + + constructor(address cWUSDC_, address quoteToken_, address dodoIntegration_, address owner_) Ownable(owner_) { + require(cWUSDC_ != address(0) && quoteToken_ != address(0), "zero token"); + require(dodoIntegration_ != address(0), "zero integration"); + require(owner_ != address(0), "zero owner"); + require(cWUSDC_ != quoteToken_, "same token"); + cWUSDC = IERC20(cWUSDC_); + quoteToken = IERC20(quoteToken_); + dodoIntegration = IEngineXDodoIntegrationLike(dodoIntegration_); + } + + function pause() external onlyOwner { + paused = true; + emit Paused(msg.sender); + } + + function unpause() external onlyOwner { + paused = false; + emit Unpaused(msg.sender); + } + + function setDodoPool(address pool) external onlyOwner { + _validatePool(pool); + dodoPool = pool; + emit DodoPoolUpdated(pool); + } + + function setCanary(uint256 sampleBaseIn_, uint256 minQuoteOut_, uint256 sampleQuoteIn_, uint256 minBaseOut_) + external + onlyOwner + { + require(sampleBaseIn_ > 0 || sampleQuoteIn_ > 0, "zero canary"); + require((sampleBaseIn_ == 0) == (minQuoteOut_ == 0), "base canary mismatch"); + require((sampleQuoteIn_ == 0) == (minBaseOut_ == 0), "quote canary mismatch"); + sampleBaseIn = sampleBaseIn_; + minQuoteOut = minQuoteOut_; + sampleQuoteIn = sampleQuoteIn_; + minBaseOut = minBaseOut_; + emit CanaryUpdated(sampleBaseIn_, minQuoteOut_, sampleQuoteIn_, minBaseOut_); + } + + function depositCwusdc(uint256 amount) external nonReentrant whenNotPaused { + require(amount > 0, "zero deposit"); + cWUSDC.safeTransferFrom(msg.sender, address(this), amount); + accountedCwusdcInventory += amount; + emit CwusdcInventoryDeposited(msg.sender, amount); + } + + function depositQuote(uint256 amount) external nonReentrant whenNotPaused { + require(amount > 0, "zero deposit"); + quoteToken.safeTransferFrom(msg.sender, address(this), amount); + accountedQuoteInventory += amount; + emit QuoteInventoryDeposited(msg.sender, amount); + } + + function withdrawCwusdcInventory(address to, uint256 amount) external onlyOwner nonReentrant { + require(to != address(0), "zero to"); + require(amount > 0, "zero withdraw"); + require(amount <= accountedCwusdcInventory, "insufficient cwusdc inventory"); + accountedCwusdcInventory -= amount; + cWUSDC.safeTransfer(to, amount); + _requireSolvent(); + emit InventoryWithdrawn(address(cWUSDC), to, amount); + } + + function withdrawQuoteInventory(address to, uint256 amount) external onlyOwner nonReentrant { + require(to != address(0), "zero to"); + require(amount > 0, "zero withdraw"); + require(amount <= accountedQuoteInventory, "insufficient quote inventory"); + accountedQuoteInventory -= amount; + quoteToken.safeTransfer(to, amount); + _requireSolvent(); + emit InventoryWithdrawn(address(quoteToken), to, amount); + } + + function promoteToDodo( + uint256 baseAmount, + uint256 quoteAmount, + uint256 minBaseShare, + uint256 minQuoteShare, + uint256 minLpShare + ) external onlyOwner nonReentrant whenNotPaused returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare) { + require(dodoPool != address(0), "pool not set"); + require(baseAmount > 0 && quoteAmount > 0, "two-sided required"); + require(baseAmount <= accountedCwusdcInventory, "insufficient cwusdc inventory"); + require(quoteAmount <= accountedQuoteInventory, "insufficient quote inventory"); + + accountedCwusdcInventory -= baseAmount; + accountedQuoteInventory -= quoteAmount; + + cWUSDC.forceApprove(address(dodoIntegration), baseAmount); + quoteToken.forceApprove(address(dodoIntegration), quoteAmount); + (baseShare, quoteShare, lpShare) = dodoIntegration.addLiquidity(dodoPool, baseAmount, quoteAmount); + cWUSDC.forceApprove(address(dodoIntegration), 0); + quoteToken.forceApprove(address(dodoIntegration), 0); + + require(baseShare >= minBaseShare, "base share too low"); + require(quoteShare >= minQuoteShare, "quote share too low"); + require(lpShare >= minLpShare, "lp share too low"); + require(canaryPasses(), "canary failed"); + + totalCwusdcPromotedToDodo += baseAmount; + totalQuotePromotedToDodo += quoteAmount; + _requireSolvent(); + emit DodoLiquidityPromoted(dodoPool, baseAmount, quoteAmount, baseShare, quoteShare, lpShare); + } + + function canaryPasses() public view returns (bool) { + if (dodoPool == address(0)) return false; + IEngineXDodoPoolLike pool = IEngineXDodoPoolLike(dodoPool); + (uint256 baseReserve, uint256 quoteReserve) = pool.getVaultReserve(); + if (baseReserve == 0 || quoteReserve == 0) return false; + if (sampleBaseIn > 0) { + (uint256 quoteOut,) = pool.querySellBase(address(this), sampleBaseIn); + if (quoteOut < minQuoteOut) return false; + } + if (sampleQuoteIn > 0) { + (uint256 baseOut,) = pool.querySellQuote(address(this), sampleQuoteIn); + if (baseOut < minBaseOut) return false; + } + return true; + } + + function solvencyState() + external + view + returns ( + uint256 cwusdcBalance, + uint256 quoteBalance, + uint256 cwusdcInventory, + uint256 quoteInventory, + bool solvent, + bool executable + ) + { + cwusdcBalance = cWUSDC.balanceOf(address(this)); + quoteBalance = quoteToken.balanceOf(address(this)); + cwusdcInventory = accountedCwusdcInventory; + quoteInventory = accountedQuoteInventory; + solvent = cwusdcBalance >= cwusdcInventory && quoteBalance >= quoteInventory; + executable = canaryPasses(); + } + + function rescueUnaccountedToken(address token, address to, uint256 amount) external onlyOwner nonReentrant { + require(to != address(0), "zero to"); + require(amount > 0, "zero withdraw"); + IERC20(token).safeTransfer(to, amount); + _requireSolvent(); + emit UnaccountedTokenWithdrawn(token, to, amount); + } + + function _validatePool(address pool) internal view { + require(pool != address(0), "zero pool"); + require(IEngineXDodoPoolLike(pool)._BASE_TOKEN_() == address(cWUSDC), "unexpected base"); + require(IEngineXDodoPoolLike(pool)._QUOTE_TOKEN_() == address(quoteToken), "unexpected quote"); + } + + function _requireSolvent() internal view { + require(cWUSDC.balanceOf(address(this)) >= accountedCwusdcInventory, "cwusdc insolvent"); + require(quoteToken.balanceOf(address(this)) >= accountedQuoteInventory, "quote insolvent"); + } +} diff --git a/contracts/flash/DBISEngineXVirtualBatchVault.sol b/contracts/flash/DBISEngineXVirtualBatchVault.sol index d9d2e20..8e8bb95 100644 --- a/contracts/flash/DBISEngineXVirtualBatchVault.sol +++ b/contracts/flash/DBISEngineXVirtualBatchVault.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; +import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; @@ -8,9 +10,12 @@ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol /// @notice Engine X maintained proof vault with virtual batch settlement. /// @dev Use only for accounting proofs: it compresses identical maintained loops into one net settlement. -contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { +contract DBISEngineXVirtualBatchVault is IERC3156FlashLender, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; + bytes32 private constant _FLASH_RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + uint256 public constant MAX_FLASH_FEE_BPS = 1_000; + IERC20 public immutable cWUSDC; IERC20 public immutable usdc; IERC20 public immutable xaut; @@ -22,7 +27,13 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { uint256 public totalVirtualLoops; uint256 public totalVirtualDebtUsdc; uint256 public totalVirtualCwusdcIn; + uint256 public totalFlashFeesCollectedUsdc; + uint256 public flashFeeBps = 5; + uint256 public maxFlashLoanAmount; + bool public paused; + bool public flashBorrowerAllowlistEnabled; mapping(bytes32 => bool) public usedProofIds; + mapping(address => bool) public approvedFlashBorrower; uint256 public immutable xautUsdPrice6; uint256 public immutable ltvBps; @@ -58,7 +69,27 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { bytes32 auditEnvelopeHash, bytes32 pegProofHash ); - event OwnerWithdraw(address indexed token, address indexed to, uint256 amount); + event PoolLiquidityWithdrawn(address indexed to, uint256 cwusdcAmount, uint256 usdcAmount); + event LenderUsdcWithdrawn(address indexed to, uint256 amount); + event UnaccountedTokenWithdrawn(address indexed token, address indexed to, uint256 amount); + event FlashFeeBpsUpdated(uint256 feeBps); + event MaxFlashLoanAmountUpdated(uint256 amount); + event Paused(address indexed operator); + event Unpaused(address indexed operator); + event FlashBorrowerAllowlistEnabledUpdated(bool enabled); + event FlashBorrowerApprovalUpdated(address indexed borrower, bool approved); + event EngineXFlashLoan( + address indexed initiator, + IERC3156FlashBorrower indexed receiver, + address indexed token, + uint256 amount, + uint256 fee + ); + + modifier whenNotPaused() { + require(!paused, "paused"); + _; + } constructor( address cWUSDC_, @@ -85,7 +116,7 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { maxRoundTripLossBps = maxRoundTripLossBps_; } - function seedPool(uint256 cwusdcAmount, uint256 usdcAmount) external onlyOwner nonReentrant { + function seedPool(uint256 cwusdcAmount, uint256 usdcAmount) external onlyOwner nonReentrant whenNotPaused { require(cwusdcAmount > 0 && usdcAmount > 0, "zero seed"); cWUSDC.safeTransferFrom(msg.sender, address(this), cwusdcAmount); usdc.safeTransferFrom(msg.sender, address(this), usdcAmount); @@ -94,7 +125,7 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { emit PoolSeeded(cwusdcAmount, usdcAmount); } - function fundLender(uint256 usdcAmount) external onlyOwner nonReentrant { + function fundLender(uint256 usdcAmount) external onlyOwner nonReentrant whenNotPaused { require(usdcAmount > 0, "zero fund"); usdc.safeTransferFrom(msg.sender, address(this), usdcAmount); lenderUsdcAvailable += usdcAmount; @@ -107,6 +138,38 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { emit SurplusReceiverUpdated(receiver); } + function pause() external onlyOwner { + paused = true; + emit Paused(msg.sender); + } + + function unpause() external onlyOwner { + paused = false; + emit Unpaused(msg.sender); + } + + function setFlashFeeBps(uint256 newFlashFeeBps) external onlyOwner { + require(newFlashFeeBps <= MAX_FLASH_FEE_BPS, "fee too high"); + flashFeeBps = newFlashFeeBps; + emit FlashFeeBpsUpdated(newFlashFeeBps); + } + + function setMaxFlashLoanAmount(uint256 amount) external onlyOwner { + maxFlashLoanAmount = amount; + emit MaxFlashLoanAmountUpdated(amount); + } + + function setFlashBorrowerAllowlistEnabled(bool enabled) external onlyOwner { + flashBorrowerAllowlistEnabled = enabled; + emit FlashBorrowerAllowlistEnabledUpdated(enabled); + } + + function setFlashBorrowerApproved(address borrower, bool approved) external onlyOwner { + require(borrower != address(0), "zero borrower"); + approvedFlashBorrower[borrower] = approved; + emit FlashBorrowerApprovalUpdated(borrower, approved); + } + function previewCwusdcInForExactUsdc(uint256 usdcOut) public view returns (uint256) { return _getAmountIn(usdcOut, poolCwusdcReserve, poolUsdcReserve); } @@ -119,6 +182,21 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { return poolCwusdcReserve > poolUsdcReserve ? poolCwusdcReserve - poolUsdcReserve : 0; } + function maxFlashLoan(address token) external view override returns (uint256) { + if (paused || token != address(usdc)) { + return 0; + } + if (maxFlashLoanAmount == 0 || maxFlashLoanAmount > lenderUsdcAvailable) { + return lenderUsdcAvailable; + } + return maxFlashLoanAmount; + } + + function flashFee(address token, uint256 amount) public view override returns (uint256) { + require(token == address(usdc), "unsupported flash token"); + return (amount * flashFeeBps) / 10_000; + } + function minimumXautCollateral(uint256 debtUsdc) public view returns (uint256) { uint256 numerator = debtUsdc * 1e6 * 10_000; uint256 denominator = xautUsdPrice6 * ltvBps; @@ -155,7 +233,11 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { totalNeutralizedCwusdcAmount = cwusdcLossPerLoop * virtualLoops; } - function runVirtualProof(bytes32 proofId, uint256 debtUsdcPerLoop, uint256 virtualLoops) external nonReentrant { + function runVirtualProof(bytes32 proofId, uint256 debtUsdcPerLoop, uint256 virtualLoops) + external + nonReentrant + whenNotPaused + { _runVirtualProofFor( msg.sender, proofId, debtUsdcPerLoop, virtualLoops, 0, msg.sender, bytes32(0), bytes32(0), bytes32(0) ); @@ -164,6 +246,7 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { function runVirtualProofTo(bytes32 proofId, uint256 debtUsdcPerLoop, uint256 virtualLoops, address outputRecipient) external nonReentrant + whenNotPaused { require(outputRecipient != address(0), "zero output"); _runVirtualProofFor( @@ -181,7 +264,7 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { bytes32 iso20022DocumentHash, bytes32 auditEnvelopeHash, bytes32 pegProofHash - ) external nonReentrant { + ) external nonReentrant whenNotPaused { require(outputRecipient != address(0), "zero output"); require(exactOutputAmount > 0, "zero exact output"); require(roundingReceiver != address(0), "zero rounding"); @@ -201,6 +284,48 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { ); } + function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) + external + override + nonReentrant + whenNotPaused + returns (bool) + { + require(token == address(usdc), "unsupported flash token"); + require(amount > 0, "zero flash amount"); + require(amount <= lenderUsdcAvailable, "insufficient lender usdc"); + require(maxFlashLoanAmount == 0 || amount <= maxFlashLoanAmount, "flash amount too high"); + require( + !flashBorrowerAllowlistEnabled || approvedFlashBorrower[address(receiver)], "flash borrower not approved" + ); + + uint256 fee = flashFee(token, amount); + uint256 balanceBefore = usdc.balanceOf(address(this)); + require(balanceBefore >= poolUsdcReserve + lenderUsdcAvailable, "accounting undercollateralized"); + + lenderUsdcAvailable -= amount; + usdc.safeTransfer(address(receiver), amount); + + bytes32 retval = receiver.onFlashLoan(msg.sender, token, amount, fee, data); + require(retval == _FLASH_RETURN_VALUE, "invalid flash callback"); + + uint256 expectedBalance = balanceBefore + fee; + uint256 balanceAfterCallback = usdc.balanceOf(address(this)); + if (balanceAfterCallback < expectedBalance) { + usdc.safeTransferFrom(address(receiver), address(this), expectedBalance - balanceAfterCallback); + } + require(usdc.balanceOf(address(this)) >= expectedBalance, "flash repayment failed"); + + lenderUsdcAvailable += amount + fee; + totalFlashFeesCollectedUsdc += fee; + require( + usdc.balanceOf(address(this)) >= poolUsdcReserve + lenderUsdcAvailable, "accounting undercollateralized" + ); + + emit EngineXFlashLoan(msg.sender, receiver, token, amount, fee); + return true; + } + function _runVirtualProofFor( address outputRecipient, bytes32 proofId, @@ -272,10 +397,58 @@ contract DBISEngineXVirtualBatchVault is Ownable, ReentrancyGuard { } } + function withdrawPoolLiquidity(address to, uint256 cwusdcAmount, uint256 usdcAmount) + external + onlyOwner + nonReentrant + { + require(to != address(0), "zero to"); + require(cwusdcAmount > 0 || usdcAmount > 0, "zero withdraw"); + require(cwusdcAmount <= poolCwusdcReserve, "insufficient pool cwusdc"); + require(usdcAmount <= poolUsdcReserve, "insufficient pool usdc"); + + uint256 nextCwusdcReserve = poolCwusdcReserve - cwusdcAmount; + uint256 nextUsdcReserve = poolUsdcReserve - usdcAmount; + require(nextCwusdcReserve == nextUsdcReserve, "would break maintained pool"); + + poolCwusdcReserve = nextCwusdcReserve; + poolUsdcReserve = nextUsdcReserve; + + if (cwusdcAmount > 0) { + cWUSDC.safeTransfer(to, cwusdcAmount); + } + if (usdcAmount > 0) { + usdc.safeTransfer(to, usdcAmount); + } + + emit PoolLiquidityWithdrawn(to, cwusdcAmount, usdcAmount); + } + + function withdrawLenderUsdc(address to, uint256 amount) external onlyOwner nonReentrant { + require(to != address(0), "zero to"); + require(amount > 0, "zero withdraw"); + require(amount <= lenderUsdcAvailable, "insufficient lender usdc"); + + lenderUsdcAvailable -= amount; + usdc.safeTransfer(to, amount); + + emit LenderUsdcWithdrawn(to, amount); + } + + /// @notice Rescue or migrate only tokens that are not backing Engine X pool/lender accounting. function withdraw(address token, address to, uint256 amount) external onlyOwner nonReentrant { require(to != address(0), "zero to"); + require(amount > 0, "zero withdraw"); IERC20(token).safeTransfer(to, amount); - emit OwnerWithdraw(token, to, amount); + _requireAccountingCollateralized(); + emit UnaccountedTokenWithdrawn(token, to, amount); + } + + function _requireAccountingCollateralized() internal view { + require(cWUSDC.balanceOf(address(this)) >= poolCwusdcReserve, "accounting undercollateralized"); + require( + usdc.balanceOf(address(this)) >= poolUsdcReserve + lenderUsdcAvailable, "accounting undercollateralized" + ); } function _getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256) { diff --git a/contracts/flash/DBISEngineXXautUsdcBorrowVault.sol b/contracts/flash/DBISEngineXXautUsdcBorrowVault.sol new file mode 100644 index 0000000..026b3b0 --- /dev/null +++ b/contracts/flash/DBISEngineXXautUsdcBorrowVault.sol @@ -0,0 +1,344 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +/// @notice Engine X XAUt-backed USDC borrowing vault. +/// @dev This vault lends only pre-funded USDC. cWUSDC can be referenced as proof +/// provenance, but debt is always repaid in official USDC. +contract DBISEngineXXautUsdcBorrowVault is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + uint256 public constant BPS = 10_000; + uint256 public constant MAX_LTV_BPS = 9_500; + uint256 public constant MAX_LIQUIDATION_THRESHOLD_BPS = 9_800; + uint256 public constant MAX_LIQUIDATION_BONUS_BPS = 2_000; + + IERC20 public immutable xaut; + IERC20 public immutable usdc; + IERC20 public immutable cWUSDC; + + uint256 public xautUsdPrice6; + uint256 public lastPriceUpdate; + bytes32 public lastPriceSourceHash; + + uint256 public ltvBps; + uint256 public liquidationThresholdBps; + uint256 public minHealthFactorBps; + uint256 public liquidationBonusBps; + uint256 public maxBorrowUsdc; + bool public paused; + + uint256 public lenderUsdcAvailable; + uint256 public totalCollateralXaut; + uint256 public totalDebtUsdc; + uint256 public totalCwusdcProofRepayUsdc; + + struct Position { + uint256 collateralXaut; + uint256 debtUsdc; + } + + mapping(address => Position) public positions; + + event PriceUpdated(uint256 price6, bytes32 indexed sourceHash); + event RiskParamsUpdated( + uint256 ltvBps, + uint256 liquidationThresholdBps, + uint256 minHealthFactorBps, + uint256 liquidationBonusBps, + uint256 maxBorrowUsdc + ); + event Paused(address indexed operator); + event Unpaused(address indexed operator); + event LenderFunded(address indexed funder, uint256 amount); + event LenderUsdcWithdrawn(address indexed to, uint256 amount); + event CollateralSupplied(address indexed account, uint256 amount); + event CollateralWithdrawn(address indexed account, address indexed to, uint256 amount); + event UsdcBorrowed(address indexed account, address indexed to, uint256 amount, uint256 debtAfter); + event UsdcRepaid(address indexed account, address indexed payer, uint256 amount, uint256 debtAfter); + event CwusdcSourcedRepay( + address indexed account, + address indexed payer, + uint256 amount, + bytes32 indexed publicSwapTxHash, + bytes32 iso20022DocumentHash, + bytes32 auditEnvelopeHash, + bytes32 pegProofHash + ); + event Liquidated( + address indexed account, + address indexed liquidator, + uint256 repayUsdc, + uint256 seizedXaut, + uint256 debtAfter + ); + event UnaccountedTokenWithdrawn(address indexed token, address indexed to, uint256 amount); + + modifier whenNotPaused() { + require(!paused, "paused"); + _; + } + + constructor( + address xaut_, + address usdc_, + address cWUSDC_, + address owner_, + uint256 xautUsdPrice6_, + uint256 ltvBps_, + uint256 liquidationThresholdBps_, + uint256 minHealthFactorBps_, + uint256 liquidationBonusBps_, + uint256 maxBorrowUsdc_, + bytes32 priceSourceHash_ + ) Ownable(owner_) { + require(xaut_ != address(0) && usdc_ != address(0) && cWUSDC_ != address(0), "zero token"); + require(owner_ != address(0), "zero owner"); + xaut = IERC20(xaut_); + usdc = IERC20(usdc_); + cWUSDC = IERC20(cWUSDC_); + _setPrice(xautUsdPrice6_, priceSourceHash_); + _setRiskParams( + ltvBps_, liquidationThresholdBps_, minHealthFactorBps_, liquidationBonusBps_, maxBorrowUsdc_ + ); + } + + function pause() external onlyOwner { + paused = true; + emit Paused(msg.sender); + } + + function unpause() external onlyOwner { + paused = false; + emit Unpaused(msg.sender); + } + + function setXautUsdPrice6(uint256 price6, bytes32 sourceHash) external onlyOwner { + _setPrice(price6, sourceHash); + } + + function setRiskParams( + uint256 ltvBps_, + uint256 liquidationThresholdBps_, + uint256 minHealthFactorBps_, + uint256 liquidationBonusBps_, + uint256 maxBorrowUsdc_ + ) external onlyOwner { + _setRiskParams( + ltvBps_, liquidationThresholdBps_, minHealthFactorBps_, liquidationBonusBps_, maxBorrowUsdc_ + ); + } + + function fundLender(uint256 amount) external nonReentrant whenNotPaused { + require(amount > 0, "zero fund"); + usdc.safeTransferFrom(msg.sender, address(this), amount); + lenderUsdcAvailable += amount; + emit LenderFunded(msg.sender, amount); + } + + function withdrawLenderUsdc(address to, uint256 amount) external onlyOwner nonReentrant { + require(to != address(0), "zero to"); + require(amount > 0, "zero withdraw"); + require(amount <= lenderUsdcAvailable, "insufficient lender usdc"); + lenderUsdcAvailable -= amount; + usdc.safeTransfer(to, amount); + emit LenderUsdcWithdrawn(to, amount); + } + + function supplyCollateral(uint256 amount) external nonReentrant whenNotPaused { + require(amount > 0, "zero collateral"); + xaut.safeTransferFrom(msg.sender, address(this), amount); + positions[msg.sender].collateralXaut += amount; + totalCollateralXaut += amount; + emit CollateralSupplied(msg.sender, amount); + } + + function withdrawCollateral(uint256 amount, address to) external nonReentrant whenNotPaused { + require(to != address(0), "zero to"); + require(amount > 0, "zero collateral"); + Position storage position = positions[msg.sender]; + require(amount <= position.collateralXaut, "insufficient collateral"); + position.collateralXaut -= amount; + totalCollateralXaut -= amount; + _requireHealthy(msg.sender); + xaut.safeTransfer(to, amount); + emit CollateralWithdrawn(msg.sender, to, amount); + } + + function borrowUsdc(uint256 amount, address to) external nonReentrant whenNotPaused { + require(to != address(0), "zero to"); + require(amount > 0, "zero borrow"); + require(amount <= lenderUsdcAvailable, "insufficient lender usdc"); + if (maxBorrowUsdc != 0) { + require(totalDebtUsdc + amount <= maxBorrowUsdc, "max borrow exceeded"); + } + + Position storage position = positions[msg.sender]; + position.debtUsdc += amount; + totalDebtUsdc += amount; + lenderUsdcAvailable -= amount; + _requireHealthy(msg.sender); + + usdc.safeTransfer(to, amount); + emit UsdcBorrowed(msg.sender, to, amount, position.debtUsdc); + } + + function repayUsdc(uint256 amount) external nonReentrant whenNotPaused returns (uint256 repaid) { + repaid = _repay(msg.sender, msg.sender, amount); + } + + function repayUsdcFor(address account, uint256 amount) external nonReentrant whenNotPaused returns (uint256 repaid) { + require(account != address(0), "zero account"); + repaid = _repay(account, msg.sender, amount); + } + + function repayUsdcFromCwusdcProof( + uint256 amount, + bytes32 publicSwapTxHash, + bytes32 iso20022DocumentHash, + bytes32 auditEnvelopeHash, + bytes32 pegProofHash + ) external nonReentrant whenNotPaused returns (uint256 repaid) { + require(publicSwapTxHash != bytes32(0), "zero swap hash"); + require(iso20022DocumentHash != bytes32(0), "zero iso hash"); + require(auditEnvelopeHash != bytes32(0), "zero audit hash"); + require(pegProofHash != bytes32(0), "zero peg hash"); + + repaid = _repay(msg.sender, msg.sender, amount); + totalCwusdcProofRepayUsdc += repaid; + emit CwusdcSourcedRepay( + msg.sender, msg.sender, repaid, publicSwapTxHash, iso20022DocumentHash, auditEnvelopeHash, pegProofHash + ); + } + + function liquidate(address account, uint256 repayAmount) external nonReentrant whenNotPaused returns (uint256 seized) { + require(account != address(0), "zero account"); + require(repayAmount > 0, "zero repay"); + require(healthFactorBps(account) < minHealthFactorBps, "position healthy"); + + Position storage position = positions[account]; + uint256 repaid = repayAmount > position.debtUsdc ? position.debtUsdc : repayAmount; + require(repaid > 0, "zero debt"); + seized = _xautForUsdcWithBonus(repaid); + require(seized <= position.collateralXaut, "insufficient collateral"); + + usdc.safeTransferFrom(msg.sender, address(this), repaid); + position.debtUsdc -= repaid; + totalDebtUsdc -= repaid; + lenderUsdcAvailable += repaid; + position.collateralXaut -= seized; + totalCollateralXaut -= seized; + xaut.safeTransfer(msg.sender, seized); + + emit Liquidated(account, msg.sender, repaid, seized, position.debtUsdc); + } + + function rescueUnaccountedToken(address token, address to, uint256 amount) external onlyOwner nonReentrant { + require(to != address(0), "zero to"); + require(amount > 0, "zero withdraw"); + IERC20(token).safeTransfer(to, amount); + _requireAccountingCollateralized(); + emit UnaccountedTokenWithdrawn(token, to, amount); + } + + function collateralValueUsd6(address account) public view returns (uint256) { + return collateralValueUsd6ForRaw(positions[account].collateralXaut); + } + + function collateralValueUsd6ForRaw(uint256 xautRaw) public view returns (uint256) { + return (xautRaw * xautUsdPrice6) / 1e6; + } + + function maxDebtForCollateral(uint256 xautRaw) public view returns (uint256) { + uint256 collateralUsd6 = collateralValueUsd6ForRaw(xautRaw); + uint256 byLtv = (collateralUsd6 * ltvBps) / BPS; + uint256 byHealth = (collateralUsd6 * liquidationThresholdBps) / minHealthFactorBps; + return byLtv < byHealth ? byLtv : byHealth; + } + + function maxAdditionalBorrow(address account) public view returns (uint256) { + uint256 maxDebt = maxDebtForCollateral(positions[account].collateralXaut); + if (positions[account].debtUsdc >= maxDebt) { + return 0; + } + return maxDebt - positions[account].debtUsdc; + } + + function healthFactorBps(address account) public view returns (uint256) { + Position memory position = positions[account]; + if (position.debtUsdc == 0) { + return type(uint256).max; + } + uint256 collateralUsd6 = collateralValueUsd6ForRaw(position.collateralXaut); + return (collateralUsd6 * liquidationThresholdBps) / position.debtUsdc; + } + + function _repay(address account, address payer, uint256 amount) internal returns (uint256 repaid) { + require(amount > 0, "zero repay"); + Position storage position = positions[account]; + require(position.debtUsdc > 0, "zero debt"); + repaid = amount > position.debtUsdc ? position.debtUsdc : amount; + usdc.safeTransferFrom(payer, address(this), repaid); + position.debtUsdc -= repaid; + totalDebtUsdc -= repaid; + lenderUsdcAvailable += repaid; + emit UsdcRepaid(account, payer, repaid, position.debtUsdc); + } + + function _requireHealthy(address account) internal view { + Position memory position = positions[account]; + if (position.debtUsdc == 0) { + return; + } + require(position.debtUsdc <= maxDebtForCollateral(position.collateralXaut), "exceeds collateral"); + require(healthFactorBps(account) >= minHealthFactorBps, "health too low"); + } + + function _xautForUsdcWithBonus(uint256 usdcAmount) internal view returns (uint256) { + uint256 numerator = usdcAmount * 1e6 * (BPS + liquidationBonusBps); + uint256 denominator = xautUsdPrice6 * BPS; + return (numerator + denominator - 1) / denominator; + } + + function _setPrice(uint256 price6, bytes32 sourceHash) internal { + require(price6 > 0, "zero price"); + require(sourceHash != bytes32(0), "zero source"); + xautUsdPrice6 = price6; + lastPriceUpdate = block.timestamp; + lastPriceSourceHash = sourceHash; + emit PriceUpdated(price6, sourceHash); + } + + function _setRiskParams( + uint256 ltvBps_, + uint256 liquidationThresholdBps_, + uint256 minHealthFactorBps_, + uint256 liquidationBonusBps_, + uint256 maxBorrowUsdc_ + ) internal { + require(ltvBps_ > 0 && ltvBps_ <= MAX_LTV_BPS, "bad ltv"); + require( + liquidationThresholdBps_ >= ltvBps_ && liquidationThresholdBps_ <= MAX_LIQUIDATION_THRESHOLD_BPS, + "bad threshold" + ); + require(minHealthFactorBps_ >= BPS, "bad health"); + require(liquidationBonusBps_ <= MAX_LIQUIDATION_BONUS_BPS, "bad bonus"); + ltvBps = ltvBps_; + liquidationThresholdBps = liquidationThresholdBps_; + minHealthFactorBps = minHealthFactorBps_; + liquidationBonusBps = liquidationBonusBps_; + maxBorrowUsdc = maxBorrowUsdc_; + emit RiskParamsUpdated( + ltvBps_, liquidationThresholdBps_, minHealthFactorBps_, liquidationBonusBps_, maxBorrowUsdc_ + ); + } + + function _requireAccountingCollateralized() internal view { + require(xaut.balanceOf(address(this)) >= totalCollateralXaut, "xaut undercollateralized"); + require(usdc.balanceOf(address(this)) >= lenderUsdcAvailable, "usdc undercollateralized"); + } +} diff --git a/docs/deployment/MULTI_CHAIN_DEPLOYMENT_GUIDE.md b/docs/deployment/MULTI_CHAIN_DEPLOYMENT_GUIDE.md index 569814d..795df11 100644 --- a/docs/deployment/MULTI_CHAIN_DEPLOYMENT_GUIDE.md +++ b/docs/deployment/MULTI_CHAIN_DEPLOYMENT_GUIDE.md @@ -1,7 +1,7 @@ # 🌐 Multi-Chain Deployment Guide - Complete Package **Version**: 1.0 -**Last Updated**: 2026-01-24 +**Last Updated**: 2026-05-09 **Status**: ✅ Foundation Complete - Ready for Expansion --- @@ -80,7 +80,7 @@ This guide covers the complete deployment of the Universal Cross-Chain Asset Hub - [ ] Hedera adapter + oracle service - [ ] Tron adapter + oracle service - [ ] TON adapter + oracle service -- [ ] Cosmos adapter (IBC integration) +- [ ] Cosmos adapter (IBC integration) — optional streams **A–E**: [COSMOS_ECOSYSTEM_CHAIN138_OPTIONAL_INTEGRATIONS_RUNBOOK.md](../../../docs/11-references/COSMOS_ECOSYSTEM_CHAIN138_OPTIONAL_INTEGRATIONS_RUNBOOK.md), [`config/cosmos-chain138-optional/README.md`](../../../config/cosmos-chain138-optional/README.md) - [ ] Solana adapter (Wormhole integration) ### **Phase 4: Hyperledger** ⚠️ diff --git a/docs/deployment/REMAINING_TASKS_COMPLETE_LIST.md b/docs/deployment/REMAINING_TASKS_COMPLETE_LIST.md index 621c2bf..df9e77c 100644 --- a/docs/deployment/REMAINING_TASKS_COMPLETE_LIST.md +++ b/docs/deployment/REMAINING_TASKS_COMPLETE_LIST.md @@ -83,7 +83,7 @@ - [ ] **BRG-DEP-002**: Deploy WETH10 bridge to ChainID 138 - 2-3h - [ ] **BRG-DEP-003**: Deploy LINK token to canonical address - 2-3h - [ ] **BRG-DEP-004**: Deploy remaining EVM adapters (Polygon, Arbitrum, Optimism, Base, Avalanche, BSC, Ethereum) - 10-15h -- [ ] **BRG-DEP-005**: Deploy remaining non-EVM adapters (Stellar, Algorand, Hedera, Tron, TON, Cosmos, Solana) - 20-30h +- [ ] **BRG-DEP-005**: Deploy remaining non-EVM adapters (Stellar, Algorand, Hedera, Tron, TON, Cosmos, Solana) - 20-30h — **Cosmos:** optional streams A–E (not production IBC today): [COSMOS_ECOSYSTEM_CHAIN138_OPTIONAL_INTEGRATIONS_RUNBOOK.md](../../../docs/11-references/COSMOS_ECOSYSTEM_CHAIN138_OPTIONAL_INTEGRATIONS_RUNBOOK.md), [`config/cosmos-chain138-optional/README.md`](../../../config/cosmos-chain138-optional/README.md) - [ ] **BRG-DEP-006**: Deploy Hyperledger components (Cacti, Fabric, Indy) - 15-20h --- diff --git a/metamask/verified-contracts.json b/metamask/verified-contracts.json index d2c7d75..a21b2ab 100644 --- a/metamask/verified-contracts.json +++ b/metamask/verified-contracts.json @@ -5,63 +5,63 @@ "name": "WETH", "address": "0x0000000000000000000000000000000000000000", "verified": false, - "explorer": "https://explorer.d-bis.org/address/0x0000000000000000000000000000000000000000", + "explorer": "https://explorer.d-bis.org/addresses/0x0000000000000000000000000000000000000000", "source": "contracts/tokens/WETH.sol" }, { "name": "Multicall", "address": "0x0000000000000000000000000000000000000000", "verified": false, - "explorer": "https://explorer.d-bis.org/address/0x0000000000000000000000000000000000000000", + "explorer": "https://explorer.d-bis.org/addresses/0x0000000000000000000000000000000000000000", "source": "contracts/utils/Multicall.sol" }, { "name": "CREATE2Factory", "address": "0x0000000000000000000000000000000000000000", "verified": false, - "explorer": "https://explorer.d-bis.org/address/0x0000000000000000000000000000000000000000", + "explorer": "https://explorer.d-bis.org/addresses/0x0000000000000000000000000000000000000000", "source": "contracts/utils/CREATE2Factory.sol" }, { "name": "Aggregator", "address": "0x0000000000000000000000000000000000000000", "verified": false, - "explorer": "https://explorer.d-bis.org/address/0x0000000000000000000000000000000000000000", + "explorer": "https://explorer.d-bis.org/addresses/0x0000000000000000000000000000000000000000", "source": "contracts/oracle/Aggregator.sol" }, { "name": "CCIPRouter", "address": "0x0000000000000000000000000000000000000000", "verified": false, - "explorer": "https://explorer.d-bis.org/address/0x0000000000000000000000000000000000000000", + "explorer": "https://explorer.d-bis.org/addresses/0x0000000000000000000000000000000000000000", "source": "contracts/ccip/CCIPRouter.sol" }, { "name": "CCIPSender", "address": "0x0000000000000000000000000000000000000000", "verified": false, - "explorer": "https://explorer.d-bis.org/address/0x0000000000000000000000000000000000000000", + "explorer": "https://explorer.d-bis.org/addresses/0x0000000000000000000000000000000000000000", "source": "contracts/ccip/CCIPSender.sol" }, { "name": "CCIPReceiver", "address": "0x0000000000000000000000000000000000000000", "verified": false, - "explorer": "https://explorer.d-bis.org/address/0x0000000000000000000000000000000000000000", + "explorer": "https://explorer.d-bis.org/addresses/0x0000000000000000000000000000000000000000", "source": "contracts/ccip/CCIPReceiver.sol" }, { "name": "Voting", "address": "0x0000000000000000000000000000000000000000", "verified": false, - "explorer": "https://explorer.d-bis.org/address/0x0000000000000000000000000000000000000000", + "explorer": "https://explorer.d-bis.org/addresses/0x0000000000000000000000000000000000000000", "source": "contracts/governance/Voting.sol" }, { "name": "MultiSig", "address": "0x0000000000000000000000000000000000000000", "verified": false, - "explorer": "https://explorer.d-bis.org/address/0x0000000000000000000000000000000000000000", + "explorer": "https://explorer.d-bis.org/addresses/0x0000000000000000000000000000000000000000", "source": "contracts/governance/MultiSig.sol" } ], diff --git a/scripts/deployment/deploy-dapp-lxc.sh b/scripts/deployment/deploy-dapp-lxc.sh index ab6ca76..3500117 100755 --- a/scripts/deployment/deploy-dapp-lxc.sh +++ b/scripts/deployment/deploy-dapp-lxc.sh @@ -3,10 +3,12 @@ # Usage: ./scripts/deployment/deploy-dapp-lxc.sh [--dry-run] [--skip-create] # --dry-run Print commands only. # --skip-create Use existing container 5801 (only install/build/serve). -# Env: PROXMOX_HOST (optional; if unset, run pct on current host), NODE (optional; pct --node), +# Env: PROXMOX_HOST (defaults from VMID via proxmox scripts/lib/load-project-env.sh get_host_for_vmid), +# NODE (optional; only with DEPLOY_PCT_ON_LOCAL_PVE=1 on a PVE node: pct --node), # VMID, HOSTNAME, IP_DAPP_LXC, TEMPLATE, STORAGE, NETWORK, MEMORY_MB, CORES, DISK_GB, # REPO_URL (git URL to clone) or REPO_PATH (local path to copy; no git in container), # ENV_FILE (path to .env for VITE_*). +# DEPLOY_PCT_ON_LOCAL_PVE=1 — only on a Proxmox hypervisor: run pct locally (no SSH). # See: docs/03-deployment/DAPP_LXC_DEPLOYMENT.md set -euo pipefail @@ -48,6 +50,16 @@ for a in "$@"; do [[ "$a" == "--skip-create" ]] && SKIP_CREATE=true done +PROXMOX_MONOREPO_ROOT="$(cd "$SMOM_ROOT/.." && pwd)" +if [[ -f "$PROXMOX_MONOREPO_ROOT/scripts/lib/require-proxmox-ssh-for-pct.sh" ]]; then + # shellcheck disable=SC1091 + source "$PROXMOX_MONOREPO_ROOT/scripts/lib/require-proxmox-ssh-for-pct.sh" + require_proxmox_ssh_for_pct || exit 1 +else + echo "ERROR: Missing $PROXMOX_MONOREPO_ROOT/scripts/lib/require-proxmox-ssh-for-pct.sh (run from proxmox monorepo checkout)." >&2 + exit 1 +fi + run_cmd() { if [[ -n "$PROXMOX_HOST" ]]; then ssh $SSH_OPTS root@"$PROXMOX_HOST" "$@" @@ -76,7 +88,7 @@ echo "" if ! $SKIP_CREATE; then if $DRY_RUN; then - echo "[DRY-RUN] Would create LXC $VMID on ${PROXMOX_HOST:-local} with hostname=$HOSTNAME, ip=$IP/24" + echo "[DRY-RUN] Would create LXC $VMID on ${PROXMOX_HOST:-this Proxmox node (local pct)} with hostname=$HOSTNAME, ip=$IP/24" exit 0 fi diff --git a/scripts/deployment/export-cronos-verification-sources.sh b/scripts/deployment/export-cronos-verification-sources.sh index 50a7ebd..f3aa0f6 100755 --- a/scripts/deployment/export-cronos-verification-sources.sh +++ b/scripts/deployment/export-cronos-verification-sources.sh @@ -7,6 +7,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" OUT_DIR="$PROJECT_ROOT/.cronos-verify" cd "$PROJECT_ROOT" +# Cronos mainnet requires Paris EVM bytecode (no PUSH0). Match `foundry.toml` profile `cronos_legacy`. +export FOUNDRY_PROFILE="${FOUNDRY_PROFILE:-cronos_legacy}" # Load .env via dotenv (RPC CR/LF trim). Fallback: raw source. if [[ -f "$SCRIPT_DIR/../lib/deployment/dotenv.sh" ]]; then # shellcheck disable=SC1090 @@ -68,5 +70,7 @@ echo "Next: https://explorer.cronos.org/verifyContract" echo " 1. Select 'Solidity (Standard-Json-Input)'" echo " 2. Upload the *_standard_input.json file for each contract" echo " 3. See docs/deployment/CRONOS_VERIFICATION_RUNBOOK.md" +echo " 4. The verify form uses Google reCAPTCHA — Submit stays disabled until solved in the browser." +echo " 5. Compiler picker may list 0.8.21+ only; Foundry uses solc 0.8.20 by default — pick the version that matches deploy bytecode if verification fails." echo "" echo "Sources: $OUT_DIR/" diff --git a/services/token-aggregation/.env.example b/services/token-aggregation/.env.example index aa53b48..5c37376 100644 --- a/services/token-aggregation/.env.example +++ b/services/token-aggregation/.env.example @@ -26,8 +26,8 @@ CUSDC_ADDRESS_138=0xf22258f57794CC8E06237084b353Ab30fFfa640b # Compliant USD V2 (ERC-2612 / ERC-3009) — x402 / GRU transport. # Reference: docs/04-configuration/CHAIN138_X402_TOKEN_SUPPORT.md -CUSDT_V2_ADDRESS_138=0x8d342d321DdEe97D0c5011DAF8ca0B59DA617D29 -CUSDC_V2_ADDRESS_138=0x1ac3F4942a71E86A9682D91837E1E71b7BACdF99 +CUSDT_V2_ADDRESS_138=0x9FBfab33882Efe0038DAa608185718b772EE5660 +CUSDC_V2_ADDRESS_138=0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d # Live ALL Mainnet AUSDT compliant landing surface on Chain 138. CAUSDT_ADDRESS_138=0x5fdDF65733e3d590463F68f93Cf16E8c04081271 diff --git a/services/token-aggregation/config/live-uniswap-v2-pool-catalog.json b/services/token-aggregation/config/live-uniswap-v2-pool-catalog.json new file mode 100644 index 0000000..7716ea8 --- /dev/null +++ b/services/token-aggregation/config/live-uniswap-v2-pool-catalog.json @@ -0,0 +1,468 @@ +{ + "schema": "token-aggregation-live-uniswap-v2-pool-catalog/v1", + "generatedAt": "2026-05-09T05:43:40.305Z", + "sourceArtifacts": [ + "reports/extraction/promod-uniswap-v2-live-pair-discovery-latest.json", + "cross-chain-pmm-lps/config/deployment-status.json" + ], + "poolCount": 19, + "positiveLiquidityPoolCount": 19, + "pools": [ + { + "chainId": 1, + "chainName": "Ethereum Mainnet", + "poolAddress": "0x422608c5ddff909675ac2c5f872fd42f16b9287a", + "dex": "uniswap_v2", + "factoryAddress": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "routerAddress": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae", + "quoteAddress": "0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a", + "baseReserveRaw": "9900803423129", + "quoteReserveRaw": "9900803423131", + "baseReserveUnits": "9900803.423129", + "quoteReserveUnits": "9900803.423131", + "priceQuotePerBase": "1.000000000000202003808633131", + "deviationBps": "2.020038086331310000E-9", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 9900803.423129, + "reserve1Usd": 9900803.423131, + "totalLiquidityUsd": 19801606.84626 + }, + { + "chainId": 1, + "chainName": "Ethereum Mainnet", + "poolAddress": "0xc28706f899266b36bc43cc072b3a921bdf2c48d9", + "dex": "uniswap_v2", + "factoryAddress": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "routerAddress": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "baseSymbol": "cWUSDC", + "quoteSymbol": "USDC", + "baseAddress": "0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a", + "quoteAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "baseReserveRaw": "1267071063797", + "quoteReserveRaw": "2167762", + "baseReserveUnits": "1267071.063797", + "quoteReserveUnits": "2.167762", + "priceQuotePerBase": "0.000001710844846779092339761738687", + "deviationBps": "9999.982891551532209076602383", + "depthOk": false, + "parityOk": false, + "healthy": false, + "reserve0Usd": 1267071.063797, + "reserve1Usd": 2.167762, + "totalLiquidityUsd": 1267073.2315590002 + }, + { + "chainId": 10, + "chainName": "Optimism", + "poolAddress": "0xe28bff306442a8a512d2441847c27211a7c4c613", + "dex": "uniswap_v2", + "factoryAddress": "0x0c3c1c532F1e39EdF36BE9Fe0bE1410313E074Bf", + "routerAddress": "0x4A7b5Da61326A6379179b40d00F57E5bbDC962c2", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0x04b2ae3c3bb3d70df506fad8717b0fbfc78ed7e6", + "quoteAddress": "0x377a5faa3162b3fc6f4e267301a3c817bad18105", + "baseReserveRaw": "3000000000", + "quoteReserveRaw": "3000000000", + "baseReserveUnits": "3000", + "quoteReserveUnits": "3000", + "priceQuotePerBase": "1", + "deviationBps": "0", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 3000, + "reserve1Usd": 3000, + "totalLiquidityUsd": 6000 + }, + { + "chainId": 25, + "chainName": "Cronos", + "poolAddress": "0x438d8e1a8e311d2ae4b75a38e0044675fd324133", + "dex": "uniswap_v2", + "factoryAddress": "0x3B44B2a187a7b3824131F8db5a74194D0a42Fc15", + "routerAddress": "0x145863Eb42Cf62847A6Ca784e6416C1682b1b2Ae", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0x72948a7a813b60b37cd0c920c4657dbff54312b8", + "quoteAddress": "0x932566e5bb6bebf6b035b94f3de1f75f126304ec", + "baseReserveRaw": "3000000000", + "quoteReserveRaw": "3000000000", + "baseReserveUnits": "3000", + "quoteReserveUnits": "3000", + "priceQuotePerBase": "1", + "deviationBps": "0", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 3000, + "reserve1Usd": 3000, + "totalLiquidityUsd": 6000 + }, + { + "chainId": 56, + "chainName": "BSC (BNB Chain)", + "poolAddress": "0x639d7e64c6f1fc676226f20a0c42aecdd66545e8", + "dex": "uniswap_v2", + "factoryAddress": "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73", + "routerAddress": "0x10ED43C718714eb63d5aA57B78B54704E256024E", + "baseSymbol": "cWAUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0xe1a51bc037a79ab36767561b147eb41780124934", + "quoteAddress": "0x5355148c4740fcc3d7a96f05edd89ab14851206b", + "baseReserveRaw": "1500000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1500", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "0.6666666666666666666666666667", + "deviationBps": "3333.333333333333333333333333", + "depthOk": true, + "parityOk": false, + "healthy": false, + "reserve0Usd": 1500, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2500 + }, + { + "chainId": 56, + "chainName": "BSC (BNB Chain)", + "poolAddress": "0x7e308c12bd609607df9c4137e30235d5a9da2a64", + "dex": "uniswap_v2", + "factoryAddress": "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73", + "routerAddress": "0x10ED43C718714eb63d5aA57B78B54704E256024E", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0x9a1d0dbee997929ed02fd19e0e199704d20914db", + "quoteAddress": "0x5355148c4740fcc3d7a96f05edd89ab14851206b", + "baseReserveRaw": "3000000000", + "quoteReserveRaw": "3000000000", + "baseReserveUnits": "3000", + "quoteReserveUnits": "3000", + "priceQuotePerBase": "1", + "deviationBps": "0", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 3000, + "reserve1Usd": 3000, + "totalLiquidityUsd": 6000 + }, + { + "chainId": 56, + "chainName": "BSC (BNB Chain)", + "poolAddress": "0xe9b082baa73fa4dec7cb3cbd99b19d30bbfe1523", + "dex": "uniswap_v2", + "factoryAddress": "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73", + "routerAddress": "0x10ED43C718714eb63d5aA57B78B54704E256024E", + "baseSymbol": "cWAUSDT", + "quoteSymbol": "cWUSDT", + "baseAddress": "0xe1a51bc037a79ab36767561b147eb41780124934", + "quoteAddress": "0x9a1d0dbee997929ed02fd19e0e199704d20914db", + "baseReserveRaw": "1500000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1500", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "0.6666666666666666666666666667", + "deviationBps": "3333.333333333333333333333333", + "depthOk": true, + "parityOk": false, + "healthy": false, + "reserve0Usd": 1500, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2500 + }, + { + "chainId": 100, + "chainName": "Gnosis Chain", + "poolAddress": "0x064d782be0113cb427f3af0de9335c9f34a1de34", + "dex": "uniswap_v2", + "factoryAddress": "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", + "routerAddress": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0x0cb0192c056aa425c557bdead8e56c7eeabf7acf", + "quoteAddress": "0xd6969bc19b53f866c64f2148ae271b2dae0c58e4", + "baseReserveRaw": "4000000000", + "quoteReserveRaw": "4000000000", + "baseReserveUnits": "4000", + "quoteReserveUnits": "4000", + "priceQuotePerBase": "1", + "deviationBps": "0", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 4000, + "reserve1Usd": 4000, + "totalLiquidityUsd": 8000 + }, + { + "chainId": 137, + "chainName": "Polygon", + "poolAddress": "0x3411a20c39773d1a18cb53864893b236f41f1e99", + "dex": "uniswap_v2", + "factoryAddress": "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32", + "routerAddress": "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0x0cb0192c056aa425c557bdead8e56c7eeabf7acf", + "quoteAddress": "0xd6969bc19b53f866c64f2148ae271b2dae0c58e4", + "baseReserveRaw": "10994302668", + "quoteReserveRaw": "10996392462", + "baseReserveUnits": "10994.302668", + "quoteReserveUnits": "10996.392462", + "priceQuotePerBase": "1.000190079722480494476414209", + "deviationBps": "1.900797224804944764142090000", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 10994.302668, + "reserve1Usd": 10996.392462, + "totalLiquidityUsd": 21990.69513 + }, + { + "chainId": 137, + "chainName": "Polygon", + "poolAddress": "0x8cd2cb42b81f894eb10d15446db22a3b31d6fb2e", + "dex": "uniswap_v2", + "factoryAddress": "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32", + "routerAddress": "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff", + "baseSymbol": "cWAUSDT", + "quoteSymbol": "cWUSDT", + "baseAddress": "0xf12e262f85107df26741726b074606cafa24aae7", + "quoteAddress": "0x0cb0192c056aa425c557bdead8e56c7eeabf7acf", + "baseReserveRaw": "1500000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1500", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "0.6666666666666666666666666667", + "deviationBps": "3333.333333333333333333333333", + "depthOk": true, + "parityOk": false, + "healthy": false, + "reserve0Usd": 1500, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2500 + }, + { + "chainId": 137, + "chainName": "Polygon", + "poolAddress": "0xe6a5cb164d4af7e9794aed09ec373392d0e7216c", + "dex": "uniswap_v2", + "factoryAddress": "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32", + "routerAddress": "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff", + "baseSymbol": "cWAUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0xf12e262f85107df26741726b074606cafa24aae7", + "quoteAddress": "0xd6969bc19b53f866c64f2148ae271b2dae0c58e4", + "baseReserveRaw": "1500000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1500", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "0.6666666666666666666666666667", + "deviationBps": "3333.333333333333333333333333", + "depthOk": true, + "parityOk": false, + "healthy": false, + "reserve0Usd": 1500, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2500 + }, + { + "chainId": 8453, + "chainName": "Base", + "poolAddress": "0x56eb93f747d3b8251d43849cc72b39c1899fcaca", + "dex": "uniswap_v2", + "factoryAddress": "0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E", + "routerAddress": "0x8cFe327CEc66d1C090Dd72bd0FF11d690C33a2Eb", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0x04b2ae3c3bb3d70df506fad8717b0fbfc78ed7e6", + "quoteAddress": "0x377a5faa3162b3fc6f4e267301a3c817bad18105", + "baseReserveRaw": "1000000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1000", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "1", + "deviationBps": "0", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 1000, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2000 + }, + { + "chainId": 42161, + "chainName": "Arbitrum One", + "poolAddress": "0x2b2ea2ea9e7617de09fcb5063befafa01a9ef2b4", + "dex": "uniswap_v2", + "factoryAddress": "0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E", + "routerAddress": "0x8cFe327CEc66d1C090Dd72bd0FF11d690C33a2Eb", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0x73adaf7dba95221c080db5631466d2bc54f6a76b", + "quoteAddress": "0x0cb0192c056aa425c557bdead8e56c7eeabf7acf", + "baseReserveRaw": "3000000000", + "quoteReserveRaw": "3000000000", + "baseReserveUnits": "3000", + "quoteReserveUnits": "3000", + "priceQuotePerBase": "1", + "deviationBps": "0", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 3000, + "reserve1Usd": 3000, + "totalLiquidityUsd": 6000 + }, + { + "chainId": 42220, + "chainName": "Celo", + "poolAddress": "0x6f97de8ab68c722dcbc02cea0ce6b587b8210052", + "dex": "uniswap_v2", + "factoryAddress": "0x62d5b84bE28a183aBB507E125B384122D2C25fAE", + "routerAddress": "0xE3D8bd6Aed4F159bc8000a9cD47CffDb95F96121", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0x73376eb92c16977b126db9112936a20fa0de3442", + "quoteAddress": "0x4c38f9a5ed68a04cd28a72e8c68c459ec34576f3", + "baseReserveRaw": "1000000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1000", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "1", + "deviationBps": "0", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 1000, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2000 + }, + { + "chainId": 42220, + "chainName": "Celo", + "poolAddress": "0xd3b55d6d7c08fdbf5f201e486992643cff410d91", + "dex": "uniswap_v2", + "factoryAddress": "0x62d5b84bE28a183aBB507E125B384122D2C25fAE", + "routerAddress": "0xE3D8bd6Aed4F159bc8000a9cD47CffDb95F96121", + "baseSymbol": "cWAUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0xc158b6cd3a3088c52f797d41f5aa02825361629e", + "quoteAddress": "0x4c38f9a5ed68a04cd28a72e8c68c459ec34576f3", + "baseReserveRaw": "1500000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1500", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "0.6666666666666666666666666667", + "deviationBps": "3333.333333333333333333333333", + "depthOk": true, + "parityOk": false, + "healthy": false, + "reserve0Usd": 1500, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2500 + }, + { + "chainId": 42220, + "chainName": "Celo", + "poolAddress": "0xee9eebf89c1424e63efc888929e43a9423357d39", + "dex": "uniswap_v2", + "factoryAddress": "0x62d5b84bE28a183aBB507E125B384122D2C25fAE", + "routerAddress": "0xE3D8bd6Aed4F159bc8000a9cD47CffDb95F96121", + "baseSymbol": "cWAUSDT", + "quoteSymbol": "cWUSDT", + "baseAddress": "0xc158b6cd3a3088c52f797d41f5aa02825361629e", + "quoteAddress": "0x73376eb92c16977b126db9112936a20fa0de3442", + "baseReserveRaw": "1500000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1500", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "0.6666666666666666666666666667", + "deviationBps": "3333.333333333333333333333333", + "depthOk": true, + "parityOk": false, + "healthy": false, + "reserve0Usd": 1500, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2500 + }, + { + "chainId": 43114, + "chainName": "Avalanche C-Chain", + "poolAddress": "0x418322f48d857277ec4bcc96bc1580accb7ea253", + "dex": "uniswap_v2", + "factoryAddress": "0x9Ad6C38BE94206cA50bb0d90783181662f0Cfa10", + "routerAddress": "0x60aE616a2155Ee3d9A68541Ba4544862310933d4", + "baseSymbol": "cWAUSDT", + "quoteSymbol": "cWUSDT", + "baseAddress": "0xff3084410a732231472ee9f93f5855da89cc5254", + "quoteAddress": "0x8142ba530b08f3950128601f00daaa678213dfdf", + "baseReserveRaw": "1500000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1500", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "0.6666666666666666666666666667", + "deviationBps": "3333.333333333333333333333333", + "depthOk": true, + "parityOk": false, + "healthy": false, + "reserve0Usd": 1500, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2500 + }, + { + "chainId": 43114, + "chainName": "Avalanche C-Chain", + "poolAddress": "0x79c8ea153e77bc69b989f59f69bfa44c466d5dee", + "dex": "uniswap_v2", + "factoryAddress": "0x9Ad6C38BE94206cA50bb0d90783181662f0Cfa10", + "routerAddress": "0x60aE616a2155Ee3d9A68541Ba4544862310933d4", + "baseSymbol": "cWUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0x8142ba530b08f3950128601f00daaa678213dfdf", + "quoteAddress": "0x0c242b513008cd49c89078f5afb237a3112251eb", + "baseReserveRaw": "10000800000", + "quoteReserveRaw": "10000800000", + "baseReserveUnits": "10000.8", + "quoteReserveUnits": "10000.8", + "priceQuotePerBase": "1", + "deviationBps": "0", + "depthOk": true, + "parityOk": true, + "healthy": true, + "reserve0Usd": 10000.8, + "reserve1Usd": 10000.8, + "totalLiquidityUsd": 20001.6 + }, + { + "chainId": 43114, + "chainName": "Avalanche C-Chain", + "poolAddress": "0xaad6aed8d28b0195d19b4d17f8ee9a1837ff2dce", + "dex": "uniswap_v2", + "factoryAddress": "0x9Ad6C38BE94206cA50bb0d90783181662f0Cfa10", + "routerAddress": "0x60aE616a2155Ee3d9A68541Ba4544862310933d4", + "baseSymbol": "cWAUSDT", + "quoteSymbol": "cWUSDC", + "baseAddress": "0xff3084410a732231472ee9f93f5855da89cc5254", + "quoteAddress": "0x0c242b513008cd49c89078f5afb237a3112251eb", + "baseReserveRaw": "1500000000", + "quoteReserveRaw": "1000000000", + "baseReserveUnits": "1500", + "quoteReserveUnits": "1000", + "priceQuotePerBase": "0.6666666666666666666666666667", + "deviationBps": "3333.333333333333333333333333", + "depthOk": true, + "parityOk": false, + "healthy": false, + "reserve0Usd": 1500, + "reserve1Usd": 1000, + "totalLiquidityUsd": 2500 + } + ] +} diff --git a/services/token-aggregation/config/supply-proof-catalog.json b/services/token-aggregation/config/supply-proof-catalog.json new file mode 100644 index 0000000..96861f3 --- /dev/null +++ b/services/token-aggregation/config/supply-proof-catalog.json @@ -0,0 +1,2113 @@ +{ + "schema": "token-aggregation-supply-proof-catalog/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "proofs": [ + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 1, + "name": "Ethereum Mainnet", + "referenceBlock": 25054993 + }, + "token": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "decimals": 6, + "totalSupplyRaw": "55047993783402955", + "totalSupplyUnits": "55047993783.402955" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "55047993783.402955", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 1, + "name": "Ethereum Mainnet", + "referenceBlock": 25054993 + }, + "token": { + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "Tether USD", + "onchainSymbol": "USDT", + "decimals": 6, + "totalSupplyRaw": "97071875962553520", + "totalSupplyUnits": "97071875962.55352" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "97071875962.55352", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "mainnet-cwusdc-supply-proof/v1", + "generatedAt": "2026-05-08T03:16:54Z", + "network": { + "chainId": 1, + "name": "Ethereum Mainnet", + "referenceBlock": 25047586 + }, + "token": { + "address": "0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a", + "name": "Wrapped cUSDC", + "symbol": "cWUSDC", + "decimals": 6, + "verifiedSource": { + "etherscan": "https://etherscan.io/token/0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a", + "sourcifyFullMatch": true, + "compiler": "0.8.20+commit.a1b79de6" + }, + "totalSupplyRaw": "10451316981309788", + "totalSupplyUnits": "10451316981.309788" + }, + "knownBalances": { + "operator": { + "address": "0x4A666F96fC8764181194447A7dFdb7d471b301C8", + "balanceRaw": "56772617595692", + "balanceUnits": "56772617.595692" + }, + "engineXVirtualBatchVault": { + "address": "0xf108586d1FC330EA1D4EA4ff8fd983cde94279B1", + "balanceRaw": "0", + "balanceUnits": "0" + }, + "uniswapV3CwusdcUsdcPool": { + "address": "0x1Cf2e685682C7F7beF508F0Af15Dfb5CDda01ee3", + "balanceRaw": "183290270", + "balanceUnits": "183.290270" + }, + "uniswapV2CwusdcUsdcPair": { + "address": "0xC28706F899266b36BC43cc072b3a921BDf2C48D9", + "balanceRaw": "1266860669082", + "balanceUnits": "1266860.669082" + } + }, + "trackerSurfaces": { + "etherscan": { + "status": "verified_contract_page_live", + "url": "https://etherscan.io/token/0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a" + }, + "dexscreener": { + "status": "indexed", + "tokenUrl": "https://dexscreener.com/ethereum/0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a", + "pools": [ + "https://dexscreener.com/ethereum/0x1cf2e685682c7f7bef508f0af15dfb5cdda01ee3", + "https://dexscreener.com/ethereum/0xc28706f899266b36bc43cc072b3a921bdf2c48d9" + ] + }, + "geckoTerminal": { + "status": "indexed", + "pools": [ + { + "address": "0x1cf2e685682c7f7bef508f0af15dfb5cdda01ee3", + "url": "https://www.geckoterminal.com/eth/pools/0x1cf2e685682c7f7bef508f0af15dfb5cdda01ee3", + "reserveUsd": "179.1416", + "volume24hUsd": "90.6693898501" + }, + { + "address": "0xc28706f899266b36bc43cc072b3a921bdf2c48d9", + "url": "https://www.geckoterminal.com/eth/pools/0xc28706f899266b36bc43cc072b3a921bdf2c48d9", + "reserveUsd": "4.3241", + "volume24hUsd": "114770.233085417" + } + ] + }, + "coinMarketCapDex": { + "status": "token_page_reachable", + "url": "https://dex.coinmarketcap.com/token/ethereum/0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a/", + "note": "CMC DEX discoverability is not the same as full CoinMarketCap listing acceptance." + }, + "coinGecko": { + "status": "not_listed_by_contract_api", + "note": "CoinGecko Ethereum contract lookup returned 404 during the 2026-05-08 readiness pass." + } + }, + "circulatingSupplyMethodology": { + "status": "ready_for_tracker_review", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "10451316981.309788", + "notes": [ + "No public tracker has accepted a circulating-supply value for this contract yet.", + "If a tracker requires exclusion of operator, treasury, bridge, or protocol-controlled balances, use the knownBalances section plus any additional signed treasury inventory supplied at submission time.", + "The value above is an on-chain supply proof, not a third-party listing approval." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 1, + "name": "Ethereum Mainnet", + "referenceBlock": 25054993 + }, + "token": { + "address": "0xaF5017d0163ecb99D9B5D94e3b4D7b09Af44D8AE", + "name": "Tether USD (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDT", + "decimals": 6, + "totalSupplyRaw": "136106860161405", + "totalSupplyUnits": "136106860.161405" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "136106860.161405", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 10, + "name": "Optimism", + "referenceBlock": 151350237 + }, + "token": { + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD Coin", + "onchainSymbol": "USDC", + "decimals": 6, + "totalSupplyRaw": "216872376232530", + "totalSupplyUnits": "216872376.23253" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "216872376.23253", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 10, + "name": "Optimism", + "referenceBlock": 151350239 + }, + "token": { + "address": "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "Tether USD", + "onchainSymbol": "USDT", + "decimals": 6, + "totalSupplyRaw": "201850485038093", + "totalSupplyUnits": "201850485.038093" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "201850485.038093", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 10, + "name": "Optimism", + "referenceBlock": 151350241 + }, + "token": { + "address": "0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105", + "name": "USD Coin (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDC", + "onchainName": "Wrapped cUSDC", + "onchainSymbol": "cWUSDC", + "decimals": 6, + "totalSupplyRaw": "16001000000", + "totalSupplyUnits": "16001.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "16001.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 10, + "name": "Optimism", + "referenceBlock": 151350241 + }, + "token": { + "address": "0x04B2AE3c3bb3d70Df506FAd8717b0FBFC78ED7E6", + "name": "Tether USD (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDT", + "onchainName": "Wrapped cUSDT", + "onchainSymbol": "cWUSDT", + "decimals": 6, + "totalSupplyRaw": "16001000000", + "totalSupplyUnits": "16001.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "16001.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 25, + "name": "Cronos", + "referenceBlock": 70089434 + }, + "token": { + "address": "0xc21223249CA28397B4B6541dfFaEcC539BfF0c59", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD Coin", + "onchainSymbol": "USDC", + "decimals": 6, + "totalSupplyRaw": "179445548315541", + "totalSupplyUnits": "179445548.315541" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "179445548.315541", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 25, + "name": "Cronos", + "referenceBlock": 70089340 + }, + "token": { + "address": "0x66e428c3f67a68878562e79A0234c1F83c208770", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "Tether USD", + "onchainSymbol": "USDT", + "decimals": 6, + "totalSupplyRaw": "89024964111949", + "totalSupplyUnits": "89024964.111949" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "89024964.111949", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 56, + "name": "BSC (BNB Chain)", + "referenceBlock": 97212275 + }, + "token": { + "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD Coin", + "onchainSymbol": "USDC", + "decimals": 18, + "totalSupplyRaw": "1288999879439803987446539541", + "totalSupplyUnits": "1288999879.439803987446539541" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "1288999879.439803987446539541", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 56, + "name": "BSC (BNB Chain)", + "referenceBlock": 97212284 + }, + "token": { + "address": "0x55d398326f99059fF775485246999027B3197955", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "Tether USD", + "onchainSymbol": "USDT", + "decimals": 18, + "totalSupplyRaw": "9184992541113839747386612540", + "totalSupplyUnits": "9184992541.11383974738661254" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "9184992541.11383974738661254", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 56, + "name": "BSC (BNB Chain)", + "referenceBlock": 97212289 + }, + "token": { + "address": "0xe1a51Bc037a79AB36767561B147eb41780124934", + "name": "Alltra USD Token (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWAUSDT", + "onchainName": "Wrapped cAUSDT", + "onchainSymbol": "cWAUSDT", + "decimals": 6, + "totalSupplyRaw": "362506000000", + "totalSupplyUnits": "362506.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "362506.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 56, + "name": "BSC (BNB Chain)", + "referenceBlock": 97212291 + }, + "token": { + "address": "0x5355148C4740fcc3D7a96F05EdD89AB14851206b", + "name": "USD Coin (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDC", + "onchainName": "Wrapped cUSDC", + "onchainSymbol": "cWUSDC", + "decimals": 6, + "totalSupplyRaw": "1993283000000", + "totalSupplyUnits": "1993283.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "1993283.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 56, + "name": "BSC (BNB Chain)", + "referenceBlock": 97212294 + }, + "token": { + "address": "0x9a1D0dBEE997929ED02fD19E0E199704d20914dB", + "name": "Tether USD (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDT", + "onchainName": "Wrapped cUSDT", + "onchainSymbol": "cWUSDT", + "decimals": 6, + "totalSupplyRaw": "1993283000000", + "totalSupplyUnits": "1993283.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "1993283.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 56, + "name": "BSC (BNB Chain)", + "referenceBlock": 97212298 + }, + "token": { + "address": "0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55", + "name": "USD W (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDW", + "onchainName": "Wrapped cUSDW", + "onchainSymbol": "cWUSDW", + "decimals": 6, + "totalSupplyRaw": "361506000000", + "totalSupplyUnits": "361506.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "361506.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 100, + "name": "Gnosis Chain", + "referenceBlock": 46080039 + }, + "token": { + "address": "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD//C on xDai", + "onchainSymbol": "USDC", + "decimals": 6, + "totalSupplyRaw": "18374759200634", + "totalSupplyUnits": "18374759.200634" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "18374759.200634", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 100, + "name": "Gnosis Chain", + "referenceBlock": 46080039 + }, + "token": { + "address": "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "Tether USD on xDai", + "onchainSymbol": "USDT", + "decimals": 6, + "totalSupplyRaw": "1055804706651", + "totalSupplyUnits": "1055804.706651" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "1055804.706651", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 100, + "name": "Gnosis Chain", + "referenceBlock": 46080040 + }, + "token": { + "address": "0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4", + "name": "USD Coin (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDC", + "onchainName": "Wrapped cUSDC", + "onchainSymbol": "cWUSDC", + "decimals": 6, + "totalSupplyRaw": "15000000000", + "totalSupplyUnits": "15000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "15000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 100, + "name": "Gnosis Chain", + "referenceBlock": 46080041 + }, + "token": { + "address": "0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF", + "name": "Tether USD (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDT", + "onchainName": "Wrapped cUSDT", + "onchainSymbol": "cWUSDT", + "decimals": 6, + "totalSupplyRaw": "15000000000", + "totalSupplyUnits": "15000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "15000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 137, + "name": "Polygon", + "referenceBlock": 86605454 + }, + "token": { + "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD Coin", + "onchainSymbol": "USDC", + "decimals": 6, + "totalSupplyRaw": "695940174693096", + "totalSupplyUnits": "695940174.693096" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "695940174.693096", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 137, + "name": "Polygon", + "referenceBlock": 86605456 + }, + "token": { + "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "USDT0", + "onchainSymbol": "USDT0", + "decimals": 6, + "totalSupplyRaw": "831909856146147", + "totalSupplyUnits": "831909856.146147" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "831909856.146147", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 137, + "name": "Polygon", + "referenceBlock": 86605457 + }, + "token": { + "address": "0xf12e262F85107df26741726b074606CaFa24AAe7", + "name": "Alltra USD Token (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWAUSDT", + "onchainName": "Wrapped cAUSDT", + "onchainSymbol": "cWAUSDT", + "decimals": 6, + "totalSupplyRaw": "3000000000", + "totalSupplyUnits": "3000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "3000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 137, + "name": "Polygon", + "referenceBlock": 86605458 + }, + "token": { + "address": "0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4", + "name": "USD Coin (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDC", + "onchainName": "Wrapped cUSDC", + "onchainSymbol": "cWUSDC", + "decimals": 6, + "totalSupplyRaw": "22000000000", + "totalSupplyUnits": "22000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "22000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 137, + "name": "Polygon", + "referenceBlock": 86605459 + }, + "token": { + "address": "0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF", + "name": "Tether USD (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDT", + "onchainName": "Wrapped cUSDT", + "onchainSymbol": "cWUSDT", + "decimals": 6, + "totalSupplyRaw": "21998313972", + "totalSupplyUnits": "21998.313972" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "21998.313972", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952095 + }, + "token": { + "address": "0xD51482e567c03899eecE3CAe8a058161FD56069D", + "name": "Australian Dollar (Compliant)", + "symbol": "cAUDC", + "onchainName": "Australian Dollar (Compliant)", + "onchainSymbol": "cAUDC", + "decimals": 6, + "totalSupplyRaw": "56804486580000", + "totalSupplyUnits": "56804486.58" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "56804486.58", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952086 + }, + "token": { + "address": "0x5fdDF65733e3d590463F68f93Cf16E8c04081271", + "name": "Alltra USD Token (Compliant)", + "symbol": "cAUSDT", + "onchainName": "Alltra USD Token (Compliant)", + "onchainSymbol": "cAUSDT", + "decimals": 6, + "totalSupplyRaw": "32707131000004", + "totalSupplyUnits": "32707131.000004" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "32707131.000004", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952095 + }, + "token": { + "address": "0x54dBd40cF05e15906A2C21f600937e96787f5679", + "name": "Canadian Dollar (Compliant)", + "symbol": "cCADC", + "onchainName": "Canadian Dollar (Compliant)", + "onchainSymbol": "cCADC", + "decimals": 6, + "totalSupplyRaw": "44871934240000", + "totalSupplyUnits": "44871934.24" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "44871934.24", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952095 + }, + "token": { + "address": "0x873990849DDa5117d7C644f0aF24370797C03885", + "name": "Swiss Franc (Compliant)", + "symbol": "cCHFC", + "onchainName": "Swiss Franc (Compliant)", + "onchainSymbol": "cCHFC", + "decimals": 6, + "totalSupplyRaw": "32918682790000", + "totalSupplyUnits": "32918682.79" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "32918682.79", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952094 + }, + "token": { + "address": "0x8085961F9cF02b4d800A3c6d386D31da4B34266a", + "name": "Euro Coin (Compliant)", + "symbol": "cEURC", + "onchainName": "Euro Coin (Compliant)", + "onchainSymbol": "cEURC", + "decimals": 6, + "totalSupplyRaw": "72706988330000", + "totalSupplyUnits": "72706988.33" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "72706988.33", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952094 + }, + "token": { + "address": "0xdf4b71c61E5912712C1Bdd451416B9aC26949d72", + "name": "Tether EUR (Compliant)", + "symbol": "cEURT", + "onchainName": "Tether EUR (Compliant)", + "onchainSymbol": "cEURT", + "decimals": 6, + "totalSupplyRaw": "71235330330000", + "totalSupplyUnits": "71235330.33" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "71235330.33", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952094 + }, + "token": { + "address": "0x003960f16D9d34F2e98d62723B6721Fb92074aD2", + "name": "Pound Sterling (Compliant)", + "symbol": "cGBPC", + "onchainName": "Pound Sterling (Compliant)", + "onchainSymbol": "cGBPC", + "decimals": 6, + "totalSupplyRaw": "34076968470000", + "totalSupplyUnits": "34076968.47" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "34076968.47", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952095 + }, + "token": { + "address": "0x350f54e4D23795f86A9c03988c7135357CCaD97c", + "name": "Tether GBP (Compliant)", + "symbol": "cGBPT", + "onchainName": "Tether GBP (Compliant)", + "onchainSymbol": "cGBPT", + "decimals": 6, + "totalSupplyRaw": "75066546470000", + "totalSupplyUnits": "75066546.47" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "75066546.47", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952095 + }, + "token": { + "address": "0xEe269e1226a334182aace90056EE4ee5Cc8A6770", + "name": "Japanese Yen (Compliant)", + "symbol": "cJPYC", + "onchainName": "Japanese Yen (Compliant)", + "onchainSymbol": "cJPYC", + "decimals": 6, + "totalSupplyRaw": "91977287100000", + "totalSupplyUnits": "91977287.1" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "91977287.1", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952084 + }, + "token": { + "address": "0xf22258f57794CC8E06237084b353Ab30fFfa640b", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD Coin (Compliant)", + "onchainSymbol": "cUSDC", + "decimals": 6, + "totalSupplyRaw": "38601011267000000", + "totalSupplyUnits": "38601011267.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "38601011267.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952085 + }, + "token": { + "address": "0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d", + "name": "USD Coin (Compliant V2)", + "symbol": "cUSDC_V2", + "onchainName": "USD Coin (Compliant V2)", + "onchainSymbol": "cUSDC", + "decimals": 6, + "totalSupplyRaw": "380017953834", + "totalSupplyUnits": "380017.953834" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "380017.953834", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952085 + }, + "token": { + "address": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "Tether USD (Compliant)", + "onchainSymbol": "cUSDT", + "decimals": 6, + "totalSupplyRaw": "928784229000000", + "totalSupplyUnits": "928784229.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "928784229.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952086 + }, + "token": { + "address": "0x9FBfab33882Efe0038DAa608185718b772EE5660", + "name": "Tether USD (Compliant V2)", + "symbol": "cUSDT_V2", + "onchainName": "Tether USD (Compliant V2)", + "onchainSymbol": "cUSDT", + "decimals": 6, + "totalSupplyRaw": "530001062485", + "totalSupplyUnits": "530001.062485" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "530001.062485", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952086 + }, + "token": { + "address": "0xcA6BFa614935f1AB71c9aB106bAA6FBB6057095e", + "name": "USD W (Compliant)", + "symbol": "cUSDW", + "onchainName": "USDW (Compliant)", + "onchainSymbol": "cUSDW", + "decimals": 6, + "totalSupplyRaw": "0", + "totalSupplyUnits": "0.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "0.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952095 + }, + "token": { + "address": "0x290E52a8819A4fbD0714E517225429aA2B70EC6b", + "name": "Gold (Compliant)", + "symbol": "cXAUC", + "onchainName": "Gold (Compliant)", + "onchainSymbol": "cXAUC", + "decimals": 6, + "totalSupplyRaw": "1007568265651", + "totalSupplyUnits": "1007568.265651" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "1007568.265651", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 138, + "name": "DeFi Oracle Meta Mainnet", + "referenceBlock": 4952095 + }, + "token": { + "address": "0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E", + "name": "Tether XAU (Compliant)", + "symbol": "cXAUT", + "onchainName": "Tether XAU (Compliant)", + "onchainSymbol": "cXAUT", + "decimals": 6, + "totalSupplyRaw": "1007568265651", + "totalSupplyUnits": "1007568.265651" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "1007568.265651", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 1111, + "name": "Wemix", + "referenceBlock": 112065356 + }, + "token": { + "address": "0xE3F5a90F9cb311505cd691a46596599aA1A0AD7D", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD Coin", + "onchainSymbol": "USDC", + "decimals": 6, + "totalSupplyRaw": "11014520532789", + "totalSupplyUnits": "11014520.532789" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "11014520.532789", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 1111, + "name": "Wemix", + "referenceBlock": 112065360 + }, + "token": { + "address": "0xA649325Aa7C5093d12D6F98EB4378deAe68CE23F", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "Tether USD", + "onchainSymbol": "USDT", + "decimals": 6, + "totalSupplyRaw": "168569642765", + "totalSupplyUnits": "168569.642765" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "168569.642765", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 8453, + "name": "Base", + "referenceBlock": 45754953 + }, + "token": { + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD Coin", + "onchainSymbol": "USDC", + "decimals": 6, + "totalSupplyRaw": "4374380611328711", + "totalSupplyUnits": "4374380611.328711" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "4374380611.328711", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 8453, + "name": "Base", + "referenceBlock": 45754955 + }, + "token": { + "address": "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "decimals": 6, + "totalSupplyRaw": "23301271161496", + "totalSupplyUnits": "23301271.161496" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "23301271.161496", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 8453, + "name": "Base", + "referenceBlock": 45754956 + }, + "token": { + "address": "0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105", + "name": "USD Coin (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDC", + "onchainName": "Wrapped cUSDC", + "onchainSymbol": "cWUSDC", + "decimals": 6, + "totalSupplyRaw": "11000000000", + "totalSupplyUnits": "11000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "11000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 8453, + "name": "Base", + "referenceBlock": 45754957 + }, + "token": { + "address": "0x04B2AE3c3bb3d70Df506FAd8717b0FBFC78ED7E6", + "name": "Tether USD (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDT", + "decimals": 6, + "totalSupplyRaw": "11000000000", + "totalSupplyUnits": "11000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "11000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 42161, + "name": "Arbitrum One", + "referenceBlock": 460893941 + }, + "token": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD Coin", + "onchainSymbol": "USDC", + "decimals": 6, + "totalSupplyRaw": "5732089638330937", + "totalSupplyUnits": "5732089638.330937" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "5732089638.330937", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 42161, + "name": "Arbitrum One", + "referenceBlock": 460893954 + }, + "token": { + "address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "USD₮0", + "onchainSymbol": "USD₮0", + "decimals": 6, + "totalSupplyRaw": "995151040492349", + "totalSupplyUnits": "995151040.492349" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "995151040.492349", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 42161, + "name": "Arbitrum One", + "referenceBlock": 460893966 + }, + "token": { + "address": "0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF", + "name": "USD Coin (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDC", + "onchainName": "Wrapped cUSDC", + "onchainSymbol": "cWUSDC", + "decimals": 6, + "totalSupplyRaw": "13000000000", + "totalSupplyUnits": "13000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "13000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 42161, + "name": "Arbitrum One", + "referenceBlock": 460893974 + }, + "token": { + "address": "0x73ADaF7dBa95221c080db5631466d2bC54f6a76B", + "name": "Tether USD (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDT", + "onchainName": "Wrapped cUSDT", + "onchainSymbol": "cWUSDT", + "decimals": 6, + "totalSupplyRaw": "13000000000", + "totalSupplyUnits": "13000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "13000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 42220, + "name": "Celo", + "referenceBlock": 66398494 + }, + "token": { + "address": "0xcebA9300f2b948710d2653dD7B07f33A8B32118C", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USDC", + "onchainSymbol": "USDC", + "decimals": 6, + "totalSupplyRaw": "19007517220000", + "totalSupplyUnits": "19007517.22" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "19007517.22", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 42220, + "name": "Celo", + "referenceBlock": 66398497 + }, + "token": { + "address": "0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "Tether USD", + "onchainSymbol": "USD₮", + "decimals": 6, + "totalSupplyRaw": "470000000997000", + "totalSupplyUnits": "470000000.997" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "470000000.997", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 42220, + "name": "Celo", + "referenceBlock": 66398498 + }, + "token": { + "address": "0xC158b6cD3A3088C52F797D41f5Aa02825361629e", + "name": "Alltra USD Token (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWAUSDT", + "onchainName": "Wrapped cAUSDT", + "onchainSymbol": "cWAUSDT", + "decimals": 6, + "totalSupplyRaw": "3000000000", + "totalSupplyUnits": "3000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "3000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 42220, + "name": "Celo", + "referenceBlock": 66398500 + }, + "token": { + "address": "0x4C38F9A5ed68A04cd28a72E8c68C459Ec34576f3", + "name": "USD Coin (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDC", + "onchainName": "Wrapped cUSDC", + "onchainSymbol": "cWUSDC", + "decimals": 6, + "totalSupplyRaw": "13000000000", + "totalSupplyUnits": "13000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "13000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 42220, + "name": "Celo", + "referenceBlock": 66398502 + }, + "token": { + "address": "0x73376eB92c16977B126dB9112936A20Fa0De3442", + "name": "Tether USD (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDT", + "onchainName": "Wrapped cUSDT", + "onchainSymbol": "cWUSDT", + "decimals": 6, + "totalSupplyRaw": "13000000000", + "totalSupplyUnits": "13000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "13000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 43114, + "name": "Avalanche C-Chain", + "referenceBlock": 84963312 + }, + "token": { + "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "USD Coin", + "onchainSymbol": "USDC", + "decimals": 6, + "totalSupplyRaw": "545865067327219", + "totalSupplyUnits": "545865067.327219" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "545865067.327219", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 43114, + "name": "Avalanche C-Chain", + "referenceBlock": 84963315 + }, + "token": { + "address": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "TetherToken", + "onchainSymbol": "USDt", + "decimals": 6, + "totalSupplyRaw": "1847205574047338", + "totalSupplyUnits": "1847205574.047338" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "1847205574.047338", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 43114, + "name": "Avalanche C-Chain", + "referenceBlock": 84963318 + }, + "token": { + "address": "0xff3084410A732231472Ee9f93F5855dA89CC5254", + "name": "Alltra USD Token (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWAUSDT", + "onchainName": "Wrapped cAUSDT", + "onchainSymbol": "cWAUSDT", + "decimals": 6, + "totalSupplyRaw": "362506000000", + "totalSupplyUnits": "362506.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "362506.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 43114, + "name": "Avalanche C-Chain", + "referenceBlock": 84963318 + }, + "token": { + "address": "0x0C242b513008Cd49C89078F5aFb237A3112251EB", + "name": "USD Coin (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDC", + "onchainName": "Wrapped cUSDC", + "onchainSymbol": "cWUSDC", + "decimals": 6, + "totalSupplyRaw": "2000283800000", + "totalSupplyUnits": "2000283.8" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "2000283.8", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 43114, + "name": "Avalanche C-Chain", + "referenceBlock": 84963321 + }, + "token": { + "address": "0x8142BA530B08f3950128601F00DaaA678213DFdf", + "name": "Tether USD (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDT", + "onchainName": "Wrapped cUSDT", + "onchainSymbol": "cWUSDT", + "decimals": 6, + "totalSupplyRaw": "2000286268227", + "totalSupplyUnits": "2000286.268227" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "2000286.268227", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 43114, + "name": "Avalanche C-Chain", + "referenceBlock": 84963321 + }, + "token": { + "address": "0xcfdCe5E660FC2C8052BDfa7aEa1865DD753411Ae", + "name": "USD W (Compliant Wrapped ISO-4217 M1)", + "symbol": "cWUSDW", + "onchainName": "Wrapped cUSDW", + "onchainSymbol": "cWUSDW", + "decimals": 6, + "totalSupplyRaw": "361506000000", + "totalSupplyUnits": "361506.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "361506.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 651940, + "name": "ALL Mainnet", + "referenceBlock": 33419742 + }, + "token": { + "address": "0xa95EeD79f84E6A0151eaEb9d441F9Ffd50e8e881", + "name": "USD Coin (Compliant)", + "symbol": "cUSDC", + "onchainName": "AUSDC", + "onchainSymbol": "USDC", + "decimals": 18, + "totalSupplyRaw": "1000000000000000000000000", + "totalSupplyUnits": "1000000.0" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "1000000.0", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + }, + { + "schema": "token-aggregation-supply-proof/v1", + "generatedAt": "2026-05-09T04:00:51.739Z", + "network": { + "chainId": 651940, + "name": "ALL Mainnet", + "referenceBlock": 33419743 + }, + "token": { + "address": "0x015B1897Ed5279930bC2Be46F661894d219292A6", + "name": "Tether USD (Compliant)", + "symbol": "cUSDT", + "onchainName": "Alltra USD Token", + "onchainSymbol": "AUSDT", + "decimals": 18, + "totalSupplyRaw": "535053697365559770228576548644", + "totalSupplyUnits": "535053697365.559770228576548644" + }, + "circulatingSupplyMethodology": { + "status": "onchain_total_supply_proved_circulating_review_required", + "recommendedFormula": "circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances", + "currentConservativeReportableCirculatingSupplyUnits": "535053697365.559770228576548644", + "notes": [ + "totalSupply was read from the mapped token contract at the reference block.", + "circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.", + "Tracker acceptance is external and not implied by this API response." + ] + } + } + ], + "proofFailures": [ + { + "chainId": 138, + "symbol": "cBTC", + "address": "0xcb7c000000000000000000000000000000000138", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 138, + "symbol": "cETH", + "address": "0xce7e00000000000000000000000000000000008a", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 138, + "symbol": "cETHL2", + "address": "0xce7200000000000000000000000000000000008a", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 138, + "symbol": "cBNB", + "address": "0xcb6b00000000000000000000000000000000008a", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 138, + "symbol": "cPOL", + "address": "0xc90100000000000000000000000000000000008a", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 138, + "symbol": "cAVAX", + "address": "0xcaaa00000000000000000000000000000000008a", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 138, + "symbol": "cCRO", + "address": "0xcc2000000000000000000000000000000000008a", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 138, + "symbol": "cXDAI", + "address": "0xcda100000000000000000000000000000000008a", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 138, + "symbol": "cCELO", + "address": "0xcce100000000000000000000000000000000008a", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 138, + "symbol": "cWEMIX", + "address": "0xc11100000000000000000000000000000000008a", + "reason": "https://rpc-http-pub.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc2.d-bis.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.defi-oracle.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 1, + "symbol": "cWBTC", + "address": "0xcb7c000000000000000000000000000000000001", + "reason": "https://eth.llamarpc.com: missing revert data (action=\"call\", data=null, reason=null, transaction={ \"data\": \"0x18160ddd\", \"to\": \"0xCb7C000000000000000000000000000000000001\" }, invocation=null, revert=null, code=CALL_EXCEPTION, version=6.16.0) | https://rpc.ankr.com/eth: missing response for request (value=[ { \"error\": { \"code\": -32000, \"message\": \"Unauthorized: You must authenticate your request with an API key. Create an account on https://www.ankr.com/rpc/ and generate your personal API key for free.\" }, \"id\": null, \"jsonrpc\": \"2.0\" } ], info={ \"payload\": { \"id\": 1, \"jsonrpc\": \"2.0\", \"method\": \"eth_blockNumber\", \"params\": [ ] } }, code=BAD_DATA, version=6.16.0) | https://ethereum.publicnode.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://1rpc.io/eth: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 10, + "symbol": "cWBTC", + "address": "0xcb7c00000000000000000000000000000000000a", + "reason": "https://mainnet.optimism.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://optimism.llamarpc.com: getaddrinfo ENOTFOUND optimism.llamarpc.com" + }, + { + "chainId": 25, + "symbol": "cWBTC", + "address": "0xcb7c000000000000000000000000000000000019", + "reason": "https://evm.cronos.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://cronos-rpc.publicnode.com: could not coalesce error (error={ \"code\": -32601, \"message\": \"Method not found\" }, payload={ \"id\": 1, \"jsonrpc\": \"2.0\", \"method\": \"eth_blockNumber\", \"params\": [ ] }, code=UNKNOWN_ERROR, version=6.16.0)" + }, + { + "chainId": 56, + "symbol": "cWBTC", + "address": "0xcb7c000000000000000000000000000000000038", + "reason": "https://bsc-dataseed.binance.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://bsc-dataseed1.defibit.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 100, + "symbol": "cWBTC", + "address": "0xcb7c000000000000000000000000000000000064", + "reason": "https://rpc.gnosischain.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.ankr.com/gnosis: missing response for request (value=[ { \"error\": { \"code\": -32000, \"message\": \"Unauthorized: You must authenticate your request with an API key. Create an account on https://www.ankr.com/rpc/ and generate your personal API key for free.\" }, \"id\": null, \"jsonrpc\": \"2.0\" } ], info={ \"payload\": { \"id\": 1, \"jsonrpc\": \"2.0\", \"method\": \"eth_blockNumber\", \"params\": [ ] } }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 137, + "symbol": "cWBTC", + "address": "0xcb7c000000000000000000000000000000000089", + "reason": "https://polygon-bor-rpc.publicnode.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://1rpc.io/matic: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://polygon.drpc.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://polygon-rpc.com: server response 401 Unauthorized (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-rpc.com\", \"responseBody\": \"{\\\"error\\\":\\\"message: API key disabled, reason: tenant disabled, json-rpc code: -32051, rest code: 403\\\"}\", \"responseStatus\": \"401 Unauthorized\" }, code=SERVER_ERROR, version=6.16.0) | https://rpc.ankr.com/polygon: missing response for request (value=[ { \"error\": { \"code\": -32000, \"message\": \"Unauthorized: You must authenticate your request with an API key. Create an account on https://www.ankr.com/rpc/ and generate your personal API key for free.\" }, \"id\": null, \"jsonrpc\": \"2.0\" } ], info={ \"payload\": { \"id\": 1, \"jsonrpc\": \"2.0\", \"method\": \"eth_blockNumber\", \"params\": [ ] } }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 1111, + "symbol": "cWBTC", + "address": "0xcb7c000000000000000000000000000000000457", + "reason": "https://api.wemix.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://wemix-rpc.publicnode.com: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://wemix-rpc.publicnode.com\", \"responseBody\": null, \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.16.0)" + }, + { + "chainId": 8453, + "symbol": "cWBTC", + "address": "0xcb7c000000000000000000000000000000002105", + "reason": "https://mainnet.base.org: missing revert data (action=\"call\", data=null, reason=null, transaction={ \"data\": \"0x18160ddd\", \"to\": \"0xcB7c000000000000000000000000000000002105\" }, invocation=null, revert=null, code=CALL_EXCEPTION, version=6.16.0) | https://base.llamarpc.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 42161, + "symbol": "cWBTC", + "address": "0xcb7c00000000000000000000000000000000a4b1", + "reason": "https://arb1.arbitrum.io/rpc: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://arbitrum.llamarpc.com: getaddrinfo ENOTFOUND arbitrum.llamarpc.com" + }, + { + "chainId": 42220, + "symbol": "cWBTC", + "address": "0xcb7c00000000000000000000000000000000a4ec", + "reason": "https://forno.celo.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://celo-rpc.publicnode.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 43114, + "symbol": "cWBTC", + "address": "0xcb7c00000000000000000000000000000000a86a", + "reason": "https://api.avax.network/ext/bc/C/rpc: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://avalanche-c-chain-rpc.publicnode.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 1, + "symbol": "cWETH", + "address": "0xce7e000000000000000000000000000000000001", + "reason": "https://eth.llamarpc.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.ankr.com/eth: missing response for request (value=[ { \"error\": { \"code\": -32000, \"message\": \"Unauthorized: You must authenticate your request with an API key. Create an account on https://www.ankr.com/rpc/ and generate your personal API key for free.\" }, \"id\": null, \"jsonrpc\": \"2.0\" } ], info={ \"payload\": { \"id\": 1, \"jsonrpc\": \"2.0\", \"method\": \"eth_blockNumber\", \"params\": [ ] } }, code=BAD_DATA, version=6.16.0) | https://ethereum.publicnode.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://1rpc.io/eth: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 10, + "symbol": "cWETHL2", + "address": "0xce7200000000000000000000000000000000000a", + "reason": "https://mainnet.optimism.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://optimism.llamarpc.com: getaddrinfo ENOTFOUND optimism.llamarpc.com" + }, + { + "chainId": 8453, + "symbol": "cWETHL2", + "address": "0xce72000000000000000000000000000000002105", + "reason": "https://mainnet.base.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://base.llamarpc.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 42161, + "symbol": "cWETHL2", + "address": "0xce7200000000000000000000000000000000a4b1", + "reason": "https://arb1.arbitrum.io/rpc: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://arbitrum.llamarpc.com: getaddrinfo ENOTFOUND arbitrum.llamarpc.com" + }, + { + "chainId": 56, + "symbol": "cWBNB", + "address": "0xcb6b000000000000000000000000000000000038", + "reason": "https://bsc-dataseed.binance.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://bsc-dataseed1.defibit.io: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 137, + "symbol": "cWPOL", + "address": "0xc901000000000000000000000000000000000089", + "reason": "https://polygon-bor-rpc.publicnode.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://1rpc.io/matic: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://polygon.drpc.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://polygon-rpc.com: server response 401 Unauthorized (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-rpc.com\", \"responseBody\": \"{\\\"error\\\":\\\"message: API key disabled, reason: tenant disabled, json-rpc code: -32051, rest code: 403\\\"}\", \"responseStatus\": \"401 Unauthorized\" }, code=SERVER_ERROR, version=6.16.0) | https://rpc.ankr.com/polygon: missing response for request (value=[ { \"error\": { \"code\": -32000, \"message\": \"Unauthorized: You must authenticate your request with an API key. Create an account on https://www.ankr.com/rpc/ and generate your personal API key for free.\" }, \"id\": null, \"jsonrpc\": \"2.0\" } ], info={ \"payload\": { \"id\": 1, \"jsonrpc\": \"2.0\", \"method\": \"eth_blockNumber\", \"params\": [ ] } }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 43114, + "symbol": "cWAVAX", + "address": "0xcaaa00000000000000000000000000000000a86a", + "reason": "https://api.avax.network/ext/bc/C/rpc: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://avalanche-c-chain-rpc.publicnode.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 25, + "symbol": "cWCRO", + "address": "0xcc20000000000000000000000000000000000019", + "reason": "https://evm.cronos.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://cronos-rpc.publicnode.com: could not coalesce error (error={ \"code\": -32601, \"message\": \"Method not found\" }, payload={ \"id\": 1, \"jsonrpc\": \"2.0\", \"method\": \"eth_blockNumber\", \"params\": [ ] }, code=UNKNOWN_ERROR, version=6.16.0)" + }, + { + "chainId": 100, + "symbol": "cWXDAI", + "address": "0xcda1000000000000000000000000000000000064", + "reason": "https://rpc.gnosischain.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://rpc.ankr.com/gnosis: missing response for request (value=[ { \"error\": { \"code\": -32000, \"message\": \"Unauthorized: You must authenticate your request with an API key. Create an account on https://www.ankr.com/rpc/ and generate your personal API key for free.\" }, \"id\": null, \"jsonrpc\": \"2.0\" } ], info={ \"payload\": { \"id\": 1, \"jsonrpc\": \"2.0\", \"method\": \"eth_blockNumber\", \"params\": [ ] } }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 42220, + "symbol": "cWCELO", + "address": "0xcce100000000000000000000000000000000a4ec", + "reason": "https://forno.celo.org: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://celo-rpc.publicnode.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0)" + }, + { + "chainId": 1111, + "symbol": "cWWEMIX", + "address": "0xc111000000000000000000000000000000000457", + "reason": "https://api.wemix.com: could not decode result data (value=\"0x\", info={ \"method\": \"totalSupply\", \"signature\": \"totalSupply()\" }, code=BAD_DATA, version=6.16.0) | https://wemix-rpc.publicnode.com: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://wemix-rpc.publicnode.com\", \"responseBody\": null, \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.16.0)" + } + ] +} diff --git a/services/token-aggregation/deploy-to-vmid.sh b/services/token-aggregation/deploy-to-vmid.sh index d42a21c..683c868 100755 --- a/services/token-aggregation/deploy-to-vmid.sh +++ b/services/token-aggregation/deploy-to-vmid.sh @@ -26,8 +26,8 @@ log_ok "Built" # Package log_info "Creating package..." -PACKAGE_ITEMS=(dist src package.json package-lock.json tsconfig.json scripts) -for optional in .env.example .env; do +PACKAGE_ITEMS=(dist src config package.json package-lock.json tsconfig.json scripts) +for optional in public docs frontend .env.example .env; do [ -e "$SCRIPT_DIR/$optional" ] && PACKAGE_ITEMS+=("$optional") done (cd "$SCRIPT_DIR" && tar czf /tmp/token-agg.tar.gz --exclude=node_modules "${PACKAGE_ITEMS[@]}") diff --git a/services/token-aggregation/package-lock.json b/services/token-aggregation/package-lock.json index 1bed3a3..ee03c4a 100644 --- a/services/token-aggregation/package-lock.json +++ b/services/token-aggregation/package-lock.json @@ -15,7 +15,7 @@ "dotenv": "^16.6.1", "ethers": "^6.16.0", "express": "^5.1.0", - "express-rate-limit": "^8.4.1", + "express-rate-limit": "^8.5.1", "jsonwebtoken": "^9.0.3", "node-cron": "^4.2.1", "pg": "^8.18.0", @@ -3309,12 +3309,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", - "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -3941,9 +3941,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/services/token-aggregation/package.json b/services/token-aggregation/package.json index 066fc2e..1e64921 100644 --- a/services/token-aggregation/package.json +++ b/services/token-aggregation/package.json @@ -13,6 +13,8 @@ "test:omnl": "jest --runInBand --testPathPattern=omnl", "lint": "eslint src --ext .ts", "backfill:historical-pricing": "node dist/backfill-historical-pricing.js", + "generate:supply-proof-catalog": "ts-node scripts/generate-supply-proof-catalog.ts", + "generate:live-uniswap-v2-pool-catalog": "ts-node scripts/generate-live-uniswap-v2-pool-catalog.ts", "generate:route-matrix:v2": "ts-node scripts/generate-route-matrix-v2.ts", "migrate": "node -r dotenv/config dist/database/migrations.js", "example:partner-payloads": "node scripts/resolve-partner-payloads-example.mjs", @@ -26,7 +28,7 @@ "dotenv": "^16.6.1", "ethers": "^6.16.0", "express": "^5.1.0", - "express-rate-limit": "^8.4.1", + "express-rate-limit": "^8.5.1", "jsonwebtoken": "^9.0.3", "node-cron": "^4.2.1", "pg": "^8.18.0", diff --git a/services/token-aggregation/public/token-logos/gru/AVAX.svg b/services/token-aggregation/public/token-logos/gru/AVAX.svg new file mode 100644 index 0000000..610093f --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/AVAX.svg @@ -0,0 +1,6 @@ + + Avalanche + DBIS wallet logo for Avalanche compliant wrapped inventory. + + + diff --git a/services/token-aggregation/public/token-logos/gru/BNB.svg b/services/token-aggregation/public/token-logos/gru/BNB.svg new file mode 100644 index 0000000..fdb806d --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/BNB.svg @@ -0,0 +1,6 @@ + + BNB + DBIS wallet logo for BNB compliant wrapped inventory. + + + diff --git a/services/token-aggregation/public/token-logos/gru/CELO.svg b/services/token-aggregation/public/token-logos/gru/CELO.svg new file mode 100644 index 0000000..31805ae --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/CELO.svg @@ -0,0 +1,7 @@ + + Celo + DBIS wallet logo for Celo compliant wrapped inventory. + + + + diff --git a/services/token-aggregation/public/token-logos/gru/CRO.svg b/services/token-aggregation/public/token-logos/gru/CRO.svg new file mode 100644 index 0000000..b2c0abd --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/CRO.svg @@ -0,0 +1,6 @@ + + Cronos + DBIS wallet logo for Cronos compliant wrapped inventory. + + + diff --git a/services/token-aggregation/public/token-logos/gru/ETH.svg b/services/token-aggregation/public/token-logos/gru/ETH.svg new file mode 100644 index 0000000..096de40 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/ETH.svg @@ -0,0 +1,6 @@ + + Ethereum + Black Ethereum diamond mark on a transparent background. + + + diff --git a/services/token-aggregation/public/token-logos/gru/LINK.svg b/services/token-aggregation/public/token-logos/gru/LINK.svg new file mode 100644 index 0000000..b7d60a2 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/LINK.svg @@ -0,0 +1,6 @@ + + Chainlink + Chainlink mark in DBIS wallet-token format. + + + diff --git a/services/token-aggregation/public/token-logos/gru/POL.svg b/services/token-aggregation/public/token-logos/gru/POL.svg new file mode 100644 index 0000000..7c5d2b1 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/POL.svg @@ -0,0 +1,6 @@ + + POL + DBIS wallet logo for POL compliant wrapped inventory. + + + diff --git a/services/token-aggregation/public/token-logos/gru/WEMIX.svg b/services/token-aggregation/public/token-logos/gru/WEMIX.svg new file mode 100644 index 0000000..3f910c9 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/WEMIX.svg @@ -0,0 +1,6 @@ + + WEMIX + DBIS wallet logo for WEMIX compliant wrapped inventory. + + + diff --git a/services/token-aggregation/public/token-logos/gru/XDAI.svg b/services/token-aggregation/public/token-logos/gru/XDAI.svg new file mode 100644 index 0000000..4ab833a --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/XDAI.svg @@ -0,0 +1,6 @@ + + xDAI + DBIS wallet logo for xDAI compliant wrapped inventory. + + + diff --git a/services/token-aggregation/public/token-logos/gru/cAUDC.svg b/services/token-aggregation/public/token-logos/gru/cAUDC.svg new file mode 100644 index 0000000..e9c73cd --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cAUDC.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + AUD + cAUDC + diff --git a/services/token-aggregation/public/token-logos/gru/cCADC.svg b/services/token-aggregation/public/token-logos/gru/cCADC.svg new file mode 100644 index 0000000..ed91cb1 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cCADC.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + CAD + cCADC + diff --git a/services/token-aggregation/public/token-logos/gru/cCHFC.svg b/services/token-aggregation/public/token-logos/gru/cCHFC.svg new file mode 100644 index 0000000..4d4d53f --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cCHFC.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + CHF + cCHFC + diff --git a/services/token-aggregation/public/token-logos/gru/cEURC.svg b/services/token-aggregation/public/token-logos/gru/cEURC.svg new file mode 100644 index 0000000..63f5066 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cEURC.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + EUR + cEURC + diff --git a/services/token-aggregation/public/token-logos/gru/cEURT.svg b/services/token-aggregation/public/token-logos/gru/cEURT.svg new file mode 100644 index 0000000..a47ae61 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cEURT.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + EUR + cEURT + diff --git a/services/token-aggregation/public/token-logos/gru/cGBPC.svg b/services/token-aggregation/public/token-logos/gru/cGBPC.svg new file mode 100644 index 0000000..92e4833 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cGBPC.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + GBP + cGBPC + diff --git a/services/token-aggregation/public/token-logos/gru/cGBPT.svg b/services/token-aggregation/public/token-logos/gru/cGBPT.svg new file mode 100644 index 0000000..176992f --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cGBPT.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + GBP + cGBPT + diff --git a/services/token-aggregation/public/token-logos/gru/cJPYC.svg b/services/token-aggregation/public/token-logos/gru/cJPYC.svg new file mode 100644 index 0000000..46cc514 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cJPYC.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + JPY + cJPYC + diff --git a/services/token-aggregation/public/token-logos/gru/cUSDC.svg b/services/token-aggregation/public/token-logos/gru/cUSDC.svg new file mode 100644 index 0000000..b7cac38 --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cUSDC.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + USD + cUSDC + diff --git a/services/token-aggregation/public/token-logos/gru/cUSDT.svg b/services/token-aggregation/public/token-logos/gru/cUSDT.svg new file mode 100644 index 0000000..3a27a1e --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cUSDT.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + USD + cUSDT + diff --git a/services/token-aggregation/public/token-logos/gru/cXAUC.svg b/services/token-aggregation/public/token-logos/gru/cXAUC.svg new file mode 100644 index 0000000..733f40b --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cXAUC.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + XAU + cXAUC + diff --git a/services/token-aggregation/public/token-logos/gru/cXAUT.svg b/services/token-aggregation/public/token-logos/gru/cXAUT.svg new file mode 100644 index 0000000..eaf5a9e --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/cXAUT.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + DBIS + XAU + cXAUT + diff --git a/services/token-aggregation/public/token-logos/gru/chain-138.svg b/services/token-aggregation/public/token-logos/gru/chain-138.svg new file mode 100644 index 0000000..6d7fc7b --- /dev/null +++ b/services/token-aggregation/public/token-logos/gru/chain-138.svg @@ -0,0 +1,6 @@ + + + + diff --git a/services/token-aggregation/public/token-logos/ipfs/QmPh16PY241zNtePyeK7ep1uf1RcARV2ynGAuRU8U7sSqS b/services/token-aggregation/public/token-logos/ipfs/QmPh16PY241zNtePyeK7ep1uf1RcARV2ynGAuRU8U7sSqS new file mode 100644 index 0000000..389411d Binary files /dev/null and b/services/token-aggregation/public/token-logos/ipfs/QmPh16PY241zNtePyeK7ep1uf1RcARV2ynGAuRU8U7sSqS differ diff --git a/services/token-aggregation/public/token-logos/ipfs/QmT2nJ6WyhYBCsYJ6NfS1BPAqiGKkCEuMxiC8ye93Co1hF b/services/token-aggregation/public/token-logos/ipfs/QmT2nJ6WyhYBCsYJ6NfS1BPAqiGKkCEuMxiC8ye93Co1hF new file mode 100644 index 0000000..3d6ecf9 Binary files /dev/null and b/services/token-aggregation/public/token-logos/ipfs/QmT2nJ6WyhYBCsYJ6NfS1BPAqiGKkCEuMxiC8ye93Co1hF differ diff --git a/services/token-aggregation/public/token-logos/ipfs/Qma3FKtLce9MjgJgWbtyCxBiPjJ6xi8jGWUSKNS5Jc2ong b/services/token-aggregation/public/token-logos/ipfs/Qma3FKtLce9MjgJgWbtyCxBiPjJ6xi8jGWUSKNS5Jc2ong new file mode 100644 index 0000000..59d0980 Binary files /dev/null and b/services/token-aggregation/public/token-logos/ipfs/Qma3FKtLce9MjgJgWbtyCxBiPjJ6xi8jGWUSKNS5Jc2ong differ diff --git a/services/token-aggregation/public/token-logos/ipfs/Qmb9JmuD9ehaQtTLBBZmAoiAbvE53e3FMjkEty8rvbPf9K b/services/token-aggregation/public/token-logos/ipfs/Qmb9JmuD9ehaQtTLBBZmAoiAbvE53e3FMjkEty8rvbPf9K new file mode 100644 index 0000000..fb0af0e Binary files /dev/null and b/services/token-aggregation/public/token-logos/ipfs/Qmb9JmuD9ehaQtTLBBZmAoiAbvE53e3FMjkEty8rvbPf9K differ diff --git a/services/token-aggregation/public/token-logos/trustwallet/btc-logo.png b/services/token-aggregation/public/token-logos/trustwallet/btc-logo.png new file mode 100644 index 0000000..335d6b7 Binary files /dev/null and b/services/token-aggregation/public/token-logos/trustwallet/btc-logo.png differ diff --git a/services/token-aggregation/scripts/generate-live-uniswap-v2-pool-catalog.ts b/services/token-aggregation/scripts/generate-live-uniswap-v2-pool-catalog.ts new file mode 100644 index 0000000..ed290b3 --- /dev/null +++ b/services/token-aggregation/scripts/generate-live-uniswap-v2-pool-catalog.ts @@ -0,0 +1,163 @@ +import fs from 'fs'; +import path from 'path'; + +type DiscoveryPayload = { + generated_at?: string; + entries?: Array<{ + chain_id: number; + network?: string; + factoryAddress?: string; + routerAddress?: string; + pairsChecked?: Array<{ + base: string; + quote: string; + poolAddress: string; + live: boolean; + health?: { + baseReserveRaw?: string; + quoteReserveRaw?: string; + baseReserveUnits?: string; + quoteReserveUnits?: string; + priceQuotePerBase?: string; + deviationBps?: string; + healthy?: boolean; + depthOk?: boolean; + parityOk?: boolean; + }; + }>; + }>; +}; + +type DeploymentStatusChain = { + name?: string; + cwTokens?: Record; + anchorAddresses?: Record; + gasMirrors?: Record; + gasQuoteAddresses?: Record; +}; + +type DeploymentStatus = { + chains?: Record; +}; + +const PRICE_USD: Record = { + USDC: 1, + USDT: 1, + cUSDC: 1, + cUSDT: 1, + cWUSDC: 1, + cWUSDT: 1, + cWAUSDT: 1, +}; + +function findRepoRoot(): string { + const candidates = [ + process.env.PROXMOX_REPO_ROOT, + process.env.PROJECT_ROOT, + path.resolve(process.cwd(), '..', '..', '..'), + path.resolve(process.cwd(), '..'), + process.cwd(), + ].filter(Boolean) as string[]; + + for (const candidate of candidates) { + if (fs.existsSync(path.join(candidate, 'reports/extraction/promod-uniswap-v2-live-pair-discovery-latest.json'))) { + return candidate; + } + } + return path.resolve(process.cwd(), '..', '..', '..'); +} + +function readJson(filePath: string): T { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; +} + +function resolveTokenAddress(chain: DeploymentStatusChain, symbol: string): string | undefined { + return ( + chain.cwTokens?.[symbol] || + chain.anchorAddresses?.[symbol] || + chain.gasMirrors?.[symbol] || + chain.gasQuoteAddresses?.[symbol] + ); +} + +function parsePositiveNumber(value?: string): number { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; +} + +function main(): void { + const repoRoot = findRepoRoot(); + const discoveryPath = path.join(repoRoot, 'reports/extraction/promod-uniswap-v2-live-pair-discovery-latest.json'); + const deploymentStatusPath = path.join(repoRoot, 'cross-chain-pmm-lps/config/deployment-status.json'); + const outPath = path.join(process.cwd(), 'config/live-uniswap-v2-pool-catalog.json'); + + const discovery = readJson(discoveryPath); + const deploymentStatus = readJson(deploymentStatusPath); + const pools = []; + + for (const entry of discovery.entries ?? []) { + const chainId = Number(entry.chain_id); + const chain = deploymentStatus.chains?.[String(chainId)]; + if (!chain) continue; + + for (const pair of entry.pairsChecked ?? []) { + if (!pair.live || !pair.poolAddress?.startsWith('0x') || !pair.health) continue; + const baseAddress = resolveTokenAddress(chain, pair.base); + const quoteAddress = resolveTokenAddress(chain, pair.quote); + if (!baseAddress || !quoteAddress) continue; + + const baseReserveUnits = parsePositiveNumber(pair.health.baseReserveUnits); + const quoteReserveUnits = parsePositiveNumber(pair.health.quoteReserveUnits); + const basePriceUsd = PRICE_USD[pair.base] ?? 0; + const quotePriceUsd = PRICE_USD[pair.quote] ?? 0; + const reserve0Usd = baseReserveUnits * basePriceUsd; + const reserve1Usd = quoteReserveUnits * quotePriceUsd; + const totalLiquidityUsd = reserve0Usd + reserve1Usd; + if (totalLiquidityUsd <= 0) continue; + + pools.push({ + chainId, + chainName: entry.network ?? chain.name ?? `Chain ${chainId}`, + poolAddress: pair.poolAddress.toLowerCase(), + dex: 'uniswap_v2', + factoryAddress: entry.factoryAddress, + routerAddress: entry.routerAddress, + baseSymbol: pair.base, + quoteSymbol: pair.quote, + baseAddress: baseAddress.toLowerCase(), + quoteAddress: quoteAddress.toLowerCase(), + baseReserveRaw: pair.health.baseReserveRaw, + quoteReserveRaw: pair.health.quoteReserveRaw, + baseReserveUnits: pair.health.baseReserveUnits, + quoteReserveUnits: pair.health.quoteReserveUnits, + priceQuotePerBase: pair.health.priceQuotePerBase, + deviationBps: pair.health.deviationBps, + depthOk: pair.health.depthOk, + parityOk: pair.health.parityOk, + healthy: pair.health.healthy, + reserve0Usd, + reserve1Usd, + totalLiquidityUsd, + }); + } + } + + pools.sort((a, b) => a.chainId - b.chainId || a.poolAddress.localeCompare(b.poolAddress)); + const payload = { + schema: 'token-aggregation-live-uniswap-v2-pool-catalog/v1', + generatedAt: new Date().toISOString(), + sourceArtifacts: [ + 'reports/extraction/promod-uniswap-v2-live-pair-discovery-latest.json', + 'cross-chain-pmm-lps/config/deployment-status.json', + ], + poolCount: pools.length, + positiveLiquidityPoolCount: pools.filter((pool) => pool.totalLiquidityUsd > 0).length, + pools, + }; + + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, `${JSON.stringify(payload, null, 2)}\n`); + console.log(outPath); +} + +main(); diff --git a/services/token-aggregation/scripts/generate-supply-proof-catalog.ts b/services/token-aggregation/scripts/generate-supply-proof-catalog.ts new file mode 100644 index 0000000..c54c299 --- /dev/null +++ b/services/token-aggregation/scripts/generate-supply-proof-catalog.ts @@ -0,0 +1,238 @@ +#!/usr/bin/env ts-node + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import path from 'path'; +import { Contract, JsonRpcProvider, formatUnits } from 'ethers'; +import { CANONICAL_TOKENS, getTokenRegistryFamily } from '../src/config/canonical-tokens'; +import { getChainConfig } from '../src/config/chains'; +import { NETWORKS } from '../src/config/networks'; + +const ERC20_ABI = [ + 'function totalSupply() view returns (uint256)', + 'function decimals() view returns (uint8)', + 'function symbol() view returns (string)', + 'function name() view returns (string)', +]; + +type SupplyProof = { + schema: string; + generatedAt: string; + network: { + chainId: number; + name: string; + referenceBlock: number; + }; + token: { + address: string; + name: string; + symbol: string; + onchainName?: string; + onchainSymbol?: string; + decimals: number; + totalSupplyRaw: string; + totalSupplyUnits: string; + }; + circulatingSupplyMethodology: { + status: string; + recommendedFormula: string; + currentConservativeReportableCirculatingSupplyUnits: string; + notes: string[]; + }; + knownBalances?: unknown; + trackerSurfaces?: unknown; + [key: string]: unknown; +}; + +type SupplyProofCatalog = { + schema: string; + generatedAt: string; + proofs: SupplyProof[]; + proofFailures?: Array<{ + chainId: number; + symbol: string; + address: string; + reason: string; + }>; +}; + +function isCandidate(symbol: string, type?: string, registryFamily?: string): boolean { + return ( + /^c[A-Z0-9]/.test(symbol) && + (type === 'base' || + type === 'c' || + type === 'w' || + ['iso4217', 'monetary_unit', 'gas_native', 'commodity'].includes(String(registryFamily || ''))) + ); +} + +function catalogKey(chainId: number, address: string): string { + return `${chainId}:${address.toLowerCase()}`; +} + +function rpcUrlsForChain(chainId: number, primary?: string): string[] { + const network = NETWORKS.find((row) => row.chainIdDecimal === chainId); + return Array.from(new Set([primary, ...(network?.rpcUrls ?? [])].filter(Boolean) as string[])); +} + +function loadProofFile(proofPath: string): SupplyProof[] { + if (!existsSync(proofPath)) return []; + const parsed = JSON.parse(readFileSync(proofPath, 'utf8')) as Partial | SupplyProof; + const proofs = Array.isArray((parsed as Partial).proofs) + ? ((parsed as SupplyProofCatalog).proofs) + : [parsed as SupplyProof]; + return proofs.filter((proof) => proof?.network?.chainId && proof?.token?.address); +} + +function loadExistingCatalog(seedPaths: string[]): Map { + const proofs = new Map(); + for (const seedPath of seedPaths) { + for (const proof of loadProofFile(seedPath)) { + proofs.set(catalogKey(proof.network.chainId, proof.token.address), proof); + } + } + return proofs; +} + +function shouldPreserveExistingProof(proof: SupplyProof | undefined): boolean { + if (!proof) return false; + return ( + proof.knownBalances !== undefined || + proof.trackerSurfaces !== undefined || + proof.circulatingSupplyMethodology?.status === 'ready_for_tracker_review' + ); +} + +async function withTimeout(promise: Promise, timeoutMs: number, label: string): Promise { + let timeout: NodeJS.Timeout | undefined; + const timer = new Promise((_, reject) => { + timeout = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs); + }); + try { + return await Promise.race([promise, timer]); + } finally { + if (timeout) clearTimeout(timeout); + } +} + +async function main() { + const serviceRoot = path.resolve(__dirname, '..'); + const catalogPath = path.resolve( + process.env.TOKEN_AGGREGATION_SUPPLY_PROOF_CATALOG_JSON || + path.join(serviceRoot, 'config/supply-proof-catalog.json') + ); + const repoRoot = path.resolve(serviceRoot, '..', '..', '..'); + const timeoutMs = Number(process.env.SUPPLY_PROOF_RPC_TIMEOUT_MS || 12000); + const generatedAt = new Date().toISOString(); + const existing = loadExistingCatalog([ + catalogPath, + path.join(repoRoot, 'reports/status/mainnet-cwusdc-supply-proof-20260508.json'), + ]); + const proofs = new Map(existing); + const failures: SupplyProofCatalog['proofFailures'] = []; + + for (const spec of CANONICAL_TOKENS) { + const registryFamily = getTokenRegistryFamily(spec); + if (!isCandidate(spec.symbol, spec.type, registryFamily)) continue; + + for (const [chainIdText, rawAddress] of Object.entries(spec.addresses)) { + const chainId = Number(chainIdText); + const address = String(rawAddress || '').trim(); + if (!address.startsWith('0x')) continue; + const chain = getChainConfig(chainId); + if (!chain?.rpcUrl) { + failures.push({ chainId, symbol: spec.symbol, address, reason: 'missing_rpc_url' }); + continue; + } + + let proved = false; + const reasons: string[] = []; + for (const rpcUrl of rpcUrlsForChain(chainId, chain.rpcUrl)) { + try { + const provider = new JsonRpcProvider(rpcUrl, chainId, { staticNetwork: true }); + const contract = new Contract(address, ERC20_ABI, provider); + const [referenceBlock, totalSupplyRaw, decimals, onchainSymbol, onchainName] = await withTimeout( + Promise.all([ + provider.getBlockNumber(), + contract.totalSupply() as Promise, + contract.decimals().catch(() => spec.decimals) as Promise, + contract.symbol().catch(() => undefined) as Promise, + contract.name().catch(() => undefined) as Promise, + ]), + timeoutMs, + `${chainId}:${spec.symbol}` + ); + const decimalsNumber = Number(decimals); + const totalSupplyRawText = totalSupplyRaw.toString(); + const totalSupplyUnits = formatUnits(totalSupplyRaw, decimalsNumber); + const key = catalogKey(chainId, address); + if (!shouldPreserveExistingProof(proofs.get(key))) { + proofs.set(key, { + schema: 'token-aggregation-supply-proof/v1', + generatedAt, + network: { + chainId, + name: chain.name, + referenceBlock, + }, + token: { + address, + name: spec.name, + symbol: spec.symbol, + onchainName, + onchainSymbol, + decimals: decimalsNumber, + totalSupplyRaw: totalSupplyRawText, + totalSupplyUnits, + }, + circulatingSupplyMethodology: { + status: 'onchain_total_supply_proved_circulating_review_required', + recommendedFormula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances', + currentConservativeReportableCirculatingSupplyUnits: totalSupplyUnits, + notes: [ + 'totalSupply was read from the mapped token contract at the reference block.', + 'circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.', + 'Tracker acceptance is external and not implied by this API response.', + ], + }, + }); + } + proved = true; + break; + } catch (error) { + reasons.push(`${rpcUrl}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + if (!proved) { + failures.push({ + chainId, + symbol: spec.symbol, + address, + reason: reasons.join(' | '), + }); + } + } + } + + const catalog: SupplyProofCatalog = { + schema: 'token-aggregation-supply-proof-catalog/v1', + generatedAt, + proofs: Array.from(proofs.values()).sort( + (a, b) => a.network.chainId - b.network.chainId || a.token.symbol.localeCompare(b.token.symbol) + ), + proofFailures: failures, + }; + + mkdirSync(path.dirname(catalogPath), { recursive: true }); + writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`); + console.log(JSON.stringify({ + catalogPath, + proofs: catalog.proofs.length, + failures: failures.length, + }, null, 2)); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/services/token-aggregation/src/api/routes/bridge.ts b/services/token-aggregation/src/api/routes/bridge.ts index bfb6762..196ae69 100644 --- a/services/token-aggregation/src/api/routes/bridge.ts +++ b/services/token-aggregation/src/api/routes/bridge.ts @@ -78,6 +78,22 @@ function uniquePaths(paths: Array): string[] { return out; } +/** Non-empty built-in CCIP / trustless lane counts for /bridge/metrics (telemetry-friendly). */ +function summarizeBuiltInBridgeLanes() { + const payload = buildDefaultBridgeRoutes(); + const trustlessKeys = payload.routes.trustless ? Object.keys(payload.routes.trustless) : []; + const chain138 = payload.chain138Bridges as Record; + return { + weth9Destinations: Object.keys(payload.routes.weth9).length, + weth10Destinations: Object.keys(payload.routes.weth10).length, + trustlessDestinations: trustlessKeys.length, + chain138ConfiguredBridges: Object.keys(chain138).filter((k) => { + const v = chain138[k]; + return typeof v === 'string' && v.startsWith('0x'); + }), + }; +} + function resolveBridgeRoutesPath(): string | null { const candidates = uniquePaths([ process.env.BRIDGE_LIST_JSON_PATH, @@ -176,16 +192,35 @@ router.get('/status', (_req: Request, res: Response) => { router.get('/metrics', (_req: Request, res: Response) => { const gruTransport = buildGruTransportStatus(); + const builtIn = summarizeBuiltInBridgeLanes(); res.json({ ok: true, - lanes: [], + lanes: [ + { + kind: 'ccip-weth9', + label: 'WETH9 bridge destinations (built-in catalog)', + count: builtIn.weth9Destinations, + }, + { + kind: 'ccip-weth10', + label: 'WETH10 bridge destinations (built-in catalog)', + count: builtIn.weth10Destinations, + }, + { + kind: 'trustless', + label: 'Trustless / Lockbox destinations (env-backed)', + count: builtIn.trustlessDestinations, + }, + ], + chain138Bridges: builtIn.chain138ConfiguredBridges, gruTransport: gruTransport ? { system: gruTransport.system, summary: gruTransport.summary, } : null, - message: 'Bridge metrics include GRU Transport summary counts. Use /api/v1/report/cross-chain for aggregated data.', + message: + 'Lane counts reflect the built-in CCIP route catalog plus GRU Transport summary. Use /api/v1/bridge/routes for full JSON and /api/v1/report/cross-chain for volumes.', }); }); diff --git a/services/token-aggregation/src/api/routes/config.test.ts b/services/token-aggregation/src/api/routes/config.test.ts index 8d6b311..59944ae 100644 --- a/services/token-aggregation/src/api/routes/config.test.ts +++ b/services/token-aggregation/src/api/routes/config.test.ts @@ -139,4 +139,83 @@ describe('Config API runtime networks loader', () => { await new Promise((resolve, reject) => remoteServer.close((err) => (err ? reject(err) : resolve()))); } }); + + it('serves wallet-facing MetaMask aliases with absolute token images', async () => { + const networksRes = await fetch(`${baseUrl}/api/v1/config/networks`); + expect(networksRes.status).toBe(200); + const networksBody = (await networksRes.json()) as Record; + expect(networksBody.networks).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + chainIdDecimal: 138, + iconUrls: expect.arrayContaining([ + 'https://explorer.d-bis.org/token-icons/chain-138.png', + 'https://explorer.d-bis.org/api/v1/report/logo/chain-138', + ]), + }), + ]) + ); + + const metamaskRes = await fetch(`${baseUrl}/api/v1/config/metamask?chainId=138`); + expect(metamaskRes.status).toBe(200); + const metamaskBody = (await metamaskRes.json()) as Record; + expect(metamaskBody.addEthereumChain).toMatchObject({ + chainId: '0x8a', + chainName: 'DeFi Oracle Meta Mainnet', + }); + expect(metamaskBody.watchAssets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'ERC20', + options: expect.objectContaining({ + symbol: 'cUSDC', + image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/cUSDC\?v=20260510$/), + }), + }), + expect.objectContaining({ + type: 'ERC20', + options: expect.objectContaining({ + address: '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03', + symbol: 'LINK', + image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/LINK\?v=20260510$/), + }), + }), + expect.objectContaining({ + type: 'ERC20', + options: expect.objectContaining({ + symbol: 'WETH', + image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/ETH\?v=20260510$/), + }), + }), + expect.objectContaining({ + type: 'ERC20', + options: expect.objectContaining({ + symbol: 'cWEMIX', + image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/WEMIX\?v=20260510$/), + }), + }), + ]) + ); + expect(metamaskBody.watchAssets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'ERC20', + options: expect.objectContaining({ + address: '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d', + symbol: 'cUSDC', + }), + metadata: expect.objectContaining({ + catalogSymbol: 'cUSDC_V2', + familySymbol: 'cUSDC', + deploymentVersion: 'v2', + }), + }), + ]) + ); + expect( + metamaskBody.watchAssets.some( + (entry: Record) => entry.options?.symbol === 'cUSDC_V2' || entry.options?.symbol === 'cUSDT_V2' + ) + ).toBe(false); + }); }); diff --git a/services/token-aggregation/src/api/routes/config.ts b/services/token-aggregation/src/api/routes/config.ts index 8ad088a..a39f736 100644 --- a/services/token-aggregation/src/api/routes/config.ts +++ b/services/token-aggregation/src/api/routes/config.ts @@ -1,12 +1,75 @@ import { Router, Request, Response } from 'express'; import fs from 'fs'; import path from 'path'; -import { getNetworks, getConfigByChain, API_VERSION } from '../../config/networks'; +import { getNetworks, getConfigByChain, API_VERSION, type NetworkEntry } from '../../config/networks'; +import { getCanonicalTokensByChain, getLogoUriForSpec, getTokenRegistryFamily } from '../../config/canonical-tokens'; import { cacheMiddleware } from '../middleware/cache'; import { fetchRemoteJson } from '../utils/fetch-remote-json'; import { logger } from '../../utils/logger'; const router: Router = Router(); +const DEFAULT_PUBLIC_BASE_URL = 'https://explorer.d-bis.org'; +const DEFAULT_WALLET_METADATA_VERSION = '20260510'; + +function resolvePublicBaseUrl(req: Request): string { + const configured = ( + process.env.TOKEN_AGGREGATION_PUBLIC_BASE_URL ?? + process.env.PUBLIC_API_BASE_URL ?? + process.env.PUBLIC_BASE_URL ?? + '' + ).trim(); + if (configured) return configured.replace(/\/+$/, ''); + + const host = String(req.get('x-forwarded-host') || req.get('host') || '').split(',')[0].trim(); + if (host) { + let proto = String(req.get('x-forwarded-proto') || 'https').split(',')[0].trim() || 'https'; + if (proto === 'http' && !/^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(host)) { + proto = 'https'; + } + return `${proto}://${host}`.replace(/\/+$/, ''); + } + + return DEFAULT_PUBLIC_BASE_URL; +} + +function absolutePublicUrl(req: Request, value: string | undefined): string | undefined { + if (!value) return undefined; + if (/^https?:\/\//i.test(value)) return value; + if (!value.startsWith('/')) return value; + return `${resolvePublicBaseUrl(req)}${value}`; +} + +function appendWalletMetadataVersion(value: string | undefined): string | undefined { + if (!value) return undefined; + const version = (process.env.WALLET_METADATA_IMAGE_VERSION || DEFAULT_WALLET_METADATA_VERSION).trim(); + if (!version) return value; + const separator = value.includes('?') ? '&' : '?'; + return `${value}${separator}v=${encodeURIComponent(version)}`; +} + +function localLogoPathForSymbol(symbol: string, originalLogoUri: string): string { + if (originalLogoUri.includes('/token-lists/logos/gru/')) { + const fileName = originalLogoUri.split('/').pop()?.replace(/\.svg$/i, ''); + if (fileName) return `/api/v1/report/logo/${fileName}`; + } + if (originalLogoUri.includes('/blockchains/bitcoin/info/logo.png')) return '/api/v1/report/logo/cWBTC'; + if (originalLogoUri.includes('/ipfs/')) { + const cid = originalLogoUri.split('/').pop(); + if (cid) return `/api/v1/report/logo/ipfs-${cid}`; + } + if (symbol === 'cWUSDC') return '/api/v1/report/logo/cUSDC'; + return originalLogoUri; +} + +function resolveWalletWatchAssetSymbol(spec: { symbol: string; familySymbol?: string; deploymentVersion?: string }): string { + // MetaMask validates wallet_watchAsset.symbol against ERC-20 symbol(). + // Staged V2 Chain 138 deployments currently keep the family symbol on-chain + // (for example cUSDC), while the catalog symbol distinguishes the row + // (for example cUSDC_V2). Keep the catalog identity in metadata and send the + // contract-facing symbol in options.symbol so EIP-747 succeeds. + if (spec.deploymentVersion && spec.familySymbol) return spec.familySymbol; + return spec.symbol; +} type RuntimeNetworksPayload = { version?: string | { major?: number; minor?: number; patch?: number }; @@ -124,16 +187,77 @@ async function resolveNetworksPayload(): Promise { }; } +async function sendNetworks(_req: Request, res: Response): Promise { + res.set('Cache-Control', 'public, max-age=0, must-revalidate'); + const payload = await resolveNetworksPayload(); + res.json(payload); +} + /** * GET /api/v1/networks * Full EIP-3085 chain params for wallet_addEthereumChain (Chain 138, 1, 651940). * If NETWORKS_JSON_URL is set (e.g. GitHub raw URL), fetches and returns that JSON; otherwise uses built-in networks. */ -router.get('/networks', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => { +router.get(['/networks', '/config/networks', '/metamask/networks'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => { + try { + await sendNetworks(req, res); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * GET /api/v1/config/metamask + * Wallet-facing aliases for Chain 138 add-chain params and watchAsset token entries. + */ +router.get(['/config/metamask', '/metamask'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => { try { res.set('Cache-Control', 'public, max-age=0, must-revalidate'); + const chainId = parseInt(String(req.query.chainId ?? '138'), 10) || 138; const payload = await resolveNetworksPayload(); - res.json(payload); + const networks = payload.networks as NetworkEntry[]; + const network = networks.find((entry) => Number(entry.chainIdDecimal) === chainId); + if (!network) { + res.status(404).json({ error: 'Chain not found', chainId }); + return; + } + + const watchAssets = getCanonicalTokensByChain(chainId) + .map((spec) => { + const address = spec.addresses[chainId]; + if (!address) return null; + const originalLogoURI = getLogoUriForSpec(spec); + return { + type: 'ERC20', + options: { + address, + symbol: resolveWalletWatchAssetSymbol(spec), + decimals: spec.decimals, + image: appendWalletMetadataVersion(absolutePublicUrl(req, localLogoPathForSymbol(spec.symbol, originalLogoURI))), + }, + metadata: { + name: spec.name, + catalogSymbol: spec.symbol, + registryFamily: getTokenRegistryFamily(spec), + familySymbol: spec.familySymbol, + deploymentVersion: spec.deploymentVersion, + deploymentStatus: spec.deploymentStatus, + }, + }; + }) + .filter(Boolean); + + res.json({ + source: payload.source, + version: payload.version, + chainId, + addEthereumChain: network, + watchAssets, + caveats: [ + 'MetaMask custom-token prices are controlled by MetaMask and its upstream asset/price providers; this endpoint supplies wallet metadata, logo URLs, and token-add payloads but cannot force MetaMask to render fiat prices.', + 'After metadata changes, remove and re-add the custom network/token in MetaMask or use the companion UI to call wallet_addEthereumChain and wallet_watchAsset again.', + ], + }); } catch (error) { res.status(500).json({ error: 'Internal server error' }); } diff --git a/services/token-aggregation/src/api/routes/report.test.ts b/services/token-aggregation/src/api/routes/report.test.ts index ef70e3c..a74fd21 100644 --- a/services/token-aggregation/src/api/routes/report.test.ts +++ b/services/token-aggregation/src/api/routes/report.test.ts @@ -91,6 +91,24 @@ describe('Report API', () => { const body = (await res.json()) as Record; expect(body.chainId).toBe(651940); }); + + it('enriches Mainnet cWUSDC with supply proof fields for CMC reports', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/cmc?chainId=1`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + const cwusdc = body.tokens.find((token: Record) => token.symbol === 'cWUSDC'); + expect(cwusdc).toMatchObject({ + contract_address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a', + total_supply: 10451316981.309788, + total_supply_raw: '10451316981309788', + circulating_supply: 10451316981.309788, + market_cap: 10451316981.309788, + supply_proof_provenance: expect.objectContaining({ + status: 'ready_for_tracker_review', + }), + }); + expect(cwusdc.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('on-chain supply proof')])); + }); }); describe('GET /api/v1/report/coingecko', () => { @@ -104,6 +122,62 @@ describe('Report API', () => { expect(body).toHaveProperty('tokens'); expect(Array.isArray(body.tokens)).toBe(true); }); + + it('enriches Mainnet cWUSDC with supply proof, circulating supply, and market cap', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + const cwusdc = body.tokens.find((token: Record) => token.symbol === 'cWUSDC'); + expect(cwusdc).toMatchObject({ + contract_address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a', + total_supply: 10451316981.309788, + total_supply_raw: '10451316981309788', + circulating_supply: 10451316981.309788, + circulating_supply_formula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances', + supply_proof_provenance: expect.objectContaining({ + schema: 'mainnet-cwusdc-supply-proof/v1', + referenceBlock: 25047586, + }), + market_data: expect.objectContaining({ + current_price: { usd: 1 }, + market_cap: 10451316981.309788, + }), + }); + expect(cwusdc.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('No public tracker')])); + }); + + it('surfaces GRU v2 deployment-status pools in tracker-facing token reports', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + const cwusdc = body.tokens.find((token: Record) => token.symbol === 'cWUSDC'); + expect(cwusdc.liquidity_pools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + pool_address: '0x69776fc607e9eda8042e320e7e43f54d06c68f0e', + source: 'gru-v2-deployment-status', + status: 'live', + role: 'defense', + }), + ]) + ); + }); + + it('surfaces explicit supply-proof gaps for Mainnet GRU assets without proof artifacts', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + const cwusdt = body.tokens.find((token: Record) => token.symbol === 'cWUSDT'); + expect(cwusdt).toMatchObject({ + contract_address: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae', + supply_proof_provenance: { + source: 'missing-supply-proof', + status: 'proof_required', + }, + }); + expect(cwusdt).not.toHaveProperty('total_supply'); + expect(cwusdt.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('tracker-grade supply proof')])); + }); }); describe('GET /api/v1/report/all', () => { @@ -149,6 +223,72 @@ describe('Report API', () => { }), }); }); + + it('includes Mainnet cWUSDC supply proof enrichment in unified reports', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=1`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + const cwusdc = body.tokens?.['1']?.find((token: Record) => token.symbol === 'cWUSDC'); + expect(cwusdc).toMatchObject({ + totalSupply: '10451316981.309788', + totalSupplyRaw: '10451316981309788', + circulatingSupply: '10451316981.309788', + circulatingSupplyFormula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances', + market: expect.objectContaining({ + priceUsd: 1, + marketCapUsd: 10451316981.309788, + }), + supplyProofProvenance: expect.objectContaining({ + schema: 'mainnet-cwusdc-supply-proof/v1', + referenceBlock: 25047586, + }), + }); + }); + + it('distinguishes proof-gated Mainnet cW assets from deterministic placeholder bindings', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=1`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + const proofGated = body.tokens?.['1']?.find((entry: Record) => entry.symbol === 'cWUSDT'); + expect(proofGated).toMatchObject({ + supplyProofProvenance: { + source: 'missing-supply-proof', + status: 'proof_required', + }, + }); + expect(proofGated.totalSupply).toBeUndefined(); + expect(proofGated.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('proof artifact')])); + + const placeholderSymbols = ['cWBTC', 'cWETH']; + for (const symbol of placeholderSymbols) { + const token = body.tokens?.['1']?.find((entry: Record) => entry.symbol === symbol); + expect(token).toMatchObject({ + supplyProofProvenance: { + source: 'deterministic-placeholder-address', + status: 'non_reportable_until_erc20_deployed', + }, + }); + expect(token.totalSupply).toBeUndefined(); + expect(token.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('deterministic placeholder')])); + } + }); + + it('marks proofless base GRU c assets as proof gated instead of leaving silent supply fields', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=138`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + const cusdc = body.tokens?.['138']?.find((entry: Record) => entry.symbol === 'cUSDC'); + expect(cusdc).toMatchObject({ + type: 'base', + registryFamily: 'iso4217', + supplyProofProvenance: { + source: 'missing-supply-proof', + status: 'proof_required', + }, + }); + expect(cusdc.totalSupply).toBeUndefined(); + expect(cusdc.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('tracker-grade supply proof')])); + }); }); describe('GET /api/v1/report/gas-registry', () => { @@ -185,6 +325,63 @@ describe('Report API', () => { }); }); + describe('GET /api/v1/report/adoption-readiness', () => { + it('summarizes proved, proof-gated, pool-indexed, and scoring gates', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/adoption-readiness`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + expect(body.scope).toBe('gru-c-and-cw-assets'); + expect(body.counts).toMatchObject({ + candidates: expect.any(Number), + reportableCandidates: expect.any(Number), + nonReportablePlaceholder: expect.any(Number), + proved: expect.any(Number), + proofRequired: expect.any(Number), + silent: 0, + liquidityMissing: expect.any(Number), + liquidityMissingWithPools: expect.any(Number), + liquidityMissingWithoutPools: expect.any(Number), + gruV2PoolsWithStatus: expect.any(Number), + }); + expect(body.institutional.score).toEqual(expect.any(Number)); + expect(body.cryptoListing.score).toEqual(expect.any(Number)); + expect(Array.isArray(body.blockerInventory.proofRequiredByChain)).toBe(true); + expect(Array.isArray(body.blockerInventory.liquidityMissingByChain)).toBe(true); + expect(Array.isArray(body.blockerInventory.liquidityMissingWithPoolsByChain)).toBe(true); + expect(Array.isArray(body.blockerInventory.liquidityMissingWithoutPoolsByChain)).toBe(true); + expect(Array.isArray(body.blockerInventory.liquidityMissingDetails)).toBe(true); + expect(Array.isArray(body.blockerInventory.externalOfficialQuoteLiquidityByChain)).toBe(true); + expect(Array.isArray(body.blockerInventory.nonReportablePlaceholderByChain)).toBe(true); + expect(Array.isArray(body.blockerInventory.gruV2PoolsMissingStatus)).toBe(true); + expect(Array.isArray(body.blockerInventory.notes)).toBe(true); + }); + + it('does not treat external official USDC/USDT mirrors as GRU liquidity blockers', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/adoption-readiness`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + expect(body.counts.externalOfficialQuoteLiquidity).toBeGreaterThan(0); + expect(body.blockerInventory.externalOfficialQuoteLiquidityByChain).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + chainId: 1, + symbols: expect.arrayContaining(['cUSDC', 'cUSDT']), + }), + expect.objectContaining({ + chainId: 56, + symbols: expect.arrayContaining(['cUSDC', 'cUSDT']), + }), + ]) + ); + expect(body.blockerInventory.liquidityMissingDetails).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ chainId: 1, symbol: 'cUSDT' }), + expect.objectContaining({ chainId: 56, symbol: 'cUSDC' }), + ]) + ); + }); + }); + describe('GET /api/v1/report/token-list', () => { it('surfaces both V1 and V2 Chain 138 canonical GRU deployments explicitly', async () => { const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=138`); @@ -379,6 +576,17 @@ describe('Report API', () => { ]) ); }); + + it('uses packaged DBIS-level local logo assets while preserving original logo references', async () => { + const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=1`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + const cwusdc = body.tokens.find((token: Record) => token.symbol === 'cWUSDC'); + expect(cwusdc).toMatchObject({ + logoURI: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/cUSDC$/), + originalLogoURI: expect.stringContaining('/token-lists/logos/gru/cUSDC.svg'), + }); + }); }); describe('GET /api/v1/report/cw-registry', () => { @@ -488,6 +696,8 @@ describe('Report API', () => { expect((body.pools as Array<{ poolAddress: string }>)[0]).toMatchObject({ poolAddress: '0x1111111111111111111111111111111111111111', section: 'pmmPools', + status: 'routing_enabled', + statusReason: expect.stringContaining('public routing is enabled'), }); } finally { await import('fs/promises').then((fs) => fs.unlink(tempPath).catch(() => undefined)); diff --git a/services/token-aggregation/src/api/routes/report.ts b/services/token-aggregation/src/api/routes/report.ts index 4f06c95..1e3b9f7 100644 --- a/services/token-aggregation/src/api/routes/report.ts +++ b/services/token-aggregation/src/api/routes/report.ts @@ -4,6 +4,8 @@ */ import { Router, Request, Response } from 'express'; +import { existsSync, readFileSync } from 'fs'; +import path from 'path'; import { TokenRepository } from '../../database/repositories/token-repo'; import { MarketDataRepository } from '../../database/repositories/market-data-repo'; import { PoolRepository } from '../../database/repositories/pool-repo'; @@ -19,21 +21,543 @@ import { cacheMiddleware } from '../middleware/cache'; import { fetchRemoteJson } from '../utils/fetch-remote-json'; import { buildCrossChainReport } from '../../indexer/cross-chain-indexer'; import { logger } from '../../utils/logger'; -import { filterPoolsForExposure, getActiveTransportPairs, getGruTransportMetadata } from '../../config/gru-transport'; +import { + filterPoolsForExposure, + getActiveTransportPairs, + getGruTransportMetadata, + type GruTransportGasAssetFamily, + type GruTransportPair, +} from '../../config/gru-transport'; import { buildCwRegistryChains, buildGasRegistryChains, loadDeploymentStatusFile, + type DeploymentStatusFile, type CwRegistryChain, } from '../../config/deployment-status'; import { getGruV2DeploymentPoolRows } from '../../config/gru-v2-deployment-pools'; import { getCanonicalPriceSnapshotGeneratedAt, getCanonicalPriceUsd } from '../../services/canonical-price-oracle'; +import { pmmVaultReserveFromChain, resolvePmmQuoteRpcUrl } from '../../services/pmm-onchain-quote'; const router: Router = Router(); const tokenRepo = new TokenRepository(); const marketDataRepo = new MarketDataRepository(); const poolRepo = new PoolRepository(); +const MAINNET_CWUSDC_ADDRESS = '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a'; +const DEFAULT_SUPPLY_CATALOG_RELATIVE_PATH = 'config/supply-proof-catalog.json'; +const DEFAULT_LIVE_UNISWAP_V2_POOL_CATALOG_RELATIVE_PATH = 'config/live-uniswap-v2-pool-catalog.json'; +const DEFAULT_PUBLIC_REPORT_BASE_URL = 'https://explorer.d-bis.org'; +const DBIS_CHAIN_138_LOGO_PATH = '/api/v1/report/logo/chain-138'; +const CWUSDC_SUPPLY_PROOF_FALLBACK = { + schema: 'mainnet-cwusdc-supply-proof/v1', + generatedAt: '2026-05-08T03:16:54Z', + network: { + chainId: 1, + name: 'Ethereum Mainnet', + referenceBlock: 25047586, + }, + token: { + address: MAINNET_CWUSDC_ADDRESS, + name: 'Wrapped cUSDC', + symbol: 'cWUSDC', + decimals: 6, + totalSupplyRaw: '10451316981309788', + totalSupplyUnits: '10451316981.309788', + }, + circulatingSupplyMethodology: { + status: 'ready_for_tracker_review', + recommendedFormula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances', + currentConservativeReportableCirculatingSupplyUnits: '10451316981.309788', + notes: [ + 'No public tracker has accepted a circulating-supply value for this contract yet.', + 'If a tracker requires exclusion of operator, treasury, bridge, or protocol-controlled balances, use the knownBalances section plus any additional signed treasury inventory supplied at submission time.', + 'The value above is an on-chain supply proof, not a third-party listing approval.', + ], + }, +}; + +type SupplyProofSnapshot = typeof CWUSDC_SUPPLY_PROOF_FALLBACK & { + _sourcePath?: string; + _source?: string; +}; + +type SupplyProofEnrichment = { + totalSupply?: string; + totalSupplyRaw?: string; + circulatingSupply?: string; + circulatingSupplyFormula?: string; + marketCapUsd?: number; + supplyProofProvenance: { + source: string; + path?: string; + schema?: string; + generatedAt?: string; + referenceBlock?: number; + status?: string; + }; + trackerCaveats: string[]; +}; + +type ReportPoolEntry = { + poolAddress: string; + dex: string; + token0: string; + token1: string; + token0Symbol?: string; + token1Symbol?: string; + tvl: number; + volume24h: number; + source?: string; + status?: string; + statusReason?: string; + role?: string; + section?: string; + publicRoutingEnabled?: boolean; + reserveSource?: string; + reserveUpdatedAt?: string; + reserve0Raw?: string; + reserve1Raw?: string; +}; + +type LiveUniswapV2PoolCatalogRow = { + chainId: number; + poolAddress: string; + dex?: string; + baseSymbol: string; + quoteSymbol: string; + baseAddress: string; + quoteAddress: string; + totalLiquidityUsd: number; + reserve0Usd?: number; + reserve1Usd?: number; + depthOk?: boolean; + parityOk?: boolean; + healthy?: boolean; + deviationBps?: string; +}; + +type LiveUniswapV2PoolCatalog = { + schema?: string; + generatedAt?: string; + pools?: LiveUniswapV2PoolCatalogRow[]; +}; + +function resolveRepoRoot(): string { + const candidates = [ + process.env.PROXMOX_REPO_ROOT, + process.env.PROJECT_ROOT, + path.resolve(process.cwd(), '..', '..', '..'), + path.resolve(process.cwd(), '..'), + process.cwd(), + ].filter(Boolean) as string[]; + + for (const candidate of candidates) { + if ( + existsSync(path.join(candidate, DEFAULT_SUPPLY_CATALOG_RELATIVE_PATH)) || + existsSync(path.join(candidate, DEFAULT_LIVE_UNISWAP_V2_POOL_CATALOG_RELATIVE_PATH)) || + existsSync(path.join(candidate, 'reports/status/mainnet-cwusdc-supply-proof-20260508.json')) + ) { + return candidate; + } + } + return process.cwd(); +} + +function loadLiveUniswapV2PoolCatalog(): LiveUniswapV2PoolCatalogRow[] { + const configuredCatalog = process.env.TOKEN_AGGREGATION_LIVE_UNISWAP_V2_POOL_CATALOG_JSON?.trim(); + const repoRoot = resolveRepoRoot(); + const candidates = [ + configuredCatalog, + path.join(repoRoot, DEFAULT_LIVE_UNISWAP_V2_POOL_CATALOG_RELATIVE_PATH), + ].filter(Boolean) as string[]; + + for (const candidate of candidates) { + if (!existsSync(candidate)) continue; + try { + const parsed = JSON.parse(readFileSync(candidate, 'utf8')) as LiveUniswapV2PoolCatalog; + return (parsed.pools ?? []).filter( + (pool) => + Number.isFinite(pool.chainId) && + pool.poolAddress?.startsWith('0x') && + pool.baseAddress?.startsWith('0x') && + pool.quoteAddress?.startsWith('0x') + ); + } catch (error) { + logger.warn('Unable to parse live Uniswap V2 pool catalog; skipping file', { candidate, error }); + } + } + + return []; +} + +function normalizeSupplyProofKey(chainId: number, address: string): string { + return `${chainId}:${address.toLowerCase()}`; +} + +function loadSupplyProofFile(proofPath: string, source: string): SupplyProofSnapshot[] { + if (!proofPath || !existsSync(proofPath)) return []; + + try { + const parsed = JSON.parse(readFileSync(proofPath, 'utf8')) as { proofs?: SupplyProofSnapshot[] } | SupplyProofSnapshot; + const proofs: SupplyProofSnapshot[] = Array.isArray((parsed as { proofs?: SupplyProofSnapshot[] })?.proofs) + ? ((parsed as { proofs: SupplyProofSnapshot[] }).proofs) + : [parsed as SupplyProofSnapshot]; + return proofs + .filter((proof) => proof?.network?.chainId && proof?.token?.address) + .map((proof) => ({ + ...proof, + _source: source, + _sourcePath: proofPath, + })); + } catch (error) { + logger.warn('Unable to parse supply proof file; skipping file', { proofPath, error }); + return []; + } +} + +function loadSupplyProofCatalog(): Map { + const configuredCatalog = process.env.TOKEN_AGGREGATION_SUPPLY_PROOF_CATALOG_JSON?.trim(); + const configuredSingleProof = process.env.CWUSDC_SUPPLY_PROOF_JSON?.trim(); + const repoRoot = resolveRepoRoot(); + const candidates = [ + configuredCatalog ? { path: configuredCatalog, source: 'configured-supply-proof-catalog' } : null, + { path: path.join(repoRoot, DEFAULT_SUPPLY_CATALOG_RELATIVE_PATH), source: 'repo-supply-proof-catalog' }, + configuredSingleProof ? { path: configuredSingleProof, source: 'configured-supply-proof-file' } : null, + { path: path.join(repoRoot, 'reports/status/mainnet-cwusdc-supply-proof-20260508.json'), source: 'repo-supply-proof-file' }, + ].filter(Boolean) as Array<{ path: string; source: string }>; + + const byToken = new Map(); + for (const candidate of candidates) { + for (const proof of loadSupplyProofFile(candidate.path, candidate.source)) { + const key = normalizeSupplyProofKey(Number(proof.network.chainId), String(proof.token.address)); + if (!byToken.has(key)) { + byToken.set(key, proof); + } + } + } + + if (!byToken.has(normalizeSupplyProofKey(1, MAINNET_CWUSDC_ADDRESS))) { + byToken.set(normalizeSupplyProofKey(1, MAINNET_CWUSDC_ADDRESS), { + ...CWUSDC_SUPPLY_PROOF_FALLBACK, + _source: 'embedded-fallback-snapshot', + }); + } + + return byToken; +} + +function isGruSupplyTrackedCandidate(symbol: string, type?: string, registryFamily?: string): boolean { + return ( + /^c[A-Z0-9]/.test(symbol) && + (type === 'base' || + type === 'c' || + type === 'w' || + ['iso4217', 'monetary_unit', 'gas_native', 'commodity'].includes(String(registryFamily || ''))) + ); +} + +const EXTERNAL_OFFICIAL_QUOTE_ASSETS = new Set([ + '1:cUSDC:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + '1:cUSDT:0xdac17f958d2ee523a2206206994597c13d831ec7', + '10:cUSDC:0x0b2c639c533813f4aa9d7837caf62653d097ff85', + '10:cUSDT:0x94b008aa00579c1307b0ef2c499ad98a8ce58e58', + '25:cUSDC:0xc21223249ca28397b4b6541dffaecc539bff0c59', + '25:cUSDT:0x66e428c3f67a68878562e79a0234c1f83c208770', + '56:cUSDC:0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d', + '56:cUSDT:0x55d398326f99059ff775485246999027b3197955', + '100:cUSDC:0xddafbb505ad214d7b80b1f830fccc89b60fb7a83', + '100:cUSDT:0x4ecaba5870353805a9f068101a40e0f32ed605c6', + '137:cUSDC:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359', + '137:cUSDT:0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + '8453:cUSDC:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + '8453:cUSDT:0xfde4c96c8593536e31f229ea8f37b2ada2699bb2', + '42161:cUSDC:0xaf88d065e77c8cc2239327c5edb3a432268e5831', + '42161:cUSDT:0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', + '42220:cUSDC:0xceba9300f2b948710d2653dd7b07f33a8b32118c', + '42220:cUSDT:0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e', + '43114:cUSDC:0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e', + '43114:cUSDT:0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', + '1111:cUSDC:0xe3f5a90f9cb311505cd691a46596599aa1a0ad7d', + '1111:cUSDT:0xa649325aa7c5093d12d6f98eb4378deae68ce23f', + '651940:cUSDC:0xa95eed79f84e6a0151eaeb9d441f9ffd50e8e881', + '651940:cUSDT:0x015b1897ed5279930bc2be46f661894d219292a6', +]); + +function hasExternalOfficialQuoteLiquidity(token: { chainId: number; symbol: string; address: string }): boolean { + return EXTERNAL_OFFICIAL_QUOTE_ASSETS.has(`${token.chainId}:${token.symbol}:${token.address.toLowerCase()}`); +} + +function isDeterministicPlaceholderAddress(address: string): boolean { + const normalized = address.toLowerCase(); + return /^0x[a-f0-9]{4}0{24,}[a-f0-9]{1,8}$/.test(normalized); +} + +function buildSupplyProofEnrichment( + chainId: number, + address: string, + symbol: string, + type: string | undefined, + registryFamily: string | undefined, + priceUsd?: number +): SupplyProofEnrichment | undefined { + const proof = loadSupplyProofCatalog().get(normalizeSupplyProofKey(chainId, address)); + + if (!proof) { + if (!isGruSupplyTrackedCandidate(symbol, type, registryFamily)) return undefined; + if (isDeterministicPlaceholderAddress(address)) { + return { + supplyProofProvenance: { + source: 'deterministic-placeholder-address', + status: 'non_reportable_until_erc20_deployed', + }, + trackerCaveats: [ + 'This token binding is a deterministic placeholder address and does not currently behave as an ERC-20 contract.', + 'Do not submit totalSupply, circulatingSupply, marketCapUsd, or liquidity claims for this asset until a deployed ERC-20 binding replaces the placeholder.', + 'The placeholder is kept visible for registry and routing roadmap traceability only.', + ], + }; + } + return { + supplyProofProvenance: { + source: 'missing-supply-proof', + status: 'proof_required', + }, + trackerCaveats: [ + 'No token-specific on-chain supply proof artifact is currently attached to this report response.', + 'Do not submit totalSupply, circulatingSupply, or marketCapUsd for this asset until a current proof artifact is generated and linked.', + 'Registry and pool visibility are not the same as tracker-grade supply proof.', + ], + }; + } + + const totalSupply = String(proof.token?.totalSupplyUnits ?? ''); + const totalSupplyRaw = String(proof.token?.totalSupplyRaw ?? ''); + const circulatingSupply = String( + proof.circulatingSupplyMethodology?.currentConservativeReportableCirculatingSupplyUnits ?? totalSupply + ); + const parsedCirculatingSupply = Number(circulatingSupply); + const marketCapUsd = + priceUsd !== undefined && Number.isFinite(parsedCirculatingSupply) ? parsedCirculatingSupply * priceUsd : undefined; + + return { + totalSupply, + totalSupplyRaw, + circulatingSupply, + circulatingSupplyFormula: String( + proof.circulatingSupplyMethodology?.recommendedFormula ?? + 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances' + ), + marketCapUsd, + supplyProofProvenance: { + source: proof._source ?? 'repo-supply-proof-file', + path: proof._sourcePath, + schema: proof.schema, + generatedAt: proof.generatedAt, + referenceBlock: proof.network?.referenceBlock, + status: proof.circulatingSupplyMethodology?.status, + }, + trackerCaveats: proof.circulatingSupplyMethodology?.notes ?? [ + 'Tracker acceptance is external and not implied by this API response.', + ], + }; +} + +function resolveGruV2ReserveRpcUrl(chainId: number): string { + const configured = resolvePmmQuoteRpcUrl(); + if (chainId === 138 && process.env.NODE_ENV !== 'test') return configured || 'http://192.168.11.211:8545'; + if (chainId === 56 && process.env.NODE_ENV !== 'test') { + return process.env.BSC_RPC_URL || process.env.BSC_MAINNET_RPC || process.env.RPC_URL_56 || 'https://bsc-rpc.publicnode.com'; + } + if (chainId === 43114 && process.env.NODE_ENV !== 'test') { + return ( + process.env.AVALANCHE_RPC_URL || + process.env.AVALANCHE_MAINNET_RPC || + process.env.RPC_URL_43114 || + 'https://avalanche-c-chain-rpc.publicnode.com' + ); + } + if (chainId === 1111 && process.env.NODE_ENV !== 'test') { + return process.env.WEMIX_MAINNET_RPC || process.env.WEMIX_RPC || process.env.RPC_URL_1111 || 'https://api.wemix.com'; + } + return ''; +} + +function decimalFromRawForReport(raw: bigint, decimals: number): number { + const scale = 10 ** decimals; + return Number(raw) / scale; +} + +function fallbackPoolDecimals(symbol?: string): number { + const normalized = String(symbol || '').toUpperCase(); + if (normalized === 'WETH' || normalized === 'ETH') return 18; + return 6; +} + +function fallbackPoolUsdPrice(symbol?: string): number { + const normalized = String(symbol || '').toUpperCase(); + if (normalized.includes('XAU')) return Number(process.env.TOKEN_AGGREGATION_XAU_USD || '0') || 0; + if (normalized === 'WETH' || normalized === 'ETH') return Number(process.env.TOKEN_AGGREGATION_ETH_USD || '0') || 0; + return 1; +} + +async function enrichGruV2FallbackPoolWithReserves(chainId: number, pool: ReportPoolEntry): Promise { + const rpcUrl = resolveGruV2ReserveRpcUrl(chainId); + if (!rpcUrl) return pool; + + const reserves = await pmmVaultReserveFromChain({ rpcUrl, poolAddress: pool.poolAddress }); + if (!reserves) return pool; + + const baseUnits = decimalFromRawForReport(reserves.baseReserveRaw, fallbackPoolDecimals(pool.token0Symbol)); + const quoteUnits = decimalFromRawForReport(reserves.quoteReserveRaw, fallbackPoolDecimals(pool.token1Symbol)); + const reserveUsd = + baseUnits * fallbackPoolUsdPrice(pool.token0Symbol) + quoteUnits * fallbackPoolUsdPrice(pool.token1Symbol); + + if (!Number.isFinite(reserveUsd) || reserveUsd <= 0) { + return { + ...pool, + reserveSource: 'onchain-gru-v2-pmm-getVaultReserve', + reserveUpdatedAt: new Date().toISOString(), + reserve0Raw: reserves.baseReserveRaw.toString(), + reserve1Raw: reserves.quoteReserveRaw.toString(), + }; + } + + return { + ...pool, + tvl: reserveUsd, + status: pool.status ?? 'reserve_visible', + statusReason: + pool.statusReason ?? + 'Pool is present in deployment-status and has positive on-chain reserves via getVaultReserve().', + reserveSource: 'onchain-gru-v2-pmm-getVaultReserve', + reserveUpdatedAt: new Date().toISOString(), + reserve0Raw: reserves.baseReserveRaw.toString(), + reserve1Raw: reserves.quoteReserveRaw.toString(), + }; +} + +async function buildGruV2FallbackPoolsForToken( + chainId: number, + tokenAddress: string, + existingPools: ReportPoolEntry[] +): Promise { + const token = tokenAddress.toLowerCase(); + const existing = new Set(existingPools.map((pool) => pool.poolAddress.toLowerCase())); + const pools = getGruV2DeploymentPoolRows() + .filter((pool) => pool.chainId === chainId) + .filter((pool) => pool.baseAddress.toLowerCase() === token || pool.quoteAddress.toLowerCase() === token) + .filter((pool) => !existing.has(pool.poolAddress.toLowerCase())) + .map((pool) => ({ + poolAddress: pool.poolAddress, + dex: pool.venue ?? 'gru_v2_pmm', + token0: pool.baseAddress, + token1: pool.quoteAddress, + token0Symbol: pool.baseSymbol, + token1Symbol: pool.quoteSymbol, + tvl: 0, + volume24h: 0, + source: 'gru-v2-deployment-status', + status: pool.status, + statusReason: pool.statusReason, + role: pool.role, + section: pool.section, + publicRoutingEnabled: pool.publicRoutingEnabled, + })); + + return Promise.all(pools.map((pool) => enrichGruV2FallbackPoolWithReserves(chainId, pool))); +} + +function buildLiveUniswapV2FallbackPoolsForToken( + chainId: number, + tokenAddress: string, + existingPools: ReportPoolEntry[] +): ReportPoolEntry[] { + const token = tokenAddress.toLowerCase(); + const existing = new Set(existingPools.map((pool) => pool.poolAddress.toLowerCase())); + return loadLiveUniswapV2PoolCatalog() + .filter((pool) => pool.chainId === chainId) + .filter((pool) => pool.baseAddress.toLowerCase() === token || pool.quoteAddress.toLowerCase() === token) + .filter((pool) => !existing.has(pool.poolAddress.toLowerCase())) + .map((pool) => ({ + poolAddress: pool.poolAddress, + dex: pool.dex ?? 'uniswap_v2', + token0: pool.baseAddress, + token1: pool.quoteAddress, + token0Symbol: pool.baseSymbol, + token1Symbol: pool.quoteSymbol, + tvl: Number(pool.totalLiquidityUsd ?? 0), + volume24h: 0, + source: 'live-uniswap-v2-pair-discovery', + status: pool.healthy ? 'indexed_live_healthy' : 'indexed_live_needs_repair', + statusReason: pool.healthy + ? 'Live Uniswap V2 pair discovered with on-chain reserves, depth, and parity checks passing.' + : `Live Uniswap V2 pair discovered; depthOk=${pool.depthOk === true}, parityOk=${pool.parityOk === true}, deviationBps=${pool.deviationBps ?? 'unknown'}.`, + role: 'public_indexable_liquidity', + publicRoutingEnabled: pool.healthy === true, + })); +} + +function resolveLocalLogoUri(remoteLogoUri: string, symbol: string): string | undefined { + if (remoteLogoUri.startsWith('/api/v1/report/logo/')) { + return remoteLogoUri; + } + if (remoteLogoUri.includes('/token-lists/logos/gru/')) { + const fileName = remoteLogoUri.split('/').pop()?.replace(/\.svg$/i, ''); + return fileName ? `/api/v1/report/logo/${fileName}` : undefined; + } + if (remoteLogoUri.includes('/blockchains/bitcoin/info/logo.png')) { + return '/api/v1/report/logo/cWBTC'; + } + if (remoteLogoUri.includes('/ipfs/')) { + const cid = remoteLogoUri.split('/').pop(); + return cid ? `/api/v1/report/logo/ipfs-${cid}` : undefined; + } + if (symbol === 'cWUSDC') { + return '/api/v1/report/logo/cUSDC'; + } + return undefined; +} + +function resolvePublicBaseUrl(req: Request): string { + const configured = ( + process.env.TOKEN_AGGREGATION_PUBLIC_BASE_URL ?? + process.env.PUBLIC_API_BASE_URL ?? + process.env.PUBLIC_BASE_URL ?? + '' + ).trim(); + if (configured) return configured.replace(/\/+$/, ''); + + const host = String(req.get('x-forwarded-host') || req.get('host') || '').split(',')[0].trim(); + if (host) { + let proto = String(req.get('x-forwarded-proto') || 'https').split(',')[0].trim() || 'https'; + if (proto === 'http' && !/^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(host)) { + proto = 'https'; + } + return `${proto}://${host}`.replace(/\/+$/, ''); + } + + return DEFAULT_PUBLIC_REPORT_BASE_URL; +} + +function absolutePublicUrl(req: Request, value: string | undefined): string | undefined { + if (!value) return undefined; + if (/^https?:\/\//i.test(value)) return value; + if (!value.startsWith('/')) return value; + return `${resolvePublicBaseUrl(req)}${value}`; +} + +function absoluteLogoUri(req: Request, remoteLogoUri: string, symbol: string): string { + const localLogoURI = resolveLocalLogoUri(remoteLogoUri, symbol); + return absolutePublicUrl(req, localLogoURI) ?? remoteLogoUri; +} + +function absoluteReportLogoUri(remoteLogoUri: string, symbol: string): string { + const localLogoURI = resolveLocalLogoUri(remoteLogoUri, symbol); + if (localLogoURI?.startsWith('/')) return `${DEFAULT_PUBLIC_REPORT_BASE_URL}${localLogoURI}`; + return localLogoURI ?? remoteLogoUri; +} + /** Build token entries with DB market/pool data for a chain */ async function buildTokenReport(chainId: number) { const canonical = getCanonicalTokensByChain(chainId); @@ -51,6 +575,8 @@ async function buildTokenReport(chainId: number) { deploymentStatus?: string; preferredForX402?: boolean; liquiditySourceSymbol?: string; + logoURI?: string; + originalLogoURI?: string; market?: { priceUsd?: number; volume24h: number; @@ -60,14 +586,13 @@ async function buildTokenReport(chainId: number) { liquidityUsd: number; lastUpdated: string; }; - pools: Array<{ - poolAddress: string; - dex: string; - token0: string; - token1: string; - tvl: number; - volume24h: number; - }>; + totalSupply?: string; + totalSupplyRaw?: string; + circulatingSupply?: string; + circulatingSupplyFormula?: string; + supplyProofProvenance?: SupplyProofEnrichment['supplyProofProvenance']; + trackerCaveats?: string[]; + pools: ReportPoolEntry[]; fromDb: boolean; }> = []; @@ -98,6 +623,58 @@ async function buildTokenReport(chainId: number) { const fallbackPriceUsd = getCanonicalPriceUsd(chainId, address); + const market = marketData + ? { + priceUsd: marketData.priceUsd ?? fallbackPriceUsd, + volume24h: marketData.volume24h, + volume7d: marketData.volume7d, + volume30d: marketData.volume30d, + marketCapUsd: marketData.marketCapUsd, + liquidityUsd: marketData.liquidityUsd, + lastUpdated: marketData.lastUpdated?.toISOString() ?? '', + } + : fallbackPriceUsd !== undefined + ? { + priceUsd: fallbackPriceUsd, + volume24h: 0, + volume7d: 0, + volume30d: 0, + liquidityUsd: 0, + lastUpdated: `${getCanonicalPriceSnapshotGeneratedAt()}T00:00:00.000Z`, + } + : undefined; + const registryFamily = getTokenRegistryFamily(spec); + const originalLogoURI = getLogoUriForSpec(spec); + const logoURI = absoluteReportLogoUri(originalLogoURI, spec.symbol); + const supplyProof = buildSupplyProofEnrichment(chainId, address, spec.symbol, spec.type, registryFamily, market?.priceUsd); + if (supplyProof?.marketCapUsd !== undefined && market) { + market.marketCapUsd = supplyProof.marketCapUsd; + } + + const dbPoolEntries: ReportPoolEntry[] = resolvedPools.map((p) => ({ + poolAddress: p.poolAddress, + dex: p.dex, + token0: p.token0.address, + token1: p.token1.address, + token0Symbol: p.token0.symbol, + token1Symbol: p.token1.symbol, + tvl: p.tvl, + volume24h: p.volume24h, + source: 'indexed-db', + status: 'indexed', + statusReason: 'Pool is present in the token-aggregation indexed pool repository.', + })); + const liveUniswapV2PoolEntries = buildLiveUniswapV2FallbackPoolsForToken(chainId, address, dbPoolEntries); + const gruV2FallbackPoolEntries = await buildGruV2FallbackPoolsForToken(chainId, address, [ + ...dbPoolEntries, + ...liveUniswapV2PoolEntries, + ]); + const reportPools = [ + ...dbPoolEntries, + ...liveUniswapV2PoolEntries, + ...gruV2FallbackPoolEntries, + ]; + out.push({ chainId, address: address.toLowerCase(), @@ -106,42 +683,22 @@ async function buildTokenReport(chainId: number) { type: spec.type, decimals: spec.decimals, currencyCode: spec.currencyCode, - registryFamily: getTokenRegistryFamily(spec), + registryFamily, familySymbol: spec.familySymbol, deploymentVersion: spec.deploymentVersion, deploymentStatus: spec.deploymentStatus, preferredForX402: spec.preferredForX402, liquiditySourceSymbol: spec.liquiditySourceSymbol, - market: marketData - ? { - priceUsd: marketData.priceUsd ?? fallbackPriceUsd, - volume24h: marketData.volume24h, - volume7d: marketData.volume7d, - volume30d: marketData.volume30d, - marketCapUsd: marketData.marketCapUsd, - liquidityUsd: marketData.liquidityUsd, - lastUpdated: marketData.lastUpdated?.toISOString() ?? '', - } - : fallbackPriceUsd !== undefined - ? { - priceUsd: fallbackPriceUsd, - volume24h: 0, - volume7d: 0, - volume30d: 0, - liquidityUsd: 0, - lastUpdated: `${getCanonicalPriceSnapshotGeneratedAt()}T00:00:00.000Z`, - } - : undefined, - pools: resolvedPools.map((p) => ({ - poolAddress: p.poolAddress, - dex: p.dex, - token0: p.token0.address, - token1: p.token1.address, - token0Symbol: p.token0.symbol, - token1Symbol: p.token1.symbol, - tvl: p.tvl, - volume24h: p.volume24h, - })), + logoURI, + originalLogoURI, + market, + totalSupply: supplyProof?.totalSupply, + totalSupplyRaw: supplyProof?.totalSupplyRaw, + circulatingSupply: supplyProof?.circulatingSupply, + circulatingSupplyFormula: supplyProof?.circulatingSupplyFormula, + supplyProofProvenance: supplyProof?.supplyProofProvenance, + trackerCaveats: supplyProof?.trackerCaveats, + pools: reportPools, fromDb: !!dbToken, }); } @@ -167,15 +724,42 @@ function describeToken(spec: { currencyCode?: string; registryFamily?: string }) function buildGruTransportOverview() { const gruTransportMetadata = getGruTransportMetadata(); - if (!gruTransportMetadata) return undefined; + const deploymentStatus = loadDeploymentStatusFile()?.data; + + if (!gruTransportMetadata && !deploymentStatus) return undefined; + + const activeTransportPairs = getActiveTransportPairs(); + const fallbackGasAssetFamilies = deploymentStatus ? buildGasAssetFamiliesFromDeploymentStatus(deploymentStatus) : []; + const fallbackRuntimePairs = deploymentStatus ? buildGasRuntimePairsFromDeploymentStatus(deploymentStatus) : []; + const gasAssetFamilies = gruTransportMetadata?.gasAssetFamilies ?? fallbackGasAssetFamilies; + const gasRedeemGroups = gruTransportMetadata?.gasRedeemGroups ?? []; + const gasProtocolExposure = gruTransportMetadata?.gasProtocolExposure ?? []; + const runtimePairs = activeTransportPairs.length > 0 ? activeTransportPairs : fallbackRuntimePairs; return { - system: gruTransportMetadata.system, - summary: gruTransportMetadata.counts, - gasAssetFamilies: gruTransportMetadata.gasAssetFamilies ?? [], - gasRedeemGroups: gruTransportMetadata.gasRedeemGroups ?? [], - gasProtocolExposure: gruTransportMetadata.gasProtocolExposure ?? [], - activeTransportPairs: getActiveTransportPairs().map((pair) => ({ + system: gruTransportMetadata?.system ?? { + name: 'GRU Monetary Transport Layer', + shortName: 'GRU Transport', + canonicalChainId: 138, + canonicalChainName: 'DeFi Oracle Meta Chain 138', + transportClass: 'deployment-status-fallback', + }, + summary: gruTransportMetadata?.counts ?? { + enabledCanonicalTokens: 0, + enabledDestinationChains: new Set(runtimePairs.map((pair) => pair.destinationChainId)).size, + approvedBridgePeers: 0, + transportPairs: runtimePairs.length, + gasAssetFamilies: gasAssetFamilies.length, + gasRedeemGroups: gasRedeemGroups.length, + gasProtocolExposure: gasProtocolExposure.length, + gasTransportPairs: runtimePairs.length, + runtimeReadyTransportPairs: runtimePairs.filter((pair) => pair.runtimeReady === true).length, + publicPools: 0, + }, + gasAssetFamilies, + gasRedeemGroups, + gasProtocolExposure, + activeTransportPairs: runtimePairs.map((pair) => ({ key: pair.key, canonicalSymbol: pair.canonicalSymbol, mirroredSymbol: pair.mirroredSymbol, @@ -200,6 +784,173 @@ function buildGruTransportOverview() { }; } +function gasFamilyDefaults(familyKey: string, mirroredSymbol: string) { + const byFamily: Record = { + eth_mainnet: { + canonicalSymbol138: 'cETH', + assetClass: 'gas_native', + backingMode: 'strict_escrow', + redeemPolicy: 'same_family_only', + laneGroup: 'eth_mainnet', + }, + eth_l2: { + canonicalSymbol138: 'cETHL2', + assetClass: 'gas_native', + backingMode: 'hybrid_cap', + redeemPolicy: 'same_family_only', + laneGroup: 'eth_l2', + }, + bnb: { + canonicalSymbol138: 'cBNB', + assetClass: 'gas_native', + backingMode: 'strict_escrow', + redeemPolicy: 'same_family_only', + laneGroup: 'bnb', + }, + pol: { + canonicalSymbol138: 'cPOL', + assetClass: 'gas_native', + backingMode: 'strict_escrow', + redeemPolicy: 'same_family_only', + laneGroup: 'pol', + }, + avax: { + canonicalSymbol138: 'cAVAX', + assetClass: 'gas_native', + backingMode: 'strict_escrow', + redeemPolicy: 'same_family_only', + laneGroup: 'avax', + }, + cro: { + canonicalSymbol138: 'cCRO', + assetClass: 'gas_native', + backingMode: 'strict_escrow', + redeemPolicy: 'same_family_only', + laneGroup: 'cro', + }, + xdai: { + canonicalSymbol138: 'cXDAI', + assetClass: 'gas_native', + backingMode: 'strict_escrow', + redeemPolicy: 'same_family_only', + laneGroup: 'xdai', + }, + celo: { + canonicalSymbol138: 'cCELO', + assetClass: 'gas_native', + backingMode: 'strict_escrow', + redeemPolicy: 'same_family_only', + laneGroup: 'celo', + }, + wemix: { + canonicalSymbol138: 'cWEMIX', + assetClass: 'gas_native', + backingMode: 'strict_escrow', + redeemPolicy: 'same_family_only', + laneGroup: 'wemix', + }, + }; + + return byFamily[familyKey] ?? { + canonicalSymbol138: mirroredSymbol.replace(/^cW/, 'c'), + assetClass: 'gas_native', + backingMode: 'strict_escrow', + redeemPolicy: 'same_family_only', + laneGroup: familyKey, + }; +} + +function buildGasAssetFamiliesFromDeploymentStatus(data: DeploymentStatusFile): GruTransportGasAssetFamily[] { + const byFamily = new Map(); + + for (const [chainIdText, chain] of Object.entries(data.chains ?? {})) { + const chainId = Number(chainIdText); + for (const pool of chain.gasPmmPools ?? []) { + const familyKey = typeof pool.familyKey === 'string' ? pool.familyKey : ''; + const mirroredSymbol = typeof pool.base === 'string' ? pool.base : ''; + if (!familyKey || !mirroredSymbol) continue; + const defaults = gasFamilyDefaults(familyKey, mirroredSymbol); + const existing = byFamily.get(familyKey) ?? { + familyKey, + active: true, + status: 'deployment_status_fallback', + canonicalSymbol138: defaults.canonicalSymbol138, + mirroredSymbol, + assetClass: defaults.assetClass, + originChains: [], + laneGroup: defaults.laneGroup, + backingMode: defaults.backingMode, + redeemPolicy: defaults.redeemPolicy, + wrappedNativeQuoteSymbol: 'WETH', + stableQuoteSymbol: 'USDC', + referenceVenue: 'deployment-status', + }; + if (!existing.originChains.includes(chainId)) { + existing.originChains.push(chainId); + } + if (typeof pool.quote === 'string' && ['WETH', 'WBNB', 'WPOL', 'WAVAX', 'WCRO', 'WXDAI', 'CELO', 'WEMIX'].includes(pool.quote)) { + existing.wrappedNativeQuoteSymbol = pool.quote; + } + if (typeof pool.quote === 'string' && ['USDC', 'USDT', 'DAI'].includes(pool.quote)) { + existing.stableQuoteSymbol = pool.quote; + } + byFamily.set(familyKey, existing); + } + } + + return Array.from(byFamily.values()).sort((a, b) => a.familyKey.localeCompare(b.familyKey)); +} + +function buildGasRuntimePairsFromDeploymentStatus(data: DeploymentStatusFile): GruTransportPair[] { + const familiesByKey = new Map(buildGasAssetFamiliesFromDeploymentStatus(data).map((family) => [family.familyKey, family])); + const pairs: GruTransportPair[] = []; + + for (const [chainIdText, chain] of Object.entries(data.chains ?? {})) { + const destinationChainId = Number(chainIdText); + const seen = new Set(); + for (const pool of chain.gasPmmPools ?? []) { + const familyKey = typeof pool.familyKey === 'string' ? pool.familyKey : ''; + const mirroredSymbol = typeof pool.base === 'string' ? pool.base : ''; + if (!familyKey || !mirroredSymbol || seen.has(familyKey)) continue; + seen.add(familyKey); + const family = familiesByKey.get(familyKey); + const canonicalSymbol = family?.canonicalSymbol138 ?? gasFamilyDefaults(familyKey, mirroredSymbol).canonicalSymbol138; + pairs.push({ + key: `138-${destinationChainId}-${canonicalSymbol}-${mirroredSymbol}`, + canonicalChainId: 138, + destinationChainId, + destinationChainName: chain.name ?? `Chain ${chainIdText}`, + active: true, + status: 'deployment_status_fallback', + canonicalSymbol, + mirroredSymbol, + mappingKey: `${canonicalSymbol}:${destinationChainId}`, + peerKey: `chain-${destinationChainId}`, + assetClass: 'gas_native', + familyKey, + laneGroup: family?.laneGroup, + backingMode: family?.backingMode, + redeemPolicy: family?.redeemPolicy, + wrappedNativeQuoteSymbol: family?.wrappedNativeQuoteSymbol, + stableQuoteSymbol: family?.stableQuoteSymbol, + mirrorDeploymentAddress: chain.gasMirrors?.[mirroredSymbol], + mirrorDeployed: !!chain.gasMirrors?.[mirroredSymbol], + canonicalEnabled: true, + destinationEnabled: true, + bridgeAvailable: chain.bridgeAvailable === true, + bridgePeerConfigured: chain.bridgeAvailable === true, + runtimeBridgeReady: chain.bridgeAvailable === true, + runtimeReady: chain.bridgeAvailable === true && !!chain.gasMirrors?.[mirroredSymbol], + eligible: true, + runtimeMissingRequirements: chain.bridgeAvailable === true ? [] : ['bridgeAvailable is not true in deployment-status'], + eligibilityBlockers: [], + }); + } + } + + return pairs.sort((a, b) => a.destinationChainId - b.destinationChainId || a.familyKey!.localeCompare(b.familyKey!)); +} + function buildCanonicalCwFallback(chainIdFilter?: number | null): CwRegistryChain[] { const grouped = new Map(); @@ -230,6 +981,42 @@ function buildCanonicalCwFallback(chainIdFilter?: number | null): CwRegistryChai .sort((a, b) => a.chainId - b.chainId); } +/** GET /report/assets/* — packaged report assets such as controlled token logos. */ +router.get(/^\/assets\/(.+)$/, (req: Request, res: Response) => { + const assetPath = String(req.params[0] ?? ''); + const publicRoot = path.resolve(__dirname, '../../../public'); + const resolved = path.resolve(publicRoot, assetPath); + + if (!resolved.startsWith(publicRoot) || !existsSync(resolved)) { + res.status(404).json({ error: 'Asset not found' }); + return; + } + + res.set('Cache-Control', 'public, max-age=86400, immutable'); + res.sendFile(resolved); +}); + +/** GET /report/logo/:symbol — extensionless public logo route for proxies that block static file extensions. */ +router.get('/logo/:symbol', (req: Request, res: Response) => { + const rawSymbol = String(req.params.symbol ?? '').trim(); + const publicRoot = path.resolve(__dirname, '../../../public/token-logos'); + const gruSymbol = rawSymbol.replace(/^cW(?=USD|EUR|GBP|AUD|JPY|CHF|CAD|XAU)/, 'c'); + const candidates = [ + rawSymbol === 'cWBTC' ? path.join(publicRoot, 'trustwallet/btc-logo.png') : '', + rawSymbol.startsWith('ipfs-') ? path.join(publicRoot, 'ipfs', rawSymbol.replace(/^ipfs-/, '')) : '', + path.join(publicRoot, 'gru', `${gruSymbol}.svg`), + ].filter(Boolean); + + const resolved = candidates.find((candidate) => existsSync(candidate)); + if (!resolved) { + res.status(404).json({ error: 'Logo not found' }); + return; + } + + res.set('Cache-Control', 'public, max-age=86400, immutable'); + res.sendFile(resolved); +}); + /** GET /report/cross-chain — cross-chain pools, bridge volume, atomic swaps (Chain 138, ALL Mainnet) */ router.get( '/cross-chain', @@ -338,7 +1125,15 @@ router.get( name: t.name, asset_platform_id: chainId === 138 ? 'defi-oracle-meta' : chainId === 651940 ? 'all-mainnet' : `chain-${chainId}`, decimals: t.decimals, + logo_uri: t.logoURI, + original_logo_uri: t.originalLogoURI, description: describeToken(t), + total_supply: t.totalSupply ? Number(t.totalSupply) : undefined, + total_supply_raw: t.totalSupplyRaw, + circulating_supply: t.circulatingSupply ? Number(t.circulatingSupply) : undefined, + circulating_supply_formula: t.circulatingSupplyFormula, + supply_proof_provenance: t.supplyProofProvenance, + tracker_caveats: t.trackerCaveats, market_data: t.market ? { current_price: { usd: t.market.priceUsd }, @@ -353,6 +1148,14 @@ router.get( dex_id: p.dex, tvl_usd: p.tvl, volume_24h_usd: p.volume24h, + source: p.source, + status: p.status, + status_reason: p.statusReason, + role: p.role, + section: p.section, + public_routing_enabled: p.publicRoutingEnabled, + base_symbol: p.token0Symbol, + quote_symbol: p.token1Symbol, })), })); @@ -395,6 +1198,14 @@ router.get( symbol: t.symbol, name: t.name, decimals: t.decimals, + logo_url: t.logoURI, + original_logo_url: t.originalLogoURI, + total_supply: t.totalSupply ? Number(t.totalSupply) : undefined, + total_supply_raw: t.totalSupplyRaw, + circulating_supply: t.circulatingSupply ? Number(t.circulatingSupply) : undefined, + circulating_supply_formula: t.circulatingSupplyFormula, + supply_proof_provenance: t.supplyProofProvenance, + tracker_caveats: t.trackerCaveats, volume_24h: t.market?.volume24h, market_cap: t.market?.marketCapUsd, liquidity_usd: t.market?.liquidityUsd ?? t.pools.reduce((s, p) => s + p.tvl, 0), @@ -405,6 +1216,12 @@ router.get( quote: p.token0 === t.address ? p.token1 : p.token0, liquidity_usd: p.tvl, volume_24h_usd: p.volume24h, + source: p.source, + status: p.status, + status_reason: p.statusReason, + role: p.role, + section: p.section, + public_routing_enabled: p.publicRoutingEnabled, })), })); @@ -456,12 +1273,16 @@ router.get( if (!isNaN(chainIdFilter as number)) { tokens = tokens.filter((t) => t.chainId === chainIdFilter); } + const normalizedTokens = tokens.map((token) => ({ + ...token, + logoURI: absolutePublicUrl(req, typeof token.logoURI === 'string' ? token.logoURI : undefined) ?? token.logoURI, + })); return res.json({ name: data.name ?? 'Token List', version: data.version ?? '1.0.0', timestamp: data.timestamp ?? new Date().toISOString(), - logoURI: data.logoURI, - tokens, + logoURI: absolutePublicUrl(req, data.logoURI) ?? data.logoURI, + tokens: normalizedTokens, }); } catch (err) { logger.error('TOKEN_LIST_JSON_URL fetch failed, using built-in token list:', err); @@ -481,6 +1302,7 @@ router.get( decimals: number; type: string; logoURI: string; + originalLogoURI?: string; registryFamily?: string; familySymbol?: string; deploymentVersion?: string; @@ -493,6 +1315,7 @@ router.get( for (const spec of specs) { const address = spec.addresses[chainId]; if (address) { + const originalLogoURI = getLogoUriForSpec(spec); list.push({ chainId, address: address.toLowerCase(), @@ -500,7 +1323,8 @@ router.get( name: spec.name, decimals: spec.decimals, type: spec.type, - logoURI: getLogoUriForSpec(spec), + logoURI: absoluteLogoUri(req, originalLogoURI, spec.symbol), + originalLogoURI, registryFamily: getTokenRegistryFamily(spec), familySymbol: spec.familySymbol, deploymentVersion: spec.deploymentVersion, @@ -515,7 +1339,7 @@ router.get( name: 'GRU Canonical Token List', version: '1.0.0', timestamp: new Date().toISOString(), - logoURI: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + logoURI: absolutePublicUrl(req, DBIS_CHAIN_138_LOGO_PATH), tokens: list, }); } catch (error) { @@ -586,6 +1410,150 @@ router.get('/gru-v2-pmm-pools', async (req: Request, res: Response) => { } }); +/** GET /report/adoption-readiness — summary gates for institutional and tracker/listing readiness. */ +router.get('/adoption-readiness', async (_req: Request, res: Response) => { + try { + const chainIds = getSupportedChainIds(); + const tokensByChain: Record>> = {}; + for (const chainId of chainIds) { + tokensByChain[chainId] = await buildTokenReport(chainId); + } + + const allTokens = Object.values(tokensByChain).flat(); + const candidates = allTokens.filter((token) => + isGruSupplyTrackedCandidate(token.symbol, token.type, token.registryFamily) + ); + const nonReportablePlaceholder = candidates.filter( + (token) => token.supplyProofProvenance?.source === 'deterministic-placeholder-address' + ); + const reportableCandidates = candidates.filter( + (token) => token.supplyProofProvenance?.source !== 'deterministic-placeholder-address' + ); + const proved = reportableCandidates.filter((token) => token.supplyProofProvenance && token.supplyProofProvenance.source !== 'missing-supply-proof'); + const proofRequired = reportableCandidates.filter((token) => token.supplyProofProvenance?.source === 'missing-supply-proof'); + const silent = reportableCandidates.filter((token) => !token.supplyProofProvenance); + const externalOfficialQuoteLiquidity = reportableCandidates.filter(hasExternalOfficialQuoteLiquidity); + const liquidityPositive = reportableCandidates.filter( + (token) => (token.market?.liquidityUsd ?? 0) > 0 || token.pools.some((pool) => pool.tvl > 0) || hasExternalOfficialQuoteLiquidity(token) + ); + const poolIndexed = reportableCandidates.filter((token) => token.pools.length > 0); + const liquidityMissing = reportableCandidates.filter( + (token) => (token.market?.liquidityUsd ?? 0) <= 0 && !token.pools.some((pool) => pool.tvl > 0) && !hasExternalOfficialQuoteLiquidity(token) + ); + const liquidityMissingWithPools = liquidityMissing.filter((token) => token.pools.length > 0); + const liquidityMissingWithoutPools = liquidityMissing.filter((token) => token.pools.length === 0); + const gruPools = getGruV2DeploymentPoolRows(); + const groupTokensByChain = (tokens: typeof candidates) => + tokens + .reduce>((groups, token) => { + let group = groups.find((row) => row.chainId === token.chainId); + if (!group) { + group = { chainId: token.chainId, count: 0, symbols: [] }; + groups.push(group); + } + group.count += 1; + group.symbols.push(token.symbol); + return groups; + }, []) + .map((group) => ({ + ...group, + symbols: Array.from(new Set(group.symbols)).sort(), + })) + .sort((a, b) => a.chainId - b.chainId); + + res.set('Cache-Control', 'public, max-age=0, must-revalidate'); + res.json({ + generatedAt: new Date().toISOString(), + scope: 'gru-c-and-cw-assets', + counts: { + candidates: candidates.length, + reportableCandidates: reportableCandidates.length, + nonReportablePlaceholder: nonReportablePlaceholder.length, + proved: proved.length, + proofRequired: proofRequired.length, + silent: silent.length, + poolIndexed: poolIndexed.length, + liquidityPositive: liquidityPositive.length, + liquidityMissing: liquidityMissing.length, + liquidityMissingWithPools: liquidityMissingWithPools.length, + liquidityMissingWithoutPools: liquidityMissingWithoutPools.length, + externalOfficialQuoteLiquidity: externalOfficialQuoteLiquidity.length, + gruV2Pools: gruPools.length, + gruV2PoolsWithStatus: gruPools.filter((pool) => !!pool.status).length, + }, + institutional: { + score: Math.round( + 100 * + ((silent.length === 0 ? 0.25 : 0) + + (proved.length / Math.max(reportableCandidates.length, 1)) * 0.35 + + (poolIndexed.length / Math.max(reportableCandidates.length, 1)) * 0.2 + + (gruPools.length > 0 && gruPools.every((pool) => !!pool.status) ? 0.2 : 0)) + ), + blockers: [ + proofRequired.length > 0 ? `${proofRequired.length} assets still require supply-proof artifacts.` : null, + silent.length > 0 ? `${silent.length} assets have no proof state.` : null, + gruPools.some((pool) => !pool.status) ? 'One or more GRU v2 pools lacks lifecycle status.' : null, + ].filter(Boolean), + }, + cryptoListing: { + score: Math.round( + 100 * + ((proved.length / Math.max(reportableCandidates.length, 1)) * 0.45 + + (liquidityPositive.length / Math.max(reportableCandidates.length, 1)) * 0.35 + + (poolIndexed.length / Math.max(reportableCandidates.length, 1)) * 0.2) + ), + blockers: [ + proofRequired.length > 0 ? 'Most c*/cW* assets are proof-gated for supply and market-cap claims.' : null, + liquidityPositive.length < reportableCandidates.length ? 'Not every reportable c*/cW* asset has positive indexed liquidity in the report API.' : null, + ].filter(Boolean), + }, + blockerInventory: { + proofRequiredByChain: groupTokensByChain(proofRequired), + liquidityMissingByChain: groupTokensByChain(liquidityMissing), + liquidityMissingWithPoolsByChain: groupTokensByChain(liquidityMissingWithPools), + liquidityMissingWithoutPoolsByChain: groupTokensByChain(liquidityMissingWithoutPools), + liquidityMissingDetails: liquidityMissing + .map((token) => ({ + chainId: token.chainId, + symbol: token.symbol, + address: token.address, + poolCount: token.pools.length, + zeroTvlPoolCount: token.pools.filter((pool) => pool.tvl <= 0).length, + marketLiquidityUsd: token.market?.liquidityUsd ?? 0, + category: token.pools.length > 0 ? 'configured_or_indexed_pools_zero_tvl' : 'no_visible_pool_binding', + })) + .sort((a, b) => a.chainId - b.chainId || a.symbol.localeCompare(b.symbol)), + externalOfficialQuoteLiquidityByChain: groupTokensByChain(externalOfficialQuoteLiquidity), + nonReportablePlaceholderByChain: groupTokensByChain(nonReportablePlaceholder), + gruV2PoolsMissingStatus: gruPools + .filter((pool) => !pool.status) + .map((pool) => ({ + chainId: pool.chainId, + poolAddress: pool.poolAddress, + baseSymbol: pool.baseSymbol, + quoteSymbol: pool.quoteSymbol, + })), + notes: [ + 'proof_required means the report intentionally withholds totalSupply, circulatingSupply, and marketCap claims for that asset.', + 'nonReportablePlaceholder means the configured address is intentionally visible for roadmap traceability but is not a deployed ERC-20 proof surface.', + 'liquidityMissing means no positive indexed liquidity is currently visible through the report API for that asset.', + 'External official quote mirrors (public-chain USDC/USDT and ALL AUSDC/AUSDT) are not GRU pool blockers when their canonical token binding is proved; their public venue liquidity is maintained by the issuer/external market and should not be confused with repo-native cW*/Chain 138 liquidity.', + 'A configured pool or registry entry is not the same thing as tracker-grade supply proof or live positive liquidity.', + ], + }, + proofRequiredSample: proofRequired.slice(0, 20).map((token) => ({ + chainId: token.chainId, + symbol: token.symbol, + address: token.address, + status: token.supplyProofProvenance?.status, + })), + }); + } catch (error) { + logger.error('Error building report/adoption-readiness:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + /** GET /report/gas-registry — live gas-family rollout registry from deployment-status.json plus GRU transport metadata. */ router.get('/gas-registry', async (req: Request, res: Response) => { try { @@ -593,7 +1561,9 @@ router.get('/gas-registry', async (req: Request, res: Response) => { const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null; const fileBackedRegistry = loadDeploymentStatusFile(); const gruTransport = buildGruTransportOverview(); - const runtimeGasPairs = getActiveTransportPairs() + const loaderRuntimePairs = getActiveTransportPairs(); + const fallbackRuntimePairs = fileBackedRegistry ? buildGasRuntimePairsFromDeploymentStatus(fileBackedRegistry.data) : []; + const runtimeGasPairs = (loaderRuntimePairs.length > 0 ? loaderRuntimePairs : fallbackRuntimePairs) .filter((pair) => pair.assetClass === 'gas_native') .map((pair) => ({ key: pair.key, diff --git a/services/token-aggregation/src/api/server.ts b/services/token-aggregation/src/api/server.ts index b9f3aa2..ee70ff3 100644 --- a/services/token-aggregation/src/api/server.ts +++ b/services/token-aggregation/src/api/server.ts @@ -1,4 +1,5 @@ import express, { Express, Request, Response, NextFunction } from 'express'; +import { Server } from 'http'; import path from 'path'; import { readFileSync, existsSync } from 'fs'; import cors from 'cors'; @@ -48,6 +49,7 @@ export class ApiServer { private indexerEnabled: boolean; private indexer: MultiChainIndexer | null; private omnlPoller: OmnlEventPoller | null; + private server: Server | null; private resolveTrustProxySetting(): boolean | number | string { const raw = (process.env.EXPRESS_TRUST_PROXY ?? process.env.TRUST_PROXY ?? '1').trim(); @@ -65,6 +67,7 @@ export class ApiServer { this.indexerEnabled = this.resolveFeatureFlag('ENABLE_INDEXER', true); this.indexer = this.indexerEnabled ? new MultiChainIndexer() : null; this.omnlPoller = this.resolveFeatureFlag('ENABLE_OMNL_EVENT_POLLER', false) ? new OmnlEventPoller() : null; + this.server = null; this.setupMiddleware(); this.setupRoutes(); @@ -106,6 +109,14 @@ export class ApiServer { }); next(); }); + + const publicPath = path.join(__dirname, '../../public'); + if (existsSync(publicPath)) { + this.app.use('/static', express.static(publicPath, { + immutable: true, + maxAge: '1d', + })); + } } private setupRoutes(): void { @@ -151,6 +162,39 @@ export class ApiServer { res.type('html').send(readFileSync(dashboardPath, 'utf8')); }); + // Public API catalog (register before routers so GET /api/v1 is not swallowed by middleware-only mounts) + const sendApiV1Catalog = (_req: Request, res: Response) => { + res.json({ + service: 'token-aggregation', + version: '1.0.0', + note: 'Prefix paths with your public API origin (e.g. https://explorer.d-bis.org).', + paths: { + catalog: '/api/v1', + health: '/health', + chains: '/api/v1/chains', + networks: '/api/v1/networks', + config: '/api/v1/config', + tokens: '/api/v1/tokens', + tokenDetail: '/api/v1/tokens/{address}', + quote: '/api/v1/quote', + bridgeRoutes: '/api/v1/bridge/routes', + bridgeStatus: '/api/v1/bridge/status', + bridgeMetrics: '/api/v1/bridge/metrics', + bridgePreflight: '/api/v1/bridge/preflight', + tokenMappingPairs: '/api/v1/token-mapping/pairs', + tokenMappingResolve: '/api/v1/token-mapping/resolve', + reportTokenList: '/api/v1/report/token-list', + routesTree: '/api/v1/routes/tree', + plannerProvidersCapabilities: '/api/v2/providers/capabilities', + plannerRoutesPlan: '/api/v2/routes/plan', + plannerIntentsPlan: '/api/v2/intents/plan', + plannerInternalExecutionPlan: '/api/v2/routes/internal-execution-plan', + }, + }); + }; + this.app.get('/api/v1', sendApiV1Catalog); + this.app.get('/api/v1/', sendApiV1Catalog); + // API routes this.app.use('/api/v1', tokenRoutes); this.app.use('/api/v1', configRoutes); @@ -206,21 +250,25 @@ export class ApiServer { async start(): Promise { try { + // Start server + this.server = this.app.listen(this.port, () => { + logger.info(`Token Aggregation Service listening on port ${this.port}`); + logger.info(`Health check: http://localhost:${this.port}/health`); + logger.info(`API: http://localhost:${this.port}/api/v1`); + }); + if (this.indexer) { - await this.indexer.initialize(); - await this.indexer.startAll(); + this.indexer + .initialize() + .then(() => this.indexer?.startAll()) + .catch((error) => { + logger.error('Token aggregation indexer failed after API startup:', error); + }); } else { logger.info('Token aggregation indexer disabled by ENABLE_INDEXER flag'); } this.omnlPoller?.start(); - - // Start server - this.app.listen(this.port, () => { - logger.info(`Token Aggregation Service listening on port ${this.port}`); - logger.info(`Health check: http://localhost:${this.port}/health`); - logger.info(`API: http://localhost:${this.port}/api/v1`); - }); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); @@ -230,6 +278,18 @@ export class ApiServer { async stop(): Promise { this.omnlPoller?.stop(); this.indexer?.stopAll(); + if (this.server) { + await new Promise((resolve, reject) => { + this.server?.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + this.server = null; + } logger.info('Server stopped'); } } diff --git a/services/token-aggregation/src/config/canonical-tokens.test.ts b/services/token-aggregation/src/config/canonical-tokens.test.ts index 60f8c59..bbe8858 100644 --- a/services/token-aggregation/src/config/canonical-tokens.test.ts +++ b/services/token-aggregation/src/config/canonical-tokens.test.ts @@ -44,6 +44,41 @@ describe('canonical cW token catalog', () => { expect(getCanonicalTokenByAddress(56, '0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55')?.symbol).toBe('cWUSDW'); }); + it('keeps Cronos and the broader wrapped fiat/commodity family in the canonical cW mesh', () => { + const cronosCwUsdc = getCanonicalTokenBySymbol(25, 'cWUSDC'); + expect(cronosCwUsdc).toMatchObject({ + symbol: 'cWUSDC', + type: 'w', + currencyCode: 'USD', + }); + expect(cronosCwUsdc?.addresses[25]).toBe('0x932566E5bB6BEBF6B035B94f3DE1f75f126304Ec'); + expect(getCanonicalTokenByAddress(25, '0x932566E5bB6BEBF6B035B94f3DE1f75f126304Ec')?.symbol).toBe('cWUSDC'); + + const expected = [ + ['cWEURC', 'EUR', '0x7574d37F42528B47c88962931e48FC61608a4050'], + ['cWEURT', 'EUR', '0x9f833b4f1012F52eb3317b09922a79c6EdFca77D'], + ['cWGBPC', 'GBP', '0xe5c65A76A541368d3061fe9E7A2140cABB903dbF'], + ['cWGBPT', 'GBP', '0xBb58fa16bAc8E789f09C14243adEE6480D8213A2'], + ['cWAUDC', 'AUD', '0xff3084410A732231472Ee9f93F5855dA89CC5254'], + ['cWJPYC', 'JPY', '0x52aD62B8bD01154e2A4E067F8Dc4144C9988d203'], + ['cWCHFC', 'CHF', '0xB55F49D6316322d5caA96D34C6e4b1003BD3E670'], + ['cWCADC', 'CAD', '0x32aD687F24F77bF8C86605c202c829163Ac5Ab36'], + ['cWXAUC', 'XAU', '0xf1B771c95573113E993374c0c7cB2dc1a7908B12'], + ['cWXAUT', 'XAU', '0xD517C0cF7013f988946A468c880Cc9F8e2A4BCbE'], + ] as const; + + for (const [symbol, currencyCode, cronosAddress] of expected) { + const token = getCanonicalTokenBySymbol(25, symbol); + expect(token).toMatchObject({ + symbol, + type: 'w', + currencyCode, + }); + expect(token?.addresses[25]).toBe(cronosAddress); + expect(getCanonicalTokenByAddress(25, cronosAddress)?.symbol).toBe(symbol); + } + }); + it('surfaces cUSDW on Chain 138 as the repo-native USDW hub asset', () => { const cusdw = getCanonicalTokenBySymbol(138, 'cUSDW'); expect(cusdw).toMatchObject({ diff --git a/services/token-aggregation/src/config/canonical-tokens.ts b/services/token-aggregation/src/config/canonical-tokens.ts index 09f755e..ddde2cd 100644 --- a/services/token-aggregation/src/config/canonical-tokens.ts +++ b/services/token-aggregation/src/config/canonical-tokens.ts @@ -70,7 +70,7 @@ const LEGACY_CHAIN_ENV_SUFFIX: Partial> = { }; /** L2/mainnet chain IDs for cUSDT/cUSDC multichain (env: CUSDT_ADDRESS_56, CUSDC_ADDRESS_137, etc.) */ const L2_CHAIN_IDS = [1, 56, 137, 10, 42161, 8453, 43114, 25, 100, 42220, 1111] as const; -const GRU_CW_CHAIN_IDS = [1, 56, 137, 10, 42161, 8453, 43114, 100, 42220] as const; +const GRU_CW_CHAIN_IDS = [1, 10, 25, 56, 100, 137, 8453, 42161, 42220, 43114] as const; const BTC_CW_CHAIN_IDS = [1, 10, 25, 56, 100, 137, 42161, 42220, 43114, 8453, 1111] as const; const ETH_MAINNET_CW_CHAIN_IDS = [1] as const; const ETH_L2_CW_CHAIN_IDS = [10, 42161, 8453] as const; @@ -165,33 +165,156 @@ const FALLBACK_ADDRESSES: Record>> = { cWAUSDT: { [56]: '0xe1a51Bc037a79AB36767561B147eb41780124934', [137]: '0xf12e262F85107df26741726b074606CaFa24AAe7', - [43114]: '0xff3084410A732231472Ee9f93F5855dA89CC5254', [42220]: '0xC158b6cD3A3088C52F797D41f5Aa02825361629e', + [43114]: '0xff3084410A732231472Ee9f93F5855dA89CC5254', }, cWUSDC: { [1]: '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a', - [56]: '0x5355148C4740fcc3D7a96F05EdD89AB14851206b', - [137]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4', - [100]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4', [10]: '0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105', - [42161]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', + [25]: '0x932566E5bB6BEBF6B035B94f3DE1f75f126304Ec', + [56]: '0x5355148C4740fcc3D7a96F05EdD89AB14851206b', + [100]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4', + [137]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4', [8453]: '0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105', - [43114]: '0x0C242b513008Cd49C89078F5aFb237A3112251EB', + [42161]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', [42220]: '0x4C38F9A5ed68A04cd28a72E8c68C459Ec34576f3', + [43114]: '0x0C242b513008Cd49C89078F5aFb237A3112251EB', }, cWUSDT: { [1]: '0xaF5017d0163ecb99D9B5D94e3b4D7b09Af44D8AE', - [56]: '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB', - [137]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', - [100]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', [10]: '0x04B2AE3c3bb3d70Df506FAd8717b0FBFC78ED7E6', - [42161]: '0x73ADaF7dBa95221c080db5631466d2bC54f6a76B', + [25]: '0x72948a7a813B60b37Cd0c920C4657DbFF54312b8', + [56]: '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB', + [100]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', + [137]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', [8453]: '0x04B2AE3c3bb3d70Df506FAd8717b0FBFC78ED7E6', - [43114]: '0x8142BA530B08f3950128601F00DaaA678213DFdf', + [42161]: '0x73ADaF7dBa95221c080db5631466d2bC54f6a76B', [42220]: '0x73376eB92c16977B126dB9112936A20Fa0De3442', + [43114]: '0x8142BA530B08f3950128601F00DaaA678213DFdf', + }, + cWEURC: { + [1]: '0xD4aEAa8cD3fB41Dc8437FaC7639B6d91B60A5e8d', + [10]: '0x4ab39b5bab7b463435209a9039bd40cf241f5a82', + [25]: '0x7574d37F42528B47c88962931e48FC61608a4050', + [56]: '0x50b073d0D1D2f002745cb9FC28a057d5be84911c', + [100]: '0x25603ae4bff0b71d637b3573d1b6657f5f6d17ef', + [137]: '0x3CD9ee18db7ad13616FCC1c83bC6098e03968E66', + [8453]: '0xcb145ba9a370681e3545f60e55621ebf218b1031', + [42161]: '0x2a0023ad5ce1ac6072b454575996dffb1bb11b16', + [42220]: '0xb6D2f38b9015F32ccE8818509c712264E7fceeD3', + [43114]: '0x84353ed1f0c7a703a17abad19b0db15bc9a5e3e5', + }, + cWEURT: { + [1]: '0x855d74FFB6CF75721a9bAbc8B2ed35c8119241dC', + [10]: '0x6f521cd9fcf7884cd4e9486c7790e818638e09dd', + [25]: '0x9f833b4f1012F52eb3317b09922a79c6EdFca77D', + [56]: '0x1ED9E491A5eCd53BeF21962A5FCE24880264F63f', + [100]: '0x8e54c52d34a684e22865ac9f2d7c27c30561a7b9', + [137]: '0xBeF5A0Bcc0E77740c910f197138cdD90F98d2427', + [8453]: '0x73e0cf8bf861d376b3a4c87c136f975027f045ff', + [42161]: '0x22b98130ab4d9c355512b25ade4c35e75a4e7e89', + [42220]: '0x7e6fB8D80f81430e560F8232b2A4fd06249d74ce', + [43114]: '0xfc7d256e48253f7a7e08f0e55b9ff7039eb2524c', + }, + cWGBPC: { + [1]: '0xc074007dc0bfb384b1cf6426a56287ed23fe4d52', + [10]: '0x3f8c409c6072a2b6a4ff17071927ba70f80c725f', + [25]: '0xe5c65A76A541368d3061fe9E7A2140cABB903dbF', + [56]: '0x8b6EE72001cAFcb21D56a6c4686D6Db951d499A6', + [100]: '0x4d9bc6c74ba65e37c4139f0aec9fc5ddff28dcc4', + [137]: '0x948690147D2e50ffe50C5d38C14125aD6a9FA036', + [8453]: '0x2a0023ad5ce1ac6072b454575996dffb1bb11b16', + [42161]: '0xa846aead3071df1b6439d5d813156ace7c2c1da1', + [42220]: '0xE37c332a88f112F9e039C5d92D821402A89c7052', + [43114]: '0xbdf0c4ea1d81e8e769b0f41389a2c733e3ff723e', + }, + cWGBPT: { + [1]: '0x1dDF9970F01c76A692Fdba2706203E6f16e0C46F', + [10]: '0x456373d095d6b9260f01709f93fccf1d8aa14d11', + [25]: '0xBb58fa16bAc8E789f09C14243adEE6480D8213A2', + [56]: '0xA6eFb8783C8ad2740ec880e46D4f7E608E893B1B', + [100]: '0x9f6d2578003fe04e58a9819a4943732f2a203a61', + [137]: '0x58a8D8F78F1B65c06dAd7542eC46b299629A60dd', + [8453]: '0x22b98130ab4d9c355512b25ade4c35e75a4e7e89', + [42161]: '0x29828e9ab2057cd3df3c9211455ae1f76e53d2af', + [42220]: '0x1dBa81f91f1BeC47FFf60eC3e7DeD780ad9968E3', + [43114]: '0x4611d3424e059392a52b957e508273bc761c80f2', + }, + cWAUDC: { + [1]: '0x5020Db641B3Fc0dAbBc0c688C845bc4E3699f35F', + [10]: '0x25603ae4bff0b71d637b3573d1b6657f5f6d17ef', + [25]: '0xff3084410A732231472Ee9f93F5855dA89CC5254', + [56]: '0x7062f35567BBAb4d98dc33af03B0d14Df42294D5', + [100]: '0xddc4063f770f7c49d00b5a10fb552e922aa39b2c', + [137]: '0xFb4B6Cc81211F7d886950158294A44C312abCA29', + [8453]: '0xa846aead3071df1b6439d5d813156ace7c2c1da1', + [42161]: '0xc1535e88578d984f12eab55863376b8d8b9fb05a', + [42220]: '0x2d3a2ED4Ca4d69912d217c305EE921609F7906A8', + [43114]: '0x04e1e22b0d41e99f4275bd40a50480219bc9a223', + }, + cWJPYC: { + [1]: '0x07EEd0D7dD40984e47B9D3a3bdded1c536435582', + [10]: '0x8e54c52d34a684e22865ac9f2d7c27c30561a7b9', + [25]: '0x52aD62B8bD01154e2A4E067F8Dc4144C9988d203', + [56]: '0x5fbCE65524211BC1bFb0309fd9EE09E786c6D097', + [100]: '0x145e8e8c49b6a021969dd9d2c01c8fea44374f61', + [137]: '0xf9f5D0ACD71C76F9476F10B3F3d3E201F0883C68', + [8453]: '0x29828e9ab2057cd3df3c9211455ae1f76e53d2af', + [42161]: '0xdc383c489533a4dd9a6bd3007386e25d5078b878', + [42220]: '0x0b39F47D2E68aB0eB18d4b637Bbd1dD8E97cFbB5', + [43114]: '0x3714b1a312e0916c7dcdc4edf480fc0339e59a59', + }, + cWCHFC: { + [1]: '0x0F91C5E6Ddd46403746aAC970D05d70FFe404780', + [10]: '0x4d9bc6c74ba65e37c4139f0aec9fc5ddff28dcc4', + [25]: '0xB55F49D6316322d5caA96D34C6e4b1003BD3E670', + [56]: '0xD9f8710caeeBA3b3D423D7D14a918701426B5ef3', + [100]: '0x46d90d7947f1139477c206c39268923b99cf09e4', + [137]: '0xeE17bB0322383fecCA2784fbE2d4CD7d02b1905B', + [8453]: '0xc1535e88578d984f12eab55863376b8d8b9fb05a', + [42161]: '0x7e4b4682453bcce19ec903fb69153d3031986bc4', + [42220]: '0x8142BA530B08f3950128601F00DaaA678213DFdf', + [43114]: '0xc2fa05f12a75ac84ea778af9d6935ca807275e55', + }, + cWCADC: { + [1]: '0x209FE32fe7B541751D190ae4e50cd005DcF8EDb4', + [10]: '0x9f6d2578003fe04e58a9819a4943732f2a203a61', + [25]: '0x32aD687F24F77bF8C86605c202c829163Ac5Ab36', + [56]: '0x9AE7a6B311584D60Fa93f973950d609061875775', + [100]: '0xa7133c78e0ec74503a5941bcbd44257615b6b4f6', + [137]: '0xc9750828124D4c10e7a6f4B655cA8487bD3842EB', + [8453]: '0xdc383c489533a4dd9a6bd3007386e25d5078b878', + [42161]: '0xcc6ae6016d564e9ab82aaff44d65e05a9b18951c', + [42220]: '0x0C242b513008Cd49C89078F5aFb237A3112251EB', + [43114]: '0x1872e033b30f3ce0498847926857433e0146394e', + }, + cWXAUC: { + [1]: '0x572Be0fa8CA0534d642A567CEDb398B771D8a715', + [10]: '0xddc4063f770f7c49d00b5a10fb552e922aa39b2c', + [25]: '0xf1B771c95573113E993374c0c7cB2dc1a7908B12', + [56]: '0xCB145bA9A370681e3545F60e55621eBf218B1031', + [100]: '0x23873b85cfeb343eb952618e8c9e9bfb7f6a0d45', + [137]: '0x328Cd365Bb35524297E68ED28c6fF2C9557d1363', + [8453]: '0x7e4b4682453bcce19ec903fb69153d3031986bc4', + [42161]: '0xa7762b63c4871581885ad17c5714ebb286a7480b', + [42220]: '0x61D642979eD75c1325f35b9275C5A7FE97F22451', + [43114]: '0x4f95297c23d9f4a1032b1c6a2e553225cb175bee', + }, + cWXAUT: { + [1]: '0xACE1DBF857549a11aF1322e1f91F2F64b029c906', + [10]: '0x145e8e8c49b6a021969dd9d2c01c8fea44374f61', + [25]: '0xD517C0cF7013f988946A468c880Cc9F8e2A4BCbE', + [56]: '0x73E0CF8BF861D376B3a4C87c136F975027f045ff', + [100]: '0xc6189d404dc60cae7b48e2190e44770a03193e5f', + [137]: '0x9e6044d730d4183bF7a666293d257d035Fba6d44', + [8453]: '0xcc6ae6016d564e9ab82aaff44d65e05a9b18951c', + [42161]: '0x66568899ffe8f00b25dc470e878b65a478994e76', + [42220]: '0x30751782486eed825187C1EAe5DE4b4baD428AaE', + [43114]: '0xd2b4dbf2f6bd6704e066d752eec61fb0be953fd3', }, cWUSDW: { [56]: '0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55', + [42220]: '0x176a1b6Aa59F24B3aa65F2b697AB262Bca9093B5', [43114]: '0xcfdCe5E660FC2C8052BDfa7aEa1865DD753411Ae', }, cWBTC: { @@ -236,7 +359,7 @@ const FALLBACK_ADDRESSES: Record>> = { cWWEMIX: { [1111]: '0xc111000000000000000000000000000000000457', }, - // Compliant Fiat on Chain 138 — from DeployCompliantFiatTokens (2026-02-27) + // Compliant Fiat on Chain 138 - from DeployCompliantFiatTokens (2026-02-27) cEURC: { [CHAIN_138]: '0x8085961F9cF02b4d800A3c6d386D31da4B34266a' }, cEURT: { [CHAIN_138]: '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72' }, cGBPC: { [CHAIN_138]: '0x003960f16D9d34F2e98d62723B6721Fb92074aD2' }, @@ -247,9 +370,10 @@ const FALLBACK_ADDRESSES: Record>> = { cCADC: { [CHAIN_138]: '0x54dBd40cF05e15906A2C21f600937e96787f5679' }, cXAUC: { [CHAIN_138]: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b' }, cXAUT: { [CHAIN_138]: '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E' }, + LINK: { [CHAIN_138]: '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03' }, WETH: { [CHAIN_138]: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' }, WETH10: { [CHAIN_138]: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f' }, - // ISO-4217W on Cronos (25) — from DeployISO4217WSystem + // ISO-4217W on Cronos (25) - from DeployISO4217WSystem USDW: { [CHAIN_25]: '0x948690147D2e50ffe50C5d38C14125aD6a9FA036' }, EURW: { [CHAIN_25]: '0x58a8D8F78F1B65c06dAd7542eC46b299629A60dd' }, GBPW: { [CHAIN_25]: '0xFb4B6Cc81211F7d886950158294A44C312abCA29' }, @@ -451,6 +575,16 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [ { symbol: 'cWAUSDT', name: 'Alltra USD Token (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form for the live Chain 138 cAUSDT surface.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWAUSDT', id)])) } }, { symbol: 'cWUSDC', name: 'USD Coin (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDC', id)])) } }, { symbol: 'cWUSDT', name: 'Tether USD (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDT', id)])) } }, + { symbol: 'cWEURC', name: 'Euro Coin (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'EUR', description: 'Public-network mirrored transport form of canonical Chain 138 cEURC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWEURC', id)])) } }, + { symbol: 'cWEURT', name: 'Tether EUR (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'EUR', description: 'Public-network mirrored transport form of canonical Chain 138 cEURT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWEURT', id)])) } }, + { symbol: 'cWGBPC', name: 'Pound Sterling (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'GBP', description: 'Public-network mirrored transport form of canonical Chain 138 cGBPC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWGBPC', id)])) } }, + { symbol: 'cWGBPT', name: 'Tether GBP (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'GBP', description: 'Public-network mirrored transport form of canonical Chain 138 cGBPT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWGBPT', id)])) } }, + { symbol: 'cWAUDC', name: 'Australian Dollar (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'AUD', description: 'Public-network mirrored transport form of canonical Chain 138 cAUDC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWAUDC', id)])) } }, + { symbol: 'cWJPYC', name: 'Japanese Yen (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'JPY', description: 'Public-network mirrored transport form of canonical Chain 138 cJPYC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWJPYC', id)])) } }, + { symbol: 'cWCHFC', name: 'Swiss Franc (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'CHF', description: 'Public-network mirrored transport form of canonical Chain 138 cCHFC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWCHFC', id)])) } }, + { symbol: 'cWCADC', name: 'Canadian Dollar (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'CAD', description: 'Public-network mirrored transport form of canonical Chain 138 cCADC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWCADC', id)])) } }, + { symbol: 'cWXAUC', name: 'Gold (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'XAU', registryFamily: 'commodity', description: 'Public-network mirrored transport form of canonical Chain 138 cXAUC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWXAUC', id)])) } }, + { symbol: 'cWXAUT', name: 'Tether XAU (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'XAU', registryFamily: 'commodity', description: 'Public-network mirrored transport form of canonical Chain 138 cXAUT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWXAUT', id)])) } }, { symbol: 'cWUSDW', name: 'USD W (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDW.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDW', id)])) } }, { symbol: 'WETH', @@ -472,6 +606,16 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [ description: 'Chain 138 WETH10 pilot wrapped ETH surface used by DODO v3 routing and flash-capable paths.', addresses: { [CHAIN_138]: addr('WETH10', CHAIN_138) || '' }, }, + { + symbol: 'LINK', + name: 'Chainlink Token', + type: 'base', + decimals: 18, + currencyCode: 'LINK', + registryFamily: 'unclassified', + description: 'Chain 138 LINK token used for CCIP and oracle fee accounting.', + addresses: { [CHAIN_138]: addr('LINK', CHAIN_138) || '' }, + }, { symbol: 'cWBTC', name: 'Bitcoin (Compliant Wrapped Monetary Unit)', @@ -730,7 +874,18 @@ export function resolveCanonicalQuoteAddress(chainId: number, address: string): const IPFS_GATEWAY = 'https://ipfs.io/ipfs'; const GRU_LOGO_BASE = 'https://raw.githubusercontent.com/Order-of-Hospitallers/proxmox-cp/main/token-lists/logos/gru'; -const ETH_LOGO = `${IPFS_GATEWAY}/Qma3FKtLce9MjgJgWbtyCxBiPjJ6xi8jGWUSKNS5Jc2ong`; +const ETH_LOGO = '/api/v1/report/logo/ETH'; +const LINK_LOGO = '/api/v1/report/logo/LINK'; +const GAS_NATIVE_LOGO_BY_CODE: Record = { + ETH: ETH_LOGO, + BNB: '/api/v1/report/logo/BNB', + POL: '/api/v1/report/logo/POL', + AVAX: '/api/v1/report/logo/AVAX', + CRO: '/api/v1/report/logo/CRO', + XDAI: '/api/v1/report/logo/XDAI', + CELO: '/api/v1/report/logo/CELO', + WEMIX: '/api/v1/report/logo/WEMIX', +}; const USDC_LOGO = `${GRU_LOGO_BASE}/cUSDC.svg`; const USDT_LOGO = `${GRU_LOGO_BASE}/cUSDT.svg`; const BTC_LOGO = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/bitcoin/info/logo.png'; @@ -750,8 +905,10 @@ const LOGO_BY_SYMBOL: Record = { cWUSDC: USDC_LOGO, cWUSDT: USDT_LOGO, cWUSDW: USDC_LOGO, + cWEMIX: GAS_NATIVE_LOGO_BY_CODE.WEMIX, WETH: ETH_LOGO, WETH10: ETH_LOGO, + LINK: LINK_LOGO, cEURC: `${GRU_LOGO_BASE}/cEURC.svg`, cEURT: `${GRU_LOGO_BASE}/cEURT.svg`, cGBPC: `${GRU_LOGO_BASE}/cGBPC.svg`, @@ -781,6 +938,15 @@ export function getLogoUriForSpec(spec: CanonicalTokenSpec): string { if (spec.logoUrl) return spec.logoUrl; const bySymbol = LOGO_BY_SYMBOL[spec.symbol]; if (bySymbol) return bySymbol; + const gasLogo = spec.registryFamily === 'gas_native' && spec.currencyCode + ? GAS_NATIVE_LOGO_BY_CODE[spec.currencyCode.toUpperCase()] + : undefined; + if (gasLogo) return gasLogo; + if (spec.symbol.startsWith('cW')) { + const hubSymbol = `c${spec.symbol.slice(2)}`; + const hubSpec = CANONICAL_TOKENS.find((t) => t.symbol === hubSymbol); + if (hubSpec && hubSpec.symbol !== spec.symbol) return getLogoUriForSpec(hubSpec); + } if (spec.symbol.startsWith('ac')) return getLogoUriForSpec(CANONICAL_TOKENS.find((t) => t.symbol === spec.symbol.replace('ac', 'c')) || spec); if (spec.symbol.startsWith('vdc') || spec.symbol.startsWith('sdc')) { const base = spec.symbol.replace(/^(vd|sd)c/, 'c'); diff --git a/services/token-aggregation/src/config/chains.ts b/services/token-aggregation/src/config/chains.ts index c82958c..be12534 100644 --- a/services/token-aggregation/src/config/chains.ts +++ b/services/token-aggregation/src/config/chains.ts @@ -81,7 +81,7 @@ export const CHAIN_CONFIGS: Record = { 137: { chainId: 137, name: 'Polygon', - rpcUrl: process.env.CHAIN_137_RPC_URL || 'https://polygon-rpc.com', + rpcUrl: process.env.CHAIN_137_RPC_URL || 'https://polygon-bor-rpc.publicnode.com', explorerUrl: 'https://polygonscan.com', nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 }, blockTime: 2, diff --git a/services/token-aggregation/src/config/deployment-status.ts b/services/token-aggregation/src/config/deployment-status.ts index 3de3873..5d2d905 100644 --- a/services/token-aggregation/src/config/deployment-status.ts +++ b/services/token-aggregation/src/config/deployment-status.ts @@ -69,6 +69,9 @@ function buildDeploymentStatusCandidates(): string[] { process.env.DEPLOYMENT_STATUS_JSON_PATH, process.env.CW_REGISTRY_JSON_PATH, process.env.CROSS_CHAIN_PMM_DEPLOYMENT_STATUS_PATH, + process.env.PROXMOX_REPO_ROOT + ? path.resolve(process.env.PROXMOX_REPO_ROOT, 'cross-chain-pmm-lps/config/deployment-status.json') + : undefined, path.resolve(process.cwd(), 'cross-chain-pmm-lps/config/deployment-status.json'), path.resolve(process.cwd(), '..', 'cross-chain-pmm-lps/config/deployment-status.json'), path.resolve(process.cwd(), '..', '..', 'cross-chain-pmm-lps/config/deployment-status.json'), diff --git a/services/token-aggregation/src/config/gru-v2-deployment-pools.ts b/services/token-aggregation/src/config/gru-v2-deployment-pools.ts index 95ed486..def187a 100644 --- a/services/token-aggregation/src/config/gru-v2-deployment-pools.ts +++ b/services/token-aggregation/src/config/gru-v2-deployment-pools.ts @@ -7,6 +7,8 @@ export interface GruV2DeploymentPoolRow { chainId: number; chainName: string; section: GruV2PmmSection; + status: 'live' | 'routing_enabled' | 'configured' | 'proof_required'; + statusReason: string; baseSymbol: string; quoteSymbol: string; baseAddress: string; @@ -101,6 +103,16 @@ export function buildGruV2PoolRegistryFromDeploymentData(data: DeploymentStatusF chainId, chainName, section, + status: + pool.publicRoutingEnabled === true + ? 'routing_enabled' + : poolAddress.startsWith('0x') + ? 'live' + : 'configured', + statusReason: + pool.publicRoutingEnabled === true + ? 'Pool address is configured in deployment-status and public routing is enabled.' + : 'Pool address is configured in deployment-status; routing enablement is not asserted.', baseSymbol, quoteSymbol, baseAddress, diff --git a/services/token-aggregation/src/config/networks.ts b/services/token-aggregation/src/config/networks.ts index e1d52ce..8967f35 100644 --- a/services/token-aggregation/src/config/networks.ts +++ b/services/token-aggregation/src/config/networks.ts @@ -34,8 +34,9 @@ export const NETWORKS: NetworkEntry[] = [ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, blockExplorerUrls: ['https://explorer.d-bis.org'], iconUrls: [ + 'https://explorer.d-bis.org/api/v1/report/logo/chain-138', + 'https://explorer.d-bis.org/token-icons/chain-138.png', 'https://explorer.d-bis.org/favicon.ico', - 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', ], oracles: [ { name: 'ETH/USD', address: '0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6', decimals: 8 }, @@ -87,7 +88,13 @@ export const NETWORKS: NetworkEntry[] = [ chainId: '0x89', chainIdDecimal: 137, chainName: 'Polygon', - rpcUrls: ['https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'], + rpcUrls: [ + 'https://polygon-bor-rpc.publicnode.com', + 'https://1rpc.io/matic', + 'https://polygon.drpc.org', + 'https://polygon-rpc.com', + 'https://rpc.ankr.com/polygon', + ], nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 }, blockExplorerUrls: ['https://polygonscan.com'], iconUrls: ['https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/polygon/info/logo.png'], diff --git a/services/token-aggregation/src/database/client.ts b/services/token-aggregation/src/database/client.ts index 7844e97..a9b5e7c 100644 --- a/services/token-aggregation/src/database/client.ts +++ b/services/token-aggregation/src/database/client.ts @@ -26,6 +26,7 @@ export function getDatabasePool(): Pool { connectionString: process.env.DATABASE_URL, min: parseInt(process.env.DATABASE_POOL_MIN || '2', 10), max: parseInt(process.env.DATABASE_POOL_MAX || '10', 10), + connectionTimeoutMillis: parseInt(process.env.DATABASE_CONNECTION_TIMEOUT_MS || '3000', 10), }; // If connectionString is not provided, use individual config diff --git a/services/token-aggregation/src/database/repositories/market-data-repo.ts b/services/token-aggregation/src/database/repositories/market-data-repo.ts index 917c700..37c697d 100644 --- a/services/token-aggregation/src/database/repositories/market-data-repo.ts +++ b/services/token-aggregation/src/database/repositories/market-data-repo.ts @@ -19,7 +19,16 @@ export class MarketDataRepository { return code === '42P01' || (message.includes('relation "') && message.includes('" does not exist')); } + private shouldUseReadFallback(error: unknown): boolean { + if (this.isMissingRelationError(error)) return true; + if (String(process.env.TOKEN_AGGREGATION_DB_READ_FALLBACK ?? '1').toLowerCase() === '0') return false; + const message = (error as { message?: string })?.message || ''; + const code = (error as { code?: string })?.code || ''; + return ['ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND'].includes(code) || /timeout|connect/i.test(message); + } + async getMarketData(chainId: number, tokenAddress: string): Promise { + if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return null; try { const result = await this.pool.query( `SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, @@ -49,7 +58,7 @@ export class MarketDataRepository { lastUpdated: row.last_updated, }; } catch (error) { - if (this.isMissingRelationError(error)) { + if (this.shouldUseReadFallback(error)) { return null; } throw error; @@ -99,6 +108,7 @@ export class MarketDataRepository { } async getTopTokensByVolume(chainId: number, limit: number = 50): Promise { + if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return []; try { const result = await this.pool.query( `SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, @@ -125,7 +135,7 @@ export class MarketDataRepository { lastUpdated: row.last_updated, })); } catch (error) { - if (this.isMissingRelationError(error)) { + if (this.shouldUseReadFallback(error)) { return []; } throw error; @@ -133,6 +143,7 @@ export class MarketDataRepository { } async getTopTokensByLiquidity(chainId: number, limit: number = 50): Promise { + if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return []; try { const result = await this.pool.query( `SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, @@ -159,7 +170,7 @@ export class MarketDataRepository { lastUpdated: row.last_updated, })); } catch (error) { - if (this.isMissingRelationError(error)) { + if (this.shouldUseReadFallback(error)) { return []; } throw error; diff --git a/services/token-aggregation/src/database/repositories/pool-repo.ts b/services/token-aggregation/src/database/repositories/pool-repo.ts index 4c5a28b..e4143c2 100644 --- a/services/token-aggregation/src/database/repositories/pool-repo.ts +++ b/services/token-aggregation/src/database/repositories/pool-repo.ts @@ -53,7 +53,16 @@ export class PoolRepository { return code === '42P01' || message.includes('relation "') && message.includes('" does not exist'); } + private shouldUseReadFallback(error: unknown): boolean { + if (this.isMissingRelationError(error)) return true; + if (String(process.env.TOKEN_AGGREGATION_DB_READ_FALLBACK ?? '1').toLowerCase() === '0') return false; + const message = (error as { message?: string })?.message || ''; + const code = (error as { code?: string })?.code || ''; + return ['ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND'].includes(code) || /timeout|connect/i.test(message); + } + async getPool(chainId: number, poolAddress: string): Promise { + if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return null; try { const result = await this.pool.query( `SELECT id, chain_id, pool_address, token0_address, token1_address, dex_type, @@ -70,7 +79,7 @@ export class PoolRepository { return this.mapRowToPool(result.rows[0]); } catch (error) { - if (this.isMissingRelationError(error)) { + if (this.shouldUseReadFallback(error)) { return null; } throw error; @@ -78,6 +87,7 @@ export class PoolRepository { } async getPoolsByChain(chainId: number, limit: number = 500): Promise { + if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return []; try { const result = await this.pool.query( `SELECT id, chain_id, pool_address, token0_address, token1_address, dex_type, @@ -91,7 +101,7 @@ export class PoolRepository { ); return result.rows.map((row) => this.mapRowToPool(row)); } catch (error) { - if (this.isMissingRelationError(error)) { + if (this.shouldUseReadFallback(error)) { return []; } throw error; @@ -99,6 +109,7 @@ export class PoolRepository { } async getPoolsByToken(chainId: number, tokenAddress: string): Promise { + if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return []; try { const result = await this.pool.query( `SELECT id, chain_id, pool_address, token0_address, token1_address, dex_type, @@ -112,7 +123,7 @@ export class PoolRepository { return result.rows.map((row) => this.mapRowToPool(row)); } catch (error) { - if (this.isMissingRelationError(error)) { + if (this.shouldUseReadFallback(error)) { return []; } throw error; diff --git a/services/token-aggregation/src/database/repositories/token-repo.ts b/services/token-aggregation/src/database/repositories/token-repo.ts index be4b039..71f5c02 100644 --- a/services/token-aggregation/src/database/repositories/token-repo.ts +++ b/services/token-aggregation/src/database/repositories/token-repo.ts @@ -46,7 +46,16 @@ export class TokenRepository { return code === '42P01' || (message.includes('relation "') && message.includes('" does not exist')); } + private shouldUseReadFallback(error: unknown): boolean { + if (this.isMissingRelationError(error)) return true; + if (String(process.env.TOKEN_AGGREGATION_DB_READ_FALLBACK ?? '1').toLowerCase() === '0') return false; + const message = (error as { message?: string })?.message || ''; + const code = (error as { code?: string })?.code || ''; + return ['ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND'].includes(code) || /timeout|connect/i.test(message); + } + async getToken(chainId: number, address: string): Promise { + if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return null; try { const result = await this.pool.query( `SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified @@ -73,7 +82,7 @@ export class TokenRepository { verified: row.verified, }; } catch (error) { - if (this.isMissingRelationError(error)) { + if (this.shouldUseReadFallback(error)) { return null; } throw error; @@ -81,6 +90,7 @@ export class TokenRepository { } async getTokens(chainId: number, limit: number = 50, offset: number = 0): Promise { + if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return []; try { const result = await this.pool.query( `SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified @@ -104,7 +114,7 @@ export class TokenRepository { verified: row.verified, })); } catch (error) { - if (this.isMissingRelationError(error)) { + if (this.shouldUseReadFallback(error)) { return []; } throw error; diff --git a/services/token-aggregation/src/services/pmm-onchain-quote.ts b/services/token-aggregation/src/services/pmm-onchain-quote.ts index ff20b15..a1c2b5f 100644 --- a/services/token-aggregation/src/services/pmm-onchain-quote.ts +++ b/services/token-aggregation/src/services/pmm-onchain-quote.ts @@ -3,6 +3,7 @@ import { Contract, JsonRpcProvider } from 'ethers'; const POOL_ABI = [ 'function _BASE_TOKEN_() view returns (address)', 'function _QUOTE_TOKEN_() view returns (address)', + 'function getVaultReserve() view returns (uint256,uint256)', 'function querySellBase(address,uint256) view returns (uint256,uint256)', 'function querySellQuote(address,uint256) view returns (uint256,uint256)', ]; @@ -40,6 +41,25 @@ export async function pmmQuoteAmountOutFromChain(params: { } } +/** Best-effort reserve read for DODO-style PMM/DVM pools. */ +export async function pmmVaultReserveFromChain(params: { + rpcUrl: string; + poolAddress: string; +}): Promise<{ baseReserveRaw: bigint; quoteReserveRaw: bigint } | null> { + const { rpcUrl, poolAddress } = params; + try { + const provider = new JsonRpcProvider(rpcUrl); + const pool = new Contract(poolAddress, POOL_ABI, provider); + const [baseReserve, quoteReserve] = await pool.getVaultReserve(); + return { + baseReserveRaw: BigInt(baseReserve.toString()), + quoteReserveRaw: BigInt(quoteReserve.toString()), + }; + } catch { + return null; + } +} + /** RPC for PMM eth_call quotes on Chain 138 (optional; unset = skip on-chain override). */ export function resolvePmmQuoteRpcUrl(): string { return ( diff --git a/test/flash/DBISEngineXIndexedLiquidityVault.t.sol b/test/flash/DBISEngineXIndexedLiquidityVault.t.sol new file mode 100644 index 0000000..30d4db7 --- /dev/null +++ b/test/flash/DBISEngineXIndexedLiquidityVault.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {DBISEngineXIndexedLiquidityVault} from "../../contracts/flash/DBISEngineXIndexedLiquidityVault.sol"; +import {MockMintableToken} from "../dbis/MockMintableToken.sol"; + +contract MockEngineXUniswapV3Pool { + address public immutable token0; + address public immutable token1; + uint24 public immutable fee; + uint160 public sqrtPriceX96; + int24 public tick; + uint128 public liquidity; + + constructor(address token0_, address token1_, uint24 fee_) { + token0 = token0_; + token1 = token1_; + fee = fee_; + } + + function setSlot0(uint160 sqrtPriceX96_, int24 tick_) external { + sqrtPriceX96 = sqrtPriceX96_; + tick = tick_; + } + + function setLiquidity(uint128 liquidity_) external { + liquidity = liquidity_; + } + + function slot0() external view returns (uint160, int24, uint16, uint16, uint16, uint8, bool) { + return (sqrtPriceX96, tick, 0, 0, 0, 0, true); + } +} + +contract DBISEngineXIndexedLiquidityVaultTest is Test { + MockMintableToken internal cwusdc; + MockMintableToken internal usdc; + MockEngineXUniswapV3Pool internal pool; + DBISEngineXIndexedLiquidityVault internal vault; + + address internal constant RECIPIENT = address(0xD00D); + uint160 internal constant ONE_TO_ONE_SQRT_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336; + bytes32 internal constant PROOF_ID = bytes32("indexed-proof"); + bytes32 internal constant SWAP_TX = bytes32(uint256(0xA1)); + bytes32 internal constant LIQUIDITY_TX = bytes32(uint256(0xB1)); + bytes32 internal constant ISO_HASH = bytes32(uint256(0x1001)); + bytes32 internal constant AUDIT_HASH = bytes32(uint256(0x1002)); + bytes32 internal constant PEG_HASH = bytes32(uint256(0x1003)); + + function setUp() public { + cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this)); + usdc = new MockMintableToken("USD Coin", "USDC", 6, address(this)); + pool = new MockEngineXUniswapV3Pool(address(cwusdc), address(usdc), 100); + pool.setSlot0(ONE_TO_ONE_SQRT_PRICE_X96, 0); + pool.setLiquidity(1_000_000); + cwusdc.mint(address(pool), 100_000_000); + usdc.mint(address(pool), 100_000_000); + + vault = new DBISEngineXIndexedLiquidityVault( + address(cwusdc), address(usdc), address(pool), address(this), 100, 1_000, 1_000_000 + ); + } + + function testRecordIndexedProofAnchorsPublicPoolState() public { + DBISEngineXIndexedLiquidityVault.IndexedProof memory proof = _proof(PROOF_ID); + + (uint160 sqrtPriceX96, int24 tick, uint128 liquidity) = vault.recordIndexedProof(proof); + + assertEq(sqrtPriceX96, ONE_TO_ONE_SQRT_PRICE_X96, "sqrt price"); + assertEq(tick, 0, "tick"); + assertEq(liquidity, 1_000_000, "liquidity"); + assertTrue(vault.usedProofIds(PROOF_ID), "proof consumed"); + } + + function testRejectsDuplicateProofId() public { + vault.recordIndexedProof(_proof(PROOF_ID)); + + vm.expectRevert(bytes("proof used")); + vault.recordIndexedProof(_proof(PROOF_ID)); + } + + function testRejectsTickDrift() public { + pool.setSlot0(ONE_TO_ONE_SQRT_PRICE_X96, 101); + + vm.expectRevert(bytes("tick drift too high")); + vault.recordIndexedProof(_proof(PROOF_ID)); + } + + function testRejectsInsufficientLiquidity() public { + pool.setLiquidity(999); + + vm.expectRevert(bytes("insufficient liquidity")); + vault.recordIndexedProof(_proof(PROOF_ID)); + } + + function testRejectsOversizedProofAmount() public { + DBISEngineXIndexedLiquidityVault.IndexedProof memory proof = _proof(PROOF_ID); + proof.exactOutputAmount = 1_000_001; + + vm.expectRevert(bytes("proof amount too high")); + vault.recordIndexedProof(proof); + } + + function testOperatorAllowlist() public { + vault.setOperatorAllowlistEnabled(true); + + vm.expectRevert(bytes("operator not approved")); + vm.prank(address(0xBEEF)); + vault.recordIndexedProof(_proof(PROOF_ID)); + + vault.setOperatorApproved(address(0xBEEF), true); + vm.prank(address(0xBEEF)); + vault.recordIndexedProof(_proof(PROOF_ID)); + assertTrue(vault.usedProofIds(PROOF_ID), "proof consumed"); + } + + function testPauseBlocksProofs() public { + vault.pause(); + + vm.expectRevert(bytes("paused")); + vault.recordIndexedProof(_proof(PROOF_ID)); + } + + function _proof(bytes32 proofId) internal pure returns (DBISEngineXIndexedLiquidityVault.IndexedProof memory) { + return DBISEngineXIndexedLiquidityVault.IndexedProof({ + proofId: proofId, + publicSwapTxHash: SWAP_TX, + liquidityTxHash: LIQUIDITY_TX, + outputRecipient: RECIPIENT, + exactOutputAmount: 100_000, + iso20022DocumentHash: ISO_HASH, + auditEnvelopeHash: AUDIT_HASH, + pegProofHash: PEG_HASH + }); + } +} diff --git a/test/flash/DBISEngineXSingleSidedDodoCwusdcVault.t.sol b/test/flash/DBISEngineXSingleSidedDodoCwusdcVault.t.sol new file mode 100644 index 0000000..9874b7a --- /dev/null +++ b/test/flash/DBISEngineXSingleSidedDodoCwusdcVault.t.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Test} from "forge-std/Test.sol"; +import {DBISEngineXSingleSidedDodoCwusdcVault} from + "../../contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol"; +import {MockMintableToken} from "../dbis/MockMintableToken.sol"; + +contract MockDodoPool { + using SafeERC20 for IERC20; + + address public immutable base; + address public immutable quote; + uint256 public baseReserve; + uint256 public quoteReserve; + + constructor(address base_, address quote_) { + base = base_; + quote = quote_; + } + + function _BASE_TOKEN_() external view returns (address) { + return base; + } + + function _QUOTE_TOKEN_() external view returns (address) { + return quote; + } + + function querySellBase(address, uint256 payBaseAmount) external pure returns (uint256 receiveQuoteAmount, uint256 mtFee) { + return (payBaseAmount, 0); + } + + function querySellQuote(address, uint256 payQuoteAmount) external pure returns (uint256 receiveBaseAmount, uint256 mtFee) { + return (payQuoteAmount, 0); + } + + function getVaultReserve() external view returns (uint256, uint256) { + return (baseReserve, quoteReserve); + } + + function buyShares(address) external returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare) { + uint256 baseBalance = IERC20(base).balanceOf(address(this)); + uint256 quoteBalance = IERC20(quote).balanceOf(address(this)); + baseShare = baseBalance - baseReserve; + quoteShare = quoteBalance - quoteReserve; + baseReserve = baseBalance; + quoteReserve = quoteBalance; + lpShare = baseShare < quoteShare ? baseShare : quoteShare; + } +} + +contract MockDodoIntegration { + using SafeERC20 for IERC20; + + function addLiquidity(address pool, uint256 baseAmount, uint256 quoteAmount) + external + returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare) + { + require(baseAmount > 0 && quoteAmount > 0, "zero amount"); + address base = MockDodoPool(pool)._BASE_TOKEN_(); + address quote = MockDodoPool(pool)._QUOTE_TOKEN_(); + IERC20(base).safeTransferFrom(msg.sender, pool, baseAmount); + IERC20(quote).safeTransferFrom(msg.sender, pool, quoteAmount); + return MockDodoPool(pool).buyShares(msg.sender); + } +} + +contract DBISEngineXSingleSidedDodoCwusdcVaultTest is Test { + MockMintableToken internal cwusdc; + MockMintableToken internal weth; + MockDodoIntegration internal integration; + MockDodoPool internal pool; + DBISEngineXSingleSidedDodoCwusdcVault internal vault; + + address internal constant FUNDER = address(0xF00D); + address internal constant OWNER = address(0xA11CE); + + function setUp() public { + cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this)); + weth = new MockMintableToken("Wrapped Ether", "WETH", 18, address(this)); + integration = new MockDodoIntegration(); + pool = new MockDodoPool(address(cwusdc), address(weth)); + vault = new DBISEngineXSingleSidedDodoCwusdcVault(address(cwusdc), address(weth), address(integration), OWNER); + + cwusdc.mint(FUNDER, 100_000_000); + weth.mint(FUNDER, 1 ether); + vm.startPrank(FUNDER); + cwusdc.approve(address(vault), type(uint256).max); + weth.approve(address(vault), type(uint256).max); + vm.stopPrank(); + } + + function testAcceptsSingleSidedCwusdcAsInventoryButNotExecutable() public { + vm.prank(FUNDER); + vault.depositCwusdc(10_000_000); + + ( + uint256 cwusdcBalance, + uint256 quoteBalance, + uint256 cwusdcInventory, + uint256 quoteInventory, + bool solvent, + bool executable + ) = vault.solvencyState(); + + assertEq(cwusdcBalance, 10_000_000, "cw balance"); + assertEq(quoteBalance, 0, "quote balance"); + assertEq(cwusdcInventory, 10_000_000, "cw inventory"); + assertEq(quoteInventory, 0, "quote inventory"); + assertTrue(solvent, "single-sided inventory is solvent"); + assertFalse(executable, "single-sided inventory is not executable DODO liquidity"); + } + + function testPromoteRequiresTwoSidedInventory() public { + vm.prank(OWNER); + vault.setDodoPool(address(pool)); + vm.prank(OWNER); + vault.setCanary(1_000, 1_000, 1_000, 1_000); + + vm.prank(FUNDER); + vault.depositCwusdc(10_000_000); + + vm.expectRevert(bytes("two-sided required")); + vm.prank(OWNER); + vault.promoteToDodo(1_000_000, 0, 0, 0, 0); + + vm.expectRevert(bytes("insufficient quote inventory")); + vm.prank(OWNER); + vault.promoteToDodo(1_000_000, 1, 0, 0, 0); + } + + function testPromotesTwoSidedInventoryAndPassesCanary() public { + vm.prank(OWNER); + vault.setDodoPool(address(pool)); + vm.prank(OWNER); + vault.setCanary(1_000, 1_000, 1_000, 1_000); + + vm.startPrank(FUNDER); + vault.depositCwusdc(10_000_000); + vault.depositQuote(1 ether); + vm.stopPrank(); + + vm.prank(OWNER); + (uint256 baseShare, uint256 quoteShare, uint256 lpShare) = + vault.promoteToDodo(2_000_000, 2_000_000, 2_000_000, 2_000_000, 2_000_000); + + assertEq(baseShare, 2_000_000, "base share"); + assertEq(quoteShare, 2_000_000, "quote share"); + assertEq(lpShare, 2_000_000, "lp share"); + assertEq(vault.accountedCwusdcInventory(), 8_000_000, "remaining cw inventory"); + assertEq(vault.accountedQuoteInventory(), 1 ether - 2_000_000, "remaining quote inventory"); + assertEq(cwusdc.balanceOf(address(pool)), 2_000_000, "pool cw balance"); + assertEq(weth.balanceOf(address(pool)), 2_000_000, "pool quote balance"); + assertTrue(vault.canaryPasses(), "canary passes"); + } + + function testCannotRescueAccountedInventory() public { + vm.prank(FUNDER); + vault.depositCwusdc(10_000_000); + + vm.expectRevert(bytes("cwusdc insolvent")); + vm.prank(OWNER); + vault.rescueUnaccountedToken(address(cwusdc), OWNER, 1); + } + + function testOwnerCanWithdrawAccountedInventory() public { + vm.prank(FUNDER); + vault.depositCwusdc(10_000_000); + + vm.prank(OWNER); + vault.withdrawCwusdcInventory(OWNER, 4_000_000); + + assertEq(vault.accountedCwusdcInventory(), 6_000_000, "inventory decremented"); + assertEq(cwusdc.balanceOf(OWNER), 4_000_000, "owner received"); + } +} diff --git a/test/flash/DBISEngineXVirtualBatchVault.t.sol b/test/flash/DBISEngineXVirtualBatchVault.t.sol index 9079a3d..60bbf5d 100644 --- a/test/flash/DBISEngineXVirtualBatchVault.t.sol +++ b/test/flash/DBISEngineXVirtualBatchVault.t.sol @@ -2,9 +2,32 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; +import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {DBISEngineXFlashProofBorrower} from "../../contracts/flash/DBISEngineXFlashProofBorrower.sol"; import {DBISEngineXVirtualBatchVault} from "../../contracts/flash/DBISEngineXVirtualBatchVault.sol"; import {MockMintableToken} from "../dbis/MockMintableToken.sol"; +contract EngineXFlashBorrower is IERC3156FlashBorrower { + bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + bool public repay = true; + + function setRepay(bool repay_) external { + repay = repay_; + } + + function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata) + external + override + returns (bytes32) + { + if (repay) { + IERC20(token).transfer(msg.sender, amount + fee); + } + return _RETURN_VALUE; + } +} + contract DBISEngineXVirtualBatchVaultTest is Test { MockMintableToken internal cwusdc; MockMintableToken internal usdc; @@ -138,15 +161,7 @@ contract DBISEngineXVirtualBatchVaultTest is Test { ); vm.prank(USER); vault.runVirtualProofExactOutTo( - proofId, - LENDER_USDC, - 3, - OUTPUT_RECIPIENT, - exactOutput, - ROUNDING_RECEIVER, - ISO_HASH, - AUDIT_HASH, - PEG_HASH + proofId, LENDER_USDC, 3, OUTPUT_RECIPIENT, exactOutput, ROUNDING_RECEIVER, ISO_HASH, AUDIT_HASH, PEG_HASH ); assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE, "pool cWUSDC reserve should not drift"); @@ -243,4 +258,154 @@ contract DBISEngineXVirtualBatchVaultTest is Test { PEG_HASH ); } + + function testWithdrawPoolLiquidityUpdatesAccountingAndPreservesMaintainedPool() public { + uint256 withdrawAmount = 10_000_000; + uint256 ownerCwusdcBefore = cwusdc.balanceOf(address(this)); + uint256 ownerUsdcBefore = usdc.balanceOf(address(this)); + + vault.withdrawPoolLiquidity(address(this), withdrawAmount, withdrawAmount); + + assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE - withdrawAmount, "pool cWUSDC accounting"); + assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE - withdrawAmount, "pool USDC accounting"); + assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender accounting unchanged"); + assertEq(cwusdc.balanceOf(address(this)), ownerCwusdcBefore + withdrawAmount, "owner cWUSDC received"); + assertEq(usdc.balanceOf(address(this)), ownerUsdcBefore + withdrawAmount, "owner USDC received"); + } + + function testWithdrawPoolLiquidityRejectsBreakingMaintainedPool() public { + vm.expectRevert(bytes("would break maintained pool")); + vault.withdrawPoolLiquidity(address(this), 1, 0); + } + + function testWithdrawLenderUsdcUpdatesAccounting() public { + uint256 withdrawAmount = 1_000_000; + uint256 ownerUsdcBefore = usdc.balanceOf(address(this)); + + vault.withdrawLenderUsdc(address(this), withdrawAmount); + + assertEq(vault.lenderUsdcAvailable(), LENDER_USDC - withdrawAmount, "lender accounting"); + assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool accounting unchanged"); + assertEq(usdc.balanceOf(address(this)), ownerUsdcBefore + withdrawAmount, "owner USDC received"); + } + + function testGenericWithdrawCannotTouchAccountedBalances() public { + vm.expectRevert(bytes("accounting undercollateralized")); + vault.withdraw(address(usdc), address(this), 1); + } + + function testGenericWithdrawCanRescueUnaccountedTokens() public { + uint256 dust = 123; + usdc.mint(address(vault), dust); + uint256 ownerUsdcBefore = usdc.balanceOf(address(this)); + + vault.withdraw(address(usdc), address(this), dust); + + assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool accounting unchanged"); + assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender accounting unchanged"); + assertEq(usdc.balanceOf(address(this)), ownerUsdcBefore + dust, "owner receives unaccounted dust"); + } + + function testFlashLoanUsesLenderBucketAndCollectsFee() public { + EngineXFlashBorrower borrower = new EngineXFlashBorrower(); + uint256 amount = 1_000_000; + uint256 fee = vault.flashFee(address(usdc), amount); + usdc.mint(address(borrower), fee); + + vm.prank(USER); + vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), amount, ""); + + assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket"); + assertEq(vault.totalFlashFeesCollectedUsdc(), fee, "fee accounting"); + assertEq(usdc.balanceOf(address(vault)), LIVE_POOL_RESERVE + LENDER_USDC + fee, "USDC backing"); + } + + function testFlashLoanCanPullRepaymentByAllowance() public { + EngineXFlashBorrower borrower = new EngineXFlashBorrower(); + uint256 amount = 1_000_000; + uint256 fee = vault.flashFee(address(usdc), amount); + usdc.mint(address(borrower), fee); + + borrower.setRepay(false); + vm.prank(address(borrower)); + usdc.approve(address(vault), type(uint256).max); + + vm.prank(USER); + vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), amount, ""); + + assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket"); + assertEq(vault.totalFlashFeesCollectedUsdc(), fee, "fee accounting"); + } + + function testFlashLoanRejectsBorrowingPoolUsdc() public { + EngineXFlashBorrower borrower = new EngineXFlashBorrower(); + + vm.expectRevert(bytes("insufficient lender usdc")); + vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), LENDER_USDC + 1, ""); + } + + function testFlashLoanRejectsUnsupportedToken() public { + EngineXFlashBorrower borrower = new EngineXFlashBorrower(); + + vm.expectRevert(bytes("unsupported flash token")); + vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(cwusdc), 1, ""); + } + + function testFlashLoanCanBeCapped() public { + EngineXFlashBorrower borrower = new EngineXFlashBorrower(); + vault.setMaxFlashLoanAmount(999_999); + + vm.expectRevert(bytes("flash amount too high")); + vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), 1_000_000, ""); + } + + function testFlashLoanAllowlistRejectsUnapprovedBorrower() public { + EngineXFlashBorrower borrower = new EngineXFlashBorrower(); + vault.setFlashBorrowerAllowlistEnabled(true); + + vm.expectRevert(bytes("flash borrower not approved")); + vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), 1, ""); + } + + function testFlashLoanAllowlistAllowsApprovedBorrower() public { + EngineXFlashBorrower borrower = new EngineXFlashBorrower(); + uint256 amount = 1_000_000; + uint256 fee = vault.flashFee(address(usdc), amount); + usdc.mint(address(borrower), fee); + + vault.setFlashBorrowerAllowlistEnabled(true); + vault.setFlashBorrowerApproved(address(borrower), true); + + vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), amount, ""); + + assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket"); + } + + function testPauseBlocksProofsAndFlashLoans() public { + EngineXFlashBorrower borrower = new EngineXFlashBorrower(); + vault.pause(); + + vm.expectRevert(bytes("paused")); + vm.prank(USER); + vault.runVirtualProof(bytes32("proof-paused"), LENDER_USDC, 1); + + vm.expectRevert(bytes("paused")); + vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(usdc), 1, ""); + + assertEq(vault.maxFlashLoan(address(usdc)), 0, "paused max flash"); + } + + function testEngineXFlashProofBorrowerRunsProofFlash() public { + DBISEngineXFlashProofBorrower borrower = + new DBISEngineXFlashProofBorrower(address(vault), address(usdc), address(this)); + uint256 amount = 1_000_000; + uint256 fee = vault.flashFee(address(usdc), amount); + bytes32 proofId = bytes32("flash-proof"); + usdc.mint(address(borrower), fee); + + borrower.runFlashProof(amount, proofId, ISO_HASH, AUDIT_HASH, PEG_HASH); + + assertTrue(borrower.usedProofIds(proofId), "proof consumed"); + assertEq(vault.lenderUsdcAvailable(), LENDER_USDC + fee, "fee stays in lender bucket"); + } } diff --git a/test/flash/DBISEngineXXautUsdcBorrowVault.t.sol b/test/flash/DBISEngineXXautUsdcBorrowVault.t.sol new file mode 100644 index 0000000..920073f --- /dev/null +++ b/test/flash/DBISEngineXXautUsdcBorrowVault.t.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {DBISEngineXXautUsdcBorrowVault} from "../../contracts/flash/DBISEngineXXautUsdcBorrowVault.sol"; +import {MockMintableToken} from "../dbis/MockMintableToken.sol"; + +contract DBISEngineXXautUsdcBorrowVaultTest is Test { + MockMintableToken internal xaut; + MockMintableToken internal usdc; + MockMintableToken internal cwusdc; + DBISEngineXXautUsdcBorrowVault internal vault; + + address internal constant BORROWER = address(0xB0B); + address internal constant LENDER = address(0x1EAD); + address internal constant LIQUIDATOR = address(0xA11CE); + bytes32 internal constant PRICE_SOURCE_HASH = bytes32(uint256(0x5052494345)); + bytes32 internal constant SWAP_TX = bytes32(uint256(0x51574150)); + bytes32 internal constant ISO_HASH = bytes32(uint256(0x150)); + bytes32 internal constant AUDIT_HASH = bytes32(uint256(0xA0017)); + bytes32 internal constant PEG_HASH = bytes32(uint256(0x9E6)); + + uint256 internal constant XAUT_PRICE6 = 3_226_640_000; + uint256 internal constant LTV_BPS = 7_500; + uint256 internal constant LIQUIDATION_THRESHOLD_BPS = 8_000; + uint256 internal constant MIN_HEALTH_FACTOR_BPS = 11_000; + uint256 internal constant LIQUIDATION_BONUS_BPS = 500; + uint256 internal constant LENDER_USDC = 5_000_000_000; + + event CwusdcSourcedRepay( + address indexed account, + address indexed payer, + uint256 amount, + bytes32 indexed publicSwapTxHash, + bytes32 iso20022DocumentHash, + bytes32 auditEnvelopeHash, + bytes32 pegProofHash + ); + + function setUp() public { + xaut = new MockMintableToken("Tether Gold", "XAUt", 6, address(this)); + usdc = new MockMintableToken("USD Coin", "USDC", 6, address(this)); + cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this)); + + vault = new DBISEngineXXautUsdcBorrowVault( + address(xaut), + address(usdc), + address(cwusdc), + address(this), + XAUT_PRICE6, + LTV_BPS, + LIQUIDATION_THRESHOLD_BPS, + MIN_HEALTH_FACTOR_BPS, + LIQUIDATION_BONUS_BPS, + 0, + PRICE_SOURCE_HASH + ); + + usdc.mint(LENDER, LENDER_USDC); + vm.startPrank(LENDER); + usdc.approve(address(vault), type(uint256).max); + vault.fundLender(LENDER_USDC); + vm.stopPrank(); + + xaut.mint(BORROWER, 1_000_000); + usdc.mint(BORROWER, 1_000_000_000); + vm.startPrank(BORROWER); + xaut.approve(address(vault), type(uint256).max); + usdc.approve(address(vault), type(uint256).max); + vm.stopPrank(); + + usdc.mint(LIQUIDATOR, 1_000_000_000); + vm.prank(LIQUIDATOR); + usdc.approve(address(vault), type(uint256).max); + } + + function testBorrowRepayAndWithdrawCollateral() public { + vm.startPrank(BORROWER); + vault.supplyCollateral(1_000_000); + assertEq(vault.collateralValueUsd6(BORROWER), XAUT_PRICE6, "1 XAUt value"); + + vault.borrowUsdc(2_000_000_000, BORROWER); + assertEq(usdc.balanceOf(BORROWER), 3_000_000_000, "borrowed USDC"); + assertEq(vault.lenderUsdcAvailable(), 3_000_000_000, "lender bucket lent out"); + assertEq(vault.healthFactorBps(BORROWER), 12_906, "health factor"); + + vault.repayUsdc(2_000_000_000); + vault.withdrawCollateral(1_000_000, BORROWER); + vm.stopPrank(); + + (uint256 collateral, uint256 debt) = vault.positions(BORROWER); + assertEq(collateral, 0, "collateral closed"); + assertEq(debt, 0, "debt closed"); + assertEq(xaut.balanceOf(BORROWER), 1_000_000, "xaut returned"); + assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender restored"); + } + + function testBorrowRejectsDebtAboveEffectiveCollateralLimit() public { + vm.startPrank(BORROWER); + vault.supplyCollateral(1_000_000); + + vm.expectRevert(bytes("exceeds collateral")); + vault.borrowUsdc(2_400_000_000, BORROWER); + vm.stopPrank(); + } + + function testBorrowRejectsGlobalBorrowCap() public { + vault.setRiskParams(LTV_BPS, LIQUIDATION_THRESHOLD_BPS, MIN_HEALTH_FACTOR_BPS, LIQUIDATION_BONUS_BPS, 1_000_000_000); + + vm.startPrank(BORROWER); + vault.supplyCollateral(1_000_000); + + vm.expectRevert(bytes("max borrow exceeded")); + vault.borrowUsdc(1_000_000_001, BORROWER); + vm.stopPrank(); + } + + function testRepayFromCwusdcProofStillSettlesInUsdcAndAnchorsHashes() public { + vm.startPrank(BORROWER); + vault.supplyCollateral(1_000_000); + vault.borrowUsdc(1_000_000_000, BORROWER); + + vm.expectEmit(true, true, true, true, address(vault)); + emit CwusdcSourcedRepay(BORROWER, BORROWER, 250_000_000, SWAP_TX, ISO_HASH, AUDIT_HASH, PEG_HASH); + vault.repayUsdcFromCwusdcProof(250_000_000, SWAP_TX, ISO_HASH, AUDIT_HASH, PEG_HASH); + vm.stopPrank(); + + (, uint256 debt) = vault.positions(BORROWER); + assertEq(debt, 750_000_000, "debt reduced"); + assertEq(vault.totalCwusdcProofRepayUsdc(), 250_000_000, "proof-sourced repay counter"); + } + + function testLiquidationAfterPriceDrop() public { + vm.startPrank(BORROWER); + vault.supplyCollateral(1_000_000); + vault.borrowUsdc(2_000_000_000, BORROWER); + vm.stopPrank(); + + vault.setXautUsdPrice6(2_000_000_000, bytes32(uint256(0x44524f50))); + assertEq(vault.healthFactorBps(BORROWER), 8_000, "unhealthy after price drop"); + + vm.prank(LIQUIDATOR); + uint256 seized = vault.liquidate(BORROWER, 100_000_000); + + assertEq(seized, 52_500, "5 percent bonus on 0.05 XAUt"); + assertEq(xaut.balanceOf(LIQUIDATOR), 52_500, "liquidator receives XAUt"); + (, uint256 debt) = vault.positions(BORROWER); + assertEq(debt, 1_900_000_000, "debt after partial liquidation"); + } + + function testOwnerCanWithdrawOnlyUnborrowedLenderUsdc() public { + vm.prank(BORROWER); + vault.supplyCollateral(1_000_000); + vm.prank(BORROWER); + vault.borrowUsdc(500_000_000, BORROWER); + + vault.withdrawLenderUsdc(address(this), 4_500_000_000); + assertEq(vault.lenderUsdcAvailable(), 0, "available bucket withdrawn"); + + vm.expectRevert(bytes("insufficient lender usdc")); + vault.withdrawLenderUsdc(address(this), 1); + } + + function testPauseBlocksMutableUserFlows() public { + vault.pause(); + + vm.expectRevert(bytes("paused")); + vm.prank(BORROWER); + vault.supplyCollateral(1); + + vault.unpause(); + vm.prank(BORROWER); + vault.supplyCollateral(1); + } +}