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>
@@ -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" },
|
||||
|
||||
87
contracts/flash/DBISEngineXFlashProofBorrower.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
242
contracts/flash/DBISEngineXIndexedLiquidityVault.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
234
contracts/flash/DBISEngineXSingleSidedDodoCwusdcVault.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
344
contracts/flash/DBISEngineXXautUsdcBorrowVault.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# 🌐 Multi-Chain Deployment Guide - Complete Package
|
||||
|
||||
**Version**: 1.0
|
||||
**Last Updated**: 2026-01-24
|
||||
**Last Updated**: 2026-05-09
|
||||
**Status**: ✅ Foundation Complete - Ready for Expansion
|
||||
|
||||
---
|
||||
@@ -80,7 +80,7 @@ This guide covers the complete deployment of the Universal Cross-Chain Asset Hub
|
||||
- [ ] Hedera adapter + oracle service
|
||||
- [ ] Tron adapter + oracle service
|
||||
- [ ] TON adapter + oracle service
|
||||
- [ ] Cosmos adapter (IBC integration)
|
||||
- [ ] Cosmos adapter (IBC integration) — optional streams **A–E**: [COSMOS_ECOSYSTEM_CHAIN138_OPTIONAL_INTEGRATIONS_RUNBOOK.md](../../../docs/11-references/COSMOS_ECOSYSTEM_CHAIN138_OPTIONAL_INTEGRATIONS_RUNBOOK.md), [`config/cosmos-chain138-optional/README.md`](../../../config/cosmos-chain138-optional/README.md)
|
||||
- [ ] Solana adapter (Wormhole integration)
|
||||
|
||||
### **Phase 4: Hyperledger** ⚠️
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
- [ ] **BRG-DEP-002**: Deploy WETH10 bridge to ChainID 138 - 2-3h
|
||||
- [ ] **BRG-DEP-003**: Deploy LINK token to canonical address - 2-3h
|
||||
- [ ] **BRG-DEP-004**: Deploy remaining EVM adapters (Polygon, Arbitrum, Optimism, Base, Avalanche, BSC, Ethereum) - 10-15h
|
||||
- [ ] **BRG-DEP-005**: Deploy remaining non-EVM adapters (Stellar, Algorand, Hedera, Tron, TON, Cosmos, Solana) - 20-30h
|
||||
- [ ] **BRG-DEP-005**: Deploy remaining non-EVM adapters (Stellar, Algorand, Hedera, Tron, TON, Cosmos, Solana) - 20-30h — **Cosmos:** optional streams A–E (not production IBC today): [COSMOS_ECOSYSTEM_CHAIN138_OPTIONAL_INTEGRATIONS_RUNBOOK.md](../../../docs/11-references/COSMOS_ECOSYSTEM_CHAIN138_OPTIONAL_INTEGRATIONS_RUNBOOK.md), [`config/cosmos-chain138-optional/README.md`](../../../config/cosmos-chain138-optional/README.md)
|
||||
- [ ] **BRG-DEP-006**: Deploy Hyperledger components (Cacti, Fabric, Indy) - 15-20h
|
||||
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
2113
services/token-aggregation/config/supply-proof-catalog.json
Normal 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[@]}")
|
||||
|
||||
16
services/token-aggregation/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
23
services/token-aggregation/public/token-logos/gru/cAUDC.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cCADC.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cCHFC.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cEURC.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cEURT.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cGBPC.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cGBPT.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cJPYC.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cUSDC.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cUSDT.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cXAUC.svg
Normal 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 |
23
services/token-aggregation/public/token-logos/gru/cXAUT.svg
Normal 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 |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
137
test/flash/DBISEngineXIndexedLiquidityVault.t.sol
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
179
test/flash/DBISEngineXSingleSidedDodoCwusdcVault.t.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
175
test/flash/DBISEngineXXautUsdcBorrowVault.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||