feat(token-aggregation): reports, PMM quotes, config; Engine X flash vaults

- Expand token-aggregation API (report routes), canonical tokens, pools
- Add flash vault contracts + tests (indexed, DODO cwUSDC, XAUT borrow)
- PMM pools JSON, deploy/export scripts, metamask verified list

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-10 12:56:30 -07:00
parent 27f8e3a500
commit 76143a8fe3
67 changed files with 6972 additions and 136 deletions

View File

@@ -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" },

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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) {

View File

@@ -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");
}
}

View File

@@ -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 **AE**: [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** ⚠️

View File

@@ -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 AE (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
---

View File

@@ -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"
}
],

View File

@@ -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

View File

@@ -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/"

View File

@@ -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

View File

@@ -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
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -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[@]}")

View File

@@ -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"

View File

@@ -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",

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">Avalanche</title>
<desc id="desc">DBIS wallet logo for Avalanche compliant wrapped inventory.</desc>
<circle cx="128" cy="128" r="120" fill="#E84142"/>
<path fill="#fff" d="M128 54 206 190h-52l-26-45-26 45H50L128 54Zm0 54-23 40h46l-23-40Z"/>
</svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">BNB</title>
<desc id="desc">DBIS wallet logo for BNB compliant wrapped inventory.</desc>
<circle cx="128" cy="128" r="120" fill="#F3BA2F"/>
<path fill="#111111" d="m128 56 29 29-29 29-29-29 29-29Zm-50 50 29 29-29 29-29-29 29-29Zm100 0 29 29-29 29-29-29 29-29Zm-50 50 29 29-29 29-29-29 29-29Z"/>
</svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">Celo</title>
<desc id="desc">DBIS wallet logo for Celo compliant wrapped inventory.</desc>
<circle cx="128" cy="128" r="120" fill="#35D07F"/>
<path fill="#111" d="M128 58a70 70 0 1 0 0 140 70 70 0 0 0 0-140Zm0 28a42 42 0 1 1 0 84 42 42 0 0 1 0-84Z"/>
<circle cx="128" cy="128" r="22" fill="#FCFF52"/>
</svg>

After

Width:  |  Height:  |  Size: 439 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">Cronos</title>
<desc id="desc">DBIS wallet logo for Cronos compliant wrapped inventory.</desc>
<circle cx="128" cy="128" r="120" fill="#103F68"/>
<path fill="#fff" d="M128 42 202 85v86l-74 43-74-43V85l74-43Zm0 25-52 30v60l52 30 52-30V97l-52-30Zm-34 45h68l-18 53h-32l-18-53Zm34 12-12 25h24l-12-25Z"/>
</svg>

After

Width:  |  Height:  |  Size: 436 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">Ethereum</title>
<desc id="desc">Black Ethereum diamond mark on a transparent background.</desc>
<path fill="#111111" d="M128 16 54 128l74-34 74 34L128 16Z"/>
<path fill="#111111" d="M128 103 54 137l74 103 74-103-74 34-74-34Z"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">Chainlink</title>
<desc id="desc">Chainlink mark in DBIS wallet-token format.</desc>
<circle cx="128" cy="128" r="120" fill="#2A5ADA"/>
<path fill="#FFFFFF" d="M128 52 62 90v76l66 38 66-38V90l-66-38Zm42 100-42 24-42-24v-48l42-24 42 24v48Z"/>
</svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">POL</title>
<desc id="desc">DBIS wallet logo for POL compliant wrapped inventory.</desc>
<circle cx="128" cy="128" r="120" fill="#8247E5"/>
<path fill="#fff" d="M93 88 128 68l35 20v40l-35 20-35-20V88Zm14 8v24l21 12 21-12V96l-21-12-21 12Zm56 32 35 20v40l-35 20-35-20v-40l35-20Zm14 28-14-8-21 12v24l21 12 21-12v-24l-7-4Zm-84-28 35 20v40l-35 20-35-20v-40l35-20Zm14 32-21-12-21 12v24l21 12 21-12v-24Z"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">WEMIX</title>
<desc id="desc">DBIS wallet logo for WEMIX compliant wrapped inventory.</desc>
<circle cx="128" cy="128" r="120" fill="#111111"/>
<path fill="#fff" d="M46 78h28l18 76 22-76h28l22 76 18-76h28l-31 100h-29l-22-72-22 72H77L46 78Z"/>
</svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">xDAI</title>
<desc id="desc">DBIS wallet logo for xDAI compliant wrapped inventory.</desc>
<circle cx="128" cy="128" r="120" fill="#48A9A6"/>
<path fill="#fff" d="M72 76h36l20 32 20-32h36l-38 52 40 52h-37l-21-33-21 33H70l40-52-38-52Z"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cAUDC token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#10B981"/>
<stop offset="1" stop-color="#006B3F"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">AUD</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cAUDC</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cCADC token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#EF4444"/>
<stop offset="1" stop-color="#B91C1C"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">CAD</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cCADC</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cCHFC token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#F87171"/>
<stop offset="1" stop-color="#B91C1C"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">CHF</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cCHFC</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cEURC token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#2D5BFF"/>
<stop offset="1" stop-color="#143C8C"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">EUR</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cEURC</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cEURT token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#2D5BFF"/>
<stop offset="1" stop-color="#143C8C"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">EUR</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cEURT</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cGBPC token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#7C3AED"/>
<stop offset="1" stop-color="#3D1A78"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">GBP</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cGBPC</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cGBPT token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#7C3AED"/>
<stop offset="1" stop-color="#3D1A78"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">GBP</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cGBPT</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cJPYC token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#EF4444"/>
<stop offset="1" stop-color="#9F1239"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">JPY</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cJPYC</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cUSDC token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#2F80ED"/>
<stop offset="1" stop-color="#1455D9"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">USD</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cUSDC</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cUSDT token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#31C48D"/>
<stop offset="1" stop-color="#008E72"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">USD</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cUSDT</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cXAUC token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#D6A21E"/>
<stop offset="1" stop-color="#8A6100"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">XAU</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cXAUC</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="DBIS GRU cXAUT token logo">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#D6A21E"/>
<stop offset="1" stop-color="#8A6100"/>
</linearGradient>
<linearGradient id="ring" x1="70" y1="50" x2="188" y2="206" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.96"/>
<stop offset="1" stop-color="#DCE7FF" stop-opacity="0.72"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#07111F" flood-opacity="0.28"/>
</filter>
</defs>
<rect width="256" height="256" rx="54" fill="url(#bg)"/>
<path d="M31 91C53 42 96 22 146 28c42 5 72 27 91 63-35-17-71-23-108-18-37 4-70 18-98 18Z" fill="#FFFFFF" opacity="0.13"/>
<circle cx="128" cy="128" r="82" fill="none" stroke="url(#ring)" stroke-width="14" filter="url(#shadow)"/>
<path d="M78 128c0-31 22-56 50-56 20 0 36 9 45 24l-18 11c-6-9-15-14-27-14-17 0-29 15-29 35 0 21 12 35 29 35 12 0 22-5 28-15l18 11c-10 16-26 25-46 25-29 0-50-25-50-56Z" fill="#FFFFFF"/>
<path d="M88 202h80c14 0 26-10 30-24-18 21-43 33-70 33-15 0-29-3-40-9Z" fill="#FFFFFF" opacity="0.22"/>
<text x="128" y="119" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="800" letter-spacing="2" fill="#FFFFFF">DBIS</text>
<text x="128" y="150" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="900" fill="#FFFFFF">XAU</text>
<text x="128" y="181" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="800" letter-spacing="1" fill="#FFFFFF" opacity="0.9">cXAUT</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -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<string, string>;
anchorAddresses?: Record<string, string>;
gasMirrors?: Record<string, string>;
gasQuoteAddresses?: Record<string, string>;
};
type DeploymentStatus = {
chains?: Record<string, DeploymentStatusChain>;
};
const PRICE_USD: Record<string, number> = {
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<T>(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<DiscoveryPayload>(discoveryPath);
const deploymentStatus = readJson<DeploymentStatus>(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();

View File

@@ -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<SupplyProofCatalog> | SupplyProof;
const proofs = Array.isArray((parsed as Partial<SupplyProofCatalog>).proofs)
? ((parsed as SupplyProofCatalog).proofs)
: [parsed as SupplyProof];
return proofs.filter((proof) => proof?.network?.chainId && proof?.token?.address);
}
function loadExistingCatalog(seedPaths: string[]): Map<string, SupplyProof> {
const proofs = new Map<string, SupplyProof>();
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<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
let timeout: NodeJS.Timeout | undefined;
const timer = new Promise<never>((_, 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<bigint>,
contract.decimals().catch(() => spec.decimals) as Promise<number>,
contract.symbol().catch(() => undefined) as Promise<string | undefined>,
contract.name().catch(() => undefined) as Promise<string | undefined>,
]),
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);
});

View File

@@ -78,6 +78,22 @@ function uniquePaths(paths: Array<string | undefined | null>): 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<string, string | undefined>;
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.',
});
});

View File

@@ -139,4 +139,83 @@ describe('Config API runtime networks loader', () => {
await new Promise<void>((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<string, any>;
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<string, any>;
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<string, any>) => entry.options?.symbol === 'cUSDC_V2' || entry.options?.symbol === 'cUSDT_V2'
)
).toBe(false);
});
});

View File

@@ -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<NetworksPayload> {
};
}
async function sendNetworks(_req: Request, res: Response): Promise<void> {
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' });
}

View File

@@ -91,6 +91,24 @@ describe('Report API', () => {
const body = (await res.json()) as Record<string, unknown>;
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<string, any>;
const cwusdc = body.tokens.find((token: Record<string, any>) => 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<string, any>;
const cwusdc = body.tokens.find((token: Record<string, any>) => 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<string, any>;
const cwusdc = body.tokens.find((token: Record<string, any>) => 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<string, any>;
const cwusdt = body.tokens.find((token: Record<string, any>) => 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<string, any>;
const cwusdc = body.tokens?.['1']?.find((token: Record<string, any>) => 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<string, any>;
const proofGated = body.tokens?.['1']?.find((entry: Record<string, any>) => 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<string, any>) => 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<string, any>;
const cusdc = body.tokens?.['138']?.find((entry: Record<string, any>) => 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<string, any>;
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<string, any>;
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<string, any>;
const cwusdc = body.tokens.find((token: Record<string, any>) => 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));

File diff suppressed because it is too large Load Diff

View File

@@ -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<void> {
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<void> {
this.omnlPoller?.stop();
this.indexer?.stopAll();
if (this.server) {
await new Promise<void>((resolve, reject) => {
this.server?.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
this.server = null;
}
logger.info('Server stopped');
}
}

View File

@@ -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({

View File

@@ -70,7 +70,7 @@ const LEGACY_CHAIN_ENV_SUFFIX: Partial<Record<number, string>> = {
};
/** 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<string, Partial<Record<number, string>>> = {
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<string, Partial<Record<number, string>>> = {
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<string, Partial<Record<number, string>>> = {
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<string, string> = {
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<string, string> = {
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');

View File

@@ -81,7 +81,7 @@ export const CHAIN_CONFIGS: Record<number, ChainConfig> = {
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,

View File

@@ -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'),

View File

@@ -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,

View File

@@ -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'],

View File

@@ -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

View File

@@ -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<TokenMarketData | null> {
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<TokenMarketData[]> {
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<TokenMarketData[]> {
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;

View File

@@ -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<LiquidityPool | null> {
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<LiquidityPool[]> {
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<LiquidityPool[]> {
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;

View File

@@ -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<Token | null> {
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<Token[]> {
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;

View File

@@ -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 (

View File

@@ -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
});
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}