diff --git a/config/config-rpc-public.toml b/config/config-rpc-public.toml index 17b5015..e8a2c9d 100644 --- a/config/config-rpc-public.toml +++ b/config/config-rpc-public.toml @@ -15,8 +15,7 @@ rpc-http-enabled=true rpc-http-host="0.0.0.0" rpc-http-port=8545 rpc-http-max-active-connections=256 -# TRACE required for Blockscout internal txs and block rewards (trace_block, trace_replayBlockTransactions) -rpc-http-api=["ETH","NET","WEB3","TRACE"] +rpc-http-api=["ETH","NET","WEB3"] rpc-http-cors-origins=["*"] rpc-ws-enabled=false @@ -48,4 +47,4 @@ discovery-enabled=true # Gas Configuration -max-peers=32 +max-peers=40 diff --git a/config/trustless-bridge.config.json.example b/config/trustless-bridge.config.json.example index 0687cd3..61c32be 100644 --- a/config/trustless-bridge.config.json.example +++ b/config/trustless-bridge.config.json.example @@ -20,7 +20,7 @@ "minLiquidityRatioBps": 11000 }, "dex": { - "uniswapV3Router": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + "uniswapV3Router": "0xE592427A0AEce92De3Edee1F18E0157C05861564", "curve3Pool": "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7", "oneInchRouter": "0x1111111254EEB25477B68fb85Ed929f73A960582", "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", diff --git a/contracts/bridge/trustless/SwapRouter.sol b/contracts/bridge/trustless/SwapRouter.sol index d1f91ca..5a4ccfe 100644 --- a/contracts/bridge/trustless/SwapRouter.sol +++ b/contracts/bridge/trustless/SwapRouter.sol @@ -25,7 +25,7 @@ contract SwapRouter is ReentrancyGuard { } // Contract addresses (Ethereum Mainnet) - address public immutable uniswapV3Router; // 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 + address public immutable uniswapV3Router; // legacy SwapRouter on Ethereum mainnet address public immutable curve3Pool; // 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7 address public immutable oneInchRouter; // 0x1111111254EEB25477B68fb85Ed929f73A960582 (optional) diff --git a/contracts/bridge/trustless/interfaces/ISwapRouter.sol b/contracts/bridge/trustless/interfaces/ISwapRouter.sol index b0444a9..d9d79b6 100644 --- a/contracts/bridge/trustless/interfaces/ISwapRouter.sol +++ b/contracts/bridge/trustless/interfaces/ISwapRouter.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; /** * @title ISwapRouter - Uniswap V3 SwapRouter Interface * @notice Minimal interface for Uniswap V3 SwapRouter - * @dev Based on Uniswap V3 SwapRouter02 + * @dev Matches the legacy Uniswap V3 SwapRouter exactInputSingle / exactInput ABI. */ interface ISwapRouter { struct ExactInputSingleParams { diff --git a/contracts/flash/AaveQuotePushFlashReceiver.sol b/contracts/flash/AaveQuotePushFlashReceiver.sol index 232a3f2..be9d317 100644 --- a/contracts/flash/AaveQuotePushFlashReceiver.sol +++ b/contracts/flash/AaveQuotePushFlashReceiver.sol @@ -5,6 +5,19 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface IAavePoolLike { + /// @notice Standard V3 flash loan (single-asset via length-1 arrays). Use this on Aave V3.2+ pools + /// where `flashLoanSimple` may revert with `NotActivated()`. + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata interestRateModes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) external; + + /// @dev Retained for compatibility with older pool deployments; prefer `flashLoan` for new integrations. function flashLoanSimple( address receiverAddress, address asset, @@ -14,6 +27,17 @@ interface IAavePoolLike { ) external; } +/// @dev Callback for `flashLoan` (multi-asset API). +interface IAaveFlashLoanReceiver { + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external returns (bool); +} + interface IAaveFlashLoanSimpleReceiver { function executeOperation( address asset, @@ -58,10 +82,10 @@ interface IAaveAtomicBridgeCoordinator { /** * @title AaveQuotePushFlashReceiver - * @notice Aave V3 flashLoanSimple receiver for the quote-push workflow: - * flash borrow quote -> buy PMM base -> unwind base externally -> repay lender, retaining any surplus. + * @notice Aave V3 flash-loan receiver for the quote-push workflow: + * flash borrow quote (`flashLoan` single-asset) -> buy PMM base -> unwind base externally -> repay lender, retaining any surplus. */ -contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver { +contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver, IAaveFlashLoanReceiver { using SafeERC20 for IERC20; address public immutable pool; @@ -119,7 +143,29 @@ contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver { } function flashQuotePush(address asset, uint256 amount, QuotePushParams calldata params) external { - IAavePoolLike(pool).flashLoanSimple(address(this), asset, amount, abi.encode(address(this), params), 0); + address[] memory assets = new address[](1); + uint256[] memory amts = new uint256[](1); + uint256[] memory modes = new uint256[](1); + assets[0] = asset; + amts[0] = amount; + modes[0] = 0; + // `onBehalfOf` must not be zero on some Aave V3.2+ deployments (mapping lookups revert NotActivated). + IAavePoolLike(pool).flashLoan( + address(this), assets, amts, modes, address(this), abi.encode(address(this), params), 0 + ); + } + + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external returns (bool) { + if (msg.sender != pool) revert UntrustedPool(); + if (assets.length != 1 || amounts.length != 1 || premiums.length != 1) revert BadParams(); + _executeQuotePush(assets[0], amounts[0], premiums[0], initiator, params); + return true; } function executeOperation( @@ -130,6 +176,17 @@ contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver { bytes calldata params ) external returns (bool) { if (msg.sender != pool) revert UntrustedPool(); + _executeQuotePush(asset, amount, premium, initiator, params); + return true; + } + + function _executeQuotePush( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) internal { (address expectedInitiator, QuotePushParams memory p) = abi.decode(params, (address, QuotePushParams)); if (initiator != expectedInitiator) revert UntrustedInitiator(); if ( @@ -148,7 +205,6 @@ contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver { uint256 unwindOut = _unwindBaseIntoQuote(p.baseToken, asset, p.externalUnwinder, p.minOutUnwind, p.unwindData); uint256 surplus = _approveRepayment(asset, amount + premium); emit QuotePushExecuted(asset, p.baseToken, amount, premium, baseOut, unwindOut, surplus); - return true; } function _swapQuoteForBase(address asset, uint256 amount, address integration, address pmmPool, uint256 minOutPmm) diff --git a/contracts/flash/TwoHopDodoIntegrationUnwinder.sol b/contracts/flash/TwoHopDodoIntegrationUnwinder.sol new file mode 100644 index 0000000..ad379f7 --- /dev/null +++ b/contracts/flash/TwoHopDodoIntegrationUnwinder.sol @@ -0,0 +1,54 @@ +// 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"; + +interface IDODOIntegrationSwapExactIn { + function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut) + external + returns (uint256 amountOut); +} + +/** + * @title TwoHopDodoIntegrationUnwinder + * @notice Unwind `tokenIn -> midToken` on `poolA`, then `midToken -> tokenOut` on `poolB`, both via the same DODO PMM integration. + * @dev `data` = abi.encode(address poolA, address poolB, address midToken, uint256 minMidOut). + * Use for Mainnet when there is no Uniswap V3 route for cWUSDC/USDC but there *is* depth on + * cWUSDC/cWUSDT and a second PMM pool cWUSDT/USDC (sizes must fit the second pool). + */ +contract TwoHopDodoIntegrationUnwinder { + using SafeERC20 for IERC20; + + address public immutable integration; + + error BadParams(); + + constructor(address integration_) { + if (integration_ == address(0)) revert BadParams(); + integration = integration_; + } + + function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data) + external + returns (uint256 amountOut) + { + if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams(); + + (address poolA, address poolB, address midToken, uint256 minMidOut) = + abi.decode(data, (address, address, address, uint256)); + if (poolA == address(0) || poolB == address(0) || midToken == address(0)) revert BadParams(); + if (midToken == tokenIn || midToken == tokenOut) revert BadParams(); + + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + IERC20(tokenIn).forceApprove(integration, amountIn); + + uint256 midOut = IDODOIntegrationSwapExactIn(integration).swapExactIn(poolA, tokenIn, amountIn, minMidOut); + if (midOut == 0) revert BadParams(); + + IERC20(midToken).forceApprove(integration, midOut); + amountOut = IDODOIntegrationSwapExactIn(integration).swapExactIn(poolB, midToken, midOut, minAmountOut); + + IERC20(tokenOut).safeTransfer(msg.sender, amountOut); + } +} diff --git a/docs/bridge/trustless/DEX_INTEGRATION.md b/docs/bridge/trustless/DEX_INTEGRATION.md index 4b6075b..24f7671 100644 --- a/docs/bridge/trustless/DEX_INTEGRATION.md +++ b/docs/bridge/trustless/DEX_INTEGRATION.md @@ -9,9 +9,10 @@ This document describes DEX integration for swapping ETH/WETH to stablecoins in ### Supported DEXs 1. **Uniswap V3** (Primary) - - Router: `0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45` + - Router: `0xE592427A0AEce92De3Edee1F18E0157C05861564` - Fee tiers: 0.05%, 0.3%, 1% - Direct WETH → Stablecoin swaps + - Note: the trustless router contracts in this repo call the legacy `exactInputSingle` ABI directly 2. **Curve** (Secondary) - 3Pool: `0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7` @@ -110,4 +111,3 @@ Create `test/bridge/trustless/DEXIntegration.t.sol`: - SwapRouter: `contracts/bridge/trustless/SwapRouter.sol` - Interfaces: `contracts/bridge/trustless/interfaces/` - Test Suite: `test/bridge/trustless/DEXIntegration.t.sol` - diff --git a/docs/bridge/trustless/ENV_VARIABLES_REFERENCE.md b/docs/bridge/trustless/ENV_VARIABLES_REFERENCE.md index 274782f..9dc9ae0 100644 --- a/docs/bridge/trustless/ENV_VARIABLES_REFERENCE.md +++ b/docs/bridge/trustless/ENV_VARIABLES_REFERENCE.md @@ -97,13 +97,15 @@ DAI=0x6B175474E89094C44Da98b954EedeAC495271d0F These are standard addresses and typically don't need to be changed: ```bash -UNISWAP_V3_ROUTER=0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 +UNISWAP_V3_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564 CURVE_3POOL=0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7 DODOEX_ROUTER=0xa356867fDCEa8e71AEaF87805808803806231FdC BALANCER_VAULT=0xBA12222222228d8Ba445958a75a0704d566BF2C8 ONEINCH_ROUTER=0x1111111254EEB25477B68fb85Ed929f73A960582 ``` +`UNISWAP_V3_ROUTER` should point at the legacy SwapRouter on Ethereum mainnet because the trustless router contracts in this repo call `exactInputSingle` directly. + ## Balancer Pool IDs (Configure After Deployment) ```bash @@ -317,4 +319,3 @@ MARKET_REPORTING_API_KEY=your_api_key_here - Ensure previous phase completed successfully - Check deployment output for addresses - Verify addresses are saved to `.env` - diff --git a/docs/bridge/trustless/WHITEPAPER.md b/docs/bridge/trustless/WHITEPAPER.md index 291e03e..5a513db 100644 --- a/docs/bridge/trustless/WHITEPAPER.md +++ b/docs/bridge/trustless/WHITEPAPER.md @@ -1343,7 +1343,7 @@ We invite: ### A.2 External Contract Addresses **Ethereum Mainnet**: -- Uniswap V3 SwapRouter: `0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45` +- Uniswap V3 SwapRouter: `0xE592427A0AEce92De3Edee1F18E0157C05861564` - Curve 3pool: `0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7` - 1inch Router: `0x1111111254EEB25477B68fb85Ed929f73A960582` - WETH: `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` diff --git a/forkproof/src/AaveQuotePushFlashReceiver.sol b/forkproof/src/AaveQuotePushFlashReceiver.sol index 232a3f2..806d824 100644 --- a/forkproof/src/AaveQuotePushFlashReceiver.sol +++ b/forkproof/src/AaveQuotePushFlashReceiver.sol @@ -5,6 +5,16 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface IAavePoolLike { + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata interestRateModes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) external; + function flashLoanSimple( address receiverAddress, address asset, @@ -14,6 +24,16 @@ interface IAavePoolLike { ) external; } +interface IAaveFlashLoanReceiver { + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external returns (bool); +} + interface IAaveFlashLoanSimpleReceiver { function executeOperation( address asset, @@ -58,10 +78,9 @@ interface IAaveAtomicBridgeCoordinator { /** * @title AaveQuotePushFlashReceiver - * @notice Aave V3 flashLoanSimple receiver for the quote-push workflow: - * flash borrow quote -> buy PMM base -> unwind base externally -> repay lender, retaining any surplus. + * @notice Aave V3 flash-loan receiver for the quote-push workflow (uses `flashLoan` with singleton arrays). */ -contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver { +contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver, IAaveFlashLoanReceiver { using SafeERC20 for IERC20; address public immutable pool; @@ -119,7 +138,28 @@ contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver { } function flashQuotePush(address asset, uint256 amount, QuotePushParams calldata params) external { - IAavePoolLike(pool).flashLoanSimple(address(this), asset, amount, abi.encode(address(this), params), 0); + address[] memory assets = new address[](1); + uint256[] memory amts = new uint256[](1); + uint256[] memory modes = new uint256[](1); + assets[0] = asset; + amts[0] = amount; + modes[0] = 0; + IAavePoolLike(pool).flashLoan( + address(this), assets, amts, modes, address(this), abi.encode(address(this), params), 0 + ); + } + + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external returns (bool) { + if (msg.sender != pool) revert UntrustedPool(); + if (assets.length != 1 || amounts.length != 1 || premiums.length != 1) revert BadParams(); + _executeQuotePush(assets[0], amounts[0], premiums[0], initiator, params); + return true; } function executeOperation( @@ -130,6 +170,17 @@ contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver { bytes calldata params ) external returns (bool) { if (msg.sender != pool) revert UntrustedPool(); + _executeQuotePush(asset, amount, premium, initiator, params); + return true; + } + + function _executeQuotePush( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) internal { (address expectedInitiator, QuotePushParams memory p) = abi.decode(params, (address, QuotePushParams)); if (initiator != expectedInitiator) revert UntrustedInitiator(); if ( @@ -148,7 +199,6 @@ contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver { uint256 unwindOut = _unwindBaseIntoQuote(p.baseToken, asset, p.externalUnwinder, p.minOutUnwind, p.unwindData); uint256 surplus = _approveRepayment(asset, amount + premium); emit QuotePushExecuted(asset, p.baseToken, amount, premium, baseOut, unwindOut, surplus); - return true; } function _swapQuoteForBase(address asset, uint256 amount, address integration, address pmmPool, uint256 minOutPmm) diff --git a/forkproof/test/AaveQuotePushFlashReceiverMainnetFork.t.sol b/forkproof/test/AaveQuotePushFlashReceiverMainnetFork.t.sol index cfec8dd..588bea7 100644 --- a/forkproof/test/AaveQuotePushFlashReceiverMainnetFork.t.sol +++ b/forkproof/test/AaveQuotePushFlashReceiverMainnetFork.t.sol @@ -95,7 +95,7 @@ contract AaveQuotePushFlashReceiverMainnetForkTest is Test { receiver = new AaveQuotePushFlashReceiver(AAVE_POOL_MAINNET); unwinder = new AaveForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 130, 100); - deal(USDC, address(unwinder), 100_000_000); + deal(USDC, address(unwinder), 50_000_000_000); cusdc138 = new MockMintableToken("Chain 138 USDC", "cUSDC", 6, address(this)); bondToken = new MockMintableToken("Atomic Bond", "aBOND", 6, address(this)); diff --git a/forkproof/test/DODOToUniswapV3MultiHopExternalUnwinderMainnetFork.t.sol b/forkproof/test/DODOToUniswapV3MultiHopExternalUnwinderMainnetFork.t.sol index 2484ed7..8fecdb4 100644 --- a/forkproof/test/DODOToUniswapV3MultiHopExternalUnwinderMainnetFork.t.sol +++ b/forkproof/test/DODOToUniswapV3MultiHopExternalUnwinderMainnetFork.t.sol @@ -7,7 +7,7 @@ import {DODOToUniswapV3MultiHopExternalUnwinder} from "../src/DODOToUniswapV3Mul contract DODOToUniswapV3MultiHopExternalUnwinderMainnetForkTest is Test { address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84; - address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + address constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address constant POOL_CWUSDC_USDT = 0xCC0fd27A40775c9AfcD2BBd3f7c902b0192c247A; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; diff --git a/forkproof/test/UniswapV3ExternalUnwinderMainnetFork.t.sol b/forkproof/test/UniswapV3ExternalUnwinderMainnetFork.t.sol index acede48..d58b451 100644 --- a/forkproof/test/UniswapV3ExternalUnwinderMainnetFork.t.sol +++ b/forkproof/test/UniswapV3ExternalUnwinderMainnetFork.t.sol @@ -11,7 +11,7 @@ interface IWETHFork { } contract UniswapV3ExternalUnwinderMainnetForkTest is Test { - address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + address constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a; diff --git a/foundry.toml b/foundry.toml index c4bd174..39ffbee 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,8 @@ solc = "0.8.20" optimizer = true optimizer_runs = 200 via_ir = true -evm_version = "london" +# Fork tests execute live mainnet bytecode; Cancun matches post-Dencun execution (MCOPY, etc.). +evm_version = "cancun" fs_permissions = [ { access = "read", path = "./config" } ] diff --git a/monitoring/prometheus/scrape-proxmox.yml b/monitoring/prometheus/scrape-proxmox.yml index ff9412a..e33fb6a 100644 --- a/monitoring/prometheus/scrape-proxmox.yml +++ b/monitoring/prometheus/scrape-proxmox.yml @@ -1,14 +1,14 @@ # Proxmox/LXC static scrape config (VMID 10200 on r630-01) # Add to prometheus.yml: - "scrape-proxmox.yml" in rule_files or include -# Targets: Besu RPC (2500, 2101, 2201), Blockscout 5000, Node APIs +# Targets: Besu RPC (2101, 2201, 2400), Blockscout 5000, Node APIs scrape_configs: - job_name: 'besu-rpc-proxmox' static_configs: - targets: - - '192.168.11.250:9545' # besu-rpc-alltra-1 - '192.168.11.211:9545' # besu-rpc-core-1 - '192.168.11.221:9545' # besu-rpc-public-1 + - '192.168.11.240:9545' # thirdweb-rpc-1 metrics_path: /metrics - job_name: 'blockscout-proxmox' diff --git a/monitoring/prometheus/targets-proxmox.yml b/monitoring/prometheus/targets-proxmox.yml index ff9412a..e33fb6a 100644 --- a/monitoring/prometheus/targets-proxmox.yml +++ b/monitoring/prometheus/targets-proxmox.yml @@ -1,14 +1,14 @@ # Proxmox/LXC static scrape config (VMID 10200 on r630-01) # Add to prometheus.yml: - "scrape-proxmox.yml" in rule_files or include -# Targets: Besu RPC (2500, 2101, 2201), Blockscout 5000, Node APIs +# Targets: Besu RPC (2101, 2201, 2400), Blockscout 5000, Node APIs scrape_configs: - job_name: 'besu-rpc-proxmox' static_configs: - targets: - - '192.168.11.250:9545' # besu-rpc-alltra-1 - '192.168.11.211:9545' # besu-rpc-core-1 - '192.168.11.221:9545' # besu-rpc-public-1 + - '192.168.11.240:9545' # thirdweb-rpc-1 metrics_path: /metrics - job_name: 'blockscout-proxmox' diff --git a/script/bridge/trustless/DeployEnhancedSwapRouter.s.sol b/script/bridge/trustless/DeployEnhancedSwapRouter.s.sol index 22a2c53..ea4da10 100644 --- a/script/bridge/trustless/DeployEnhancedSwapRouter.s.sol +++ b/script/bridge/trustless/DeployEnhancedSwapRouter.s.sol @@ -13,7 +13,8 @@ contract DeployEnhancedSwapRouter is Script { address constant PLACEHOLDER = 0x000000000000000000000000000000000000dEaD; // Ethereum Mainnet addresses - address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + // Legacy SwapRouter exposes the exactInputSingle ABI used by EnhancedSwapRouter. + address constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address constant CURVE_3POOL = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; address constant DODOEX_ROUTER = 0xa356867fDCEa8e71AEaF87805808803806231FdC; address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; diff --git a/script/bridge/trustless/DeployTrustlessBridge.s.sol b/script/bridge/trustless/DeployTrustlessBridge.s.sol index 77a92e1..fa1b97a 100644 --- a/script/bridge/trustless/DeployTrustlessBridge.s.sol +++ b/script/bridge/trustless/DeployTrustlessBridge.s.sol @@ -24,7 +24,8 @@ contract DeployTrustlessBridge is Script { uint256 constant DEFAULT_MIN_LIQUIDITY_RATIO_BPS = 11000; // 110% // Ethereum Mainnet addresses - address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + // Legacy SwapRouter exposes the exactInputSingle ABI used by SwapRouter. + address constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address constant CURVE_3POOL = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; address constant ONEINCH_ROUTER = 0x1111111254EEB25477B68fb85Ed929f73A960582; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; diff --git a/script/deploy/DeployDODOIntegrationExternalUnwinder.s.sol b/script/deploy/DeployDODOIntegrationExternalUnwinder.s.sol new file mode 100644 index 0000000..9b2db6c --- /dev/null +++ b/script/deploy/DeployDODOIntegrationExternalUnwinder.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console} from "forge-std/Script.sol"; +import {DODOIntegrationExternalUnwinder} from "../../contracts/flash/DODOIntegrationExternalUnwinder.sol"; + +/** + * @title DeployDODOIntegrationExternalUnwinder + * @notice Deploy DODOIntegrationExternalUnwinder (unwind via another registered PMM pool). + * + * Env: + * PRIVATE_KEY required + * DODO_PMM_INTEGRATION_MAINNET required (same integration as quote-push source pool) + * + * Usage: + * forge script script/deploy/DeployDODOIntegrationExternalUnwinder.s.sol:DeployDODOIntegrationExternalUnwinder \ + * --rpc-url $ETHEREUM_MAINNET_RPC --broadcast -vvvv + */ +contract DeployDODOIntegrationExternalUnwinder is Script { + function run() external { + uint256 pk = vm.envUint("PRIVATE_KEY"); + address integration = vm.envAddress("DODO_PMM_INTEGRATION_MAINNET"); + address deployer = vm.addr(pk); + + console.log("Deployer:", deployer); + console.log("DODO PMM integration:", integration); + + vm.startBroadcast(pk); + DODOIntegrationExternalUnwinder unwinder = new DODOIntegrationExternalUnwinder(integration); + vm.stopBroadcast(); + + console.log("DODOIntegrationExternalUnwinder:", address(unwinder)); + } +} diff --git a/script/deploy/DeployDODOToUniswapV3MultiHopExternalUnwinder.s.sol b/script/deploy/DeployDODOToUniswapV3MultiHopExternalUnwinder.s.sol new file mode 100644 index 0000000..7cc30a0 --- /dev/null +++ b/script/deploy/DeployDODOToUniswapV3MultiHopExternalUnwinder.s.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console} from "forge-std/Script.sol"; +import {DODOToUniswapV3MultiHopExternalUnwinder} from "../../contracts/flash/DODOToUniswapV3MultiHopExternalUnwinder.sol"; + +/** + * @title DeployDODOToUniswapV3MultiHopExternalUnwinder + * @notice Deploy mixed DODO->Uniswap V3 unwinder for quote-push workflows. + * + * Env: + * PRIVATE_KEY required + * DODO_PMM_INTEGRATION_MAINNET required + * UNISWAP_V3_SWAP_ROUTER_MAINNET optional; defaults to legacy SwapRouter `0xE592...` + * + * Usage: + * forge script script/deploy/DeployDODOToUniswapV3MultiHopExternalUnwinder.s.sol:DeployDODOToUniswapV3MultiHopExternalUnwinder \ + * --rpc-url $ETHEREUM_MAINNET_RPC --broadcast -vvvv + */ +contract DeployDODOToUniswapV3MultiHopExternalUnwinder is Script { + address internal constant DEFAULT_UNISWAP_V3_ROUTER_MAINNET = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + + function run() external { + uint256 pk = vm.envUint("PRIVATE_KEY"); + address integration = vm.envAddress("DODO_PMM_INTEGRATION_MAINNET"); + address router = vm.envOr("UNISWAP_V3_SWAP_ROUTER_MAINNET", DEFAULT_UNISWAP_V3_ROUTER_MAINNET); + address deployer = vm.addr(pk); + + console.log("Deployer:", deployer); + console.log("DODO PMM integration:", integration); + console.log("Uniswap V3 router:", router); + + vm.startBroadcast(pk); + DODOToUniswapV3MultiHopExternalUnwinder unwinder = + new DODOToUniswapV3MultiHopExternalUnwinder(integration, router); + vm.stopBroadcast(); + + console.log("DODOToUniswapV3MultiHopExternalUnwinder:", address(unwinder)); + } +} diff --git a/script/deploy/DeployTwoHopDodoIntegrationUnwinder.s.sol b/script/deploy/DeployTwoHopDodoIntegrationUnwinder.s.sol new file mode 100644 index 0000000..f544501 --- /dev/null +++ b/script/deploy/DeployTwoHopDodoIntegrationUnwinder.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console} from "forge-std/Script.sol"; +import {TwoHopDodoIntegrationUnwinder} from "../../contracts/flash/TwoHopDodoIntegrationUnwinder.sol"; + +/** + * @title DeployTwoHopDodoIntegrationUnwinder + * @notice Deploy TwoHopDodoIntegrationUnwinder for Mainnet cWUSDC unwind via cWUSDT. + * + * Env: + * PRIVATE_KEY required + * DODO_PMM_INTEGRATION_MAINNET required + * + * Usage: + * forge script script/deploy/DeployTwoHopDodoIntegrationUnwinder.s.sol:DeployTwoHopDodoIntegrationUnwinder \ + * --rpc-url $ETHEREUM_MAINNET_RPC --broadcast -vvvv + */ +contract DeployTwoHopDodoIntegrationUnwinder is Script { + function run() external { + uint256 pk = vm.envUint("PRIVATE_KEY"); + address integration = vm.envAddress("DODO_PMM_INTEGRATION_MAINNET"); + address deployer = vm.addr(pk); + + console.log("Deployer:", deployer); + console.log("DODO PMM integration:", integration); + + vm.startBroadcast(pk); + TwoHopDodoIntegrationUnwinder unwinder = new TwoHopDodoIntegrationUnwinder(integration); + vm.stopBroadcast(); + + console.log("TwoHopDodoIntegrationUnwinder:", address(unwinder)); + } +} diff --git a/script/deploy/DeployUniswapV3ExternalUnwinder.s.sol b/script/deploy/DeployUniswapV3ExternalUnwinder.s.sol new file mode 100644 index 0000000..da8629a --- /dev/null +++ b/script/deploy/DeployUniswapV3ExternalUnwinder.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console} from "forge-std/Script.sol"; +import {UniswapV3ExternalUnwinder} from "../../contracts/flash/UniswapV3ExternalUnwinder.sol"; + +/** + * @title DeployUniswapV3ExternalUnwinder + * @notice Deploy UniswapV3ExternalUnwinder for quote-push unwind legs (cW* -> USDC on V3). + * + * Env: + * PRIVATE_KEY required + * UNISWAP_V3_SWAP_ROUTER_MAINNET optional; defaults to legacy SwapRouter `0xE592...` on Ethereum mainnet + * + * Usage: + * forge script script/deploy/DeployUniswapV3ExternalUnwinder.s.sol:DeployUniswapV3ExternalUnwinder \ + * --rpc-url $ETHEREUM_MAINNET_RPC --broadcast -vvvv + */ +contract DeployUniswapV3ExternalUnwinder is Script { + /// @dev SwapRouter02 (`0x68b3…`) is multicall-based and does not expose the legacy `exactInputSingle` ABI used by `UniswapV3ExternalUnwinder`. + address internal constant DEFAULT_UNISWAP_V3_ROUTER_MAINNET = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + + function run() external { + uint256 pk = vm.envUint("PRIVATE_KEY"); + address router = vm.envOr("UNISWAP_V3_SWAP_ROUTER_MAINNET", DEFAULT_UNISWAP_V3_ROUTER_MAINNET); + address deployer = vm.addr(pk); + + console.log("Deployer:", deployer); + console.log("Uniswap V3 router:", router); + + vm.startBroadcast(pk); + UniswapV3ExternalUnwinder unwinder = new UniswapV3ExternalUnwinder(router); + vm.stopBroadcast(); + + console.log("UniswapV3ExternalUnwinder:", address(unwinder)); + } +} diff --git a/script/flash/RunMainnetAaveCwusdcUsdcQuotePushOnce.s.sol b/script/flash/RunMainnetAaveCwusdcUsdcQuotePushOnce.s.sol new file mode 100644 index 0000000..8a3882f --- /dev/null +++ b/script/flash/RunMainnetAaveCwusdcUsdcQuotePushOnce.s.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console} from "forge-std/Script.sol"; +import {AaveQuotePushFlashReceiver} from "../../contracts/flash/AaveQuotePushFlashReceiver.sol"; + +interface IDODOPMMPoolQuote { + function querySellQuote(address trader, uint256 payQuoteAmount) external view returns (uint256 receiveBaseAmount, uint256 mtFee); +} + +/** + * @title RunMainnetAaveCwusdcUsdcQuotePushOnce + * @notice Single Aave flash quote-push round: borrow USDC -> PMM quote-in -> external unwind -> repay. + * + * Prereqs: deploy AaveQuotePushFlashReceiver + an IAaveExternalUnwinder (e.g. UniswapV3ExternalUnwinder). + * + * Env (required unless noted): + * PRIVATE_KEY + * AAVE_QUOTE_PUSH_RECEIVER_MAINNET + * DODO_PMM_INTEGRATION_MAINNET + * QUOTE_PUSH_EXTERNAL_UNWINDER_MAINNET + * FLASH_QUOTE_AMOUNT_RAW gross USDC borrowed / pushed (6 decimals raw) + * + * Optional: + * POOL_CWUSDC_USDC_MAINNET default 0x69776fc607e9edA8042e320e7e43f54d06c68f0E + * CWUSDC_MAINNET default canonical cWUSDC + * USDC_MAINNET default official USDC + * MIN_OUT_PMM if unset, derived from querySellQuote(receiver, amount) * MIN_OUT_PMM_NUM / MIN_OUT_PMM_DEN (defaults 985/1000) + * MIN_OUT_PMM_NUM / MIN_OUT_PMM_DEN + * MIN_OUT_UNWIND if unset, amount + ceil(amount * AAVE_FLASH_PREMIUM_BPS / 10000) + MIN_OUT_UNWIND_BUFFER_RAW + * AAVE_FLASH_PREMIUM_BPS default 5 (Aave V3 simple flash typical) + * MIN_OUT_UNWIND_BUFFER_RAW default 5000 raw (~0.005 USDC) headroom + * UNWIND_MODE 0 = Uniswap V3 exactInputSingle (abi.encode uint24 fee); 1 = DODO pool (abi.encode address pool); + * 2 = Uniswap V3 exactInput multi-hop: unwindData = abi.encode(path), path from UNWIND_V3_PATH_HEX + * UNWIND_V3_FEE_U24 required when UNWIND_MODE=0 (e.g. 500, 3000, 10000) + * UNWIND_DODO_POOL required when UNWIND_MODE=1 + * UNWIND_V3_PATH_HEX required when UNWIND_MODE=2 — packed exactInput path (see UniswapV3ExternalUnwinder); build via: + * bash scripts/verify/build-uniswap-v3-exact-input-path-hex.sh ADDR0 FEE0 ... ADDRN + * UNWIND_MODE=4 TwoHopDodoIntegrationUnwinder: set UNWIND_TWO_HOP_POOL_A, UNWIND_TWO_HOP_POOL_B, + * UNWIND_TWO_HOP_MID_TOKEN, optional UNWIND_MIN_MID_OUT_RAW (default 1 wei) + * UNWIND_MODE=5 DODOToUniswapV3MultiHopExternalUnwinder: + * set UNWIND_DODO_POOL, UNWIND_INTERMEDIATE_TOKEN, UNWIND_MIN_INTERMEDIATE_OUT_RAW, + * UNWIND_V3_PATH_HEX (path starts at intermediate token and ends at tokenOut) + */ +contract RunMainnetAaveCwusdcUsdcQuotePushOnce is Script { + address internal constant DEFAULT_POOL = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E; + address internal constant DEFAULT_CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a; + address internal constant DEFAULT_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + function run() external { + uint256 pk = vm.envUint("PRIVATE_KEY"); + address receiver = vm.envAddress("AAVE_QUOTE_PUSH_RECEIVER_MAINNET"); + address pool = vm.envOr("POOL_CWUSDC_USDC_MAINNET", DEFAULT_POOL); + address integration = vm.envAddress("DODO_PMM_INTEGRATION_MAINNET"); + address baseToken = vm.envOr("CWUSDC_MAINNET", DEFAULT_CWUSDC); + address usdc = vm.envOr("USDC_MAINNET", DEFAULT_USDC); + address unwinder = vm.envAddress("QUOTE_PUSH_EXTERNAL_UNWINDER_MAINNET"); + uint256 amount = vm.envUint("FLASH_QUOTE_AMOUNT_RAW"); + + uint256 minPmmNum = vm.envOr("MIN_OUT_PMM_NUM", uint256(985)); + uint256 minPmmDen = vm.envOr("MIN_OUT_PMM_DEN", uint256(1000)); + + uint256 minOutPmm = vm.envOr("MIN_OUT_PMM", uint256(0)); + if (minOutPmm == 0) { + (uint256 baseOut,) = IDODOPMMPoolQuote(pool).querySellQuote(receiver, amount); + minOutPmm = (baseOut * minPmmNum) / minPmmDen; + } + + uint256 premiumBps = vm.envOr("AAVE_FLASH_PREMIUM_BPS", uint256(5)); + uint256 buf = vm.envOr("MIN_OUT_UNWIND_BUFFER_RAW", uint256(5000)); + uint256 minOutUnwind = vm.envOr("MIN_OUT_UNWIND", uint256(0)); + if (minOutUnwind == 0) { + uint256 premium = (amount * premiumBps) / 10000; + minOutUnwind = amount + premium + buf; + } + + uint256 unwindMode = vm.envOr("UNWIND_MODE", uint256(0)); + bytes memory unwindData; + if (unwindMode == 0) { + uint24 fee = uint24(vm.envUint("UNWIND_V3_FEE_U24")); + unwindData = abi.encode(fee); + } else if (unwindMode == 1) { + address dodoPool = vm.envAddress("UNWIND_DODO_POOL"); + unwindData = abi.encode(dodoPool); + } else if (unwindMode == 2) { + string memory pathHex = vm.envString("UNWIND_V3_PATH_HEX"); + bytes memory path = vm.parseBytes(pathHex); + unwindData = abi.encode(path); + } else if (unwindMode == 4) { + address poolA = vm.envAddress("UNWIND_TWO_HOP_POOL_A"); + address poolB = vm.envAddress("UNWIND_TWO_HOP_POOL_B"); + address midToken = vm.envAddress("UNWIND_TWO_HOP_MID_TOKEN"); + uint256 minMidOut = vm.envOr("UNWIND_MIN_MID_OUT_RAW", uint256(1)); + unwindData = abi.encode(poolA, poolB, midToken, minMidOut); + } else if (unwindMode == 5) { + address dodoPool = vm.envAddress("UNWIND_DODO_POOL"); + address intermediateToken = vm.envAddress("UNWIND_INTERMEDIATE_TOKEN"); + uint256 minIntermediateOut = vm.envOr("UNWIND_MIN_INTERMEDIATE_OUT_RAW", uint256(1)); + string memory pathHex = vm.envString("UNWIND_V3_PATH_HEX"); + bytes memory path = vm.parseBytes(pathHex); + unwindData = abi.encode(dodoPool, intermediateToken, minIntermediateOut, path); + } else { + revert("UNWIND_MODE must be 0, 1, 2, 4, or 5"); + } + + AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({ + integration: integration, + pmmPool: pool, + baseToken: baseToken, + externalUnwinder: unwinder, + minOutPmm: minOutPmm, + minOutUnwind: minOutUnwind, + unwindData: unwindData, + atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({ + coordinator: address(0), + sourceChain: 0, + destinationChain: 0, + destinationAsset: address(0), + bridgeAmount: 0, + minDestinationAmount: 0, + destinationRecipient: address(0), + destinationDeadline: 0, + routeId: bytes32(0), + settlementMode: bytes32(0), + submitCommitment: false + }) + }); + + console.log("receiver", receiver); + console.log("pool", pool); + console.log("amount", amount); + console.log("minOutPmm", minOutPmm); + console.log("minOutUnwind", minOutUnwind); + + vm.startBroadcast(pk); + AaveQuotePushFlashReceiver(receiver).flashQuotePush(usdc, amount, p); + vm.stopBroadcast(); + } +} diff --git a/services/relay/.env.avax b/services/relay/.env.avax index 163f285..3328653 100644 --- a/services/relay/.env.avax +++ b/services/relay/.env.avax @@ -1,6 +1,7 @@ # Copy to .env.avax and adjust if you need a different AVAX RPC. # start-relay.sh avax loads this profile before .env.local / .env. -RPC_URL_138=http://192.168.11.211:8545 +# Use the public 138 RPC for relay polling so a Core deploy-RPC restart does not strand this lane. +RPC_URL_138=https://rpc-http-pub.d-bis.org CCIP_ROUTER_CHAIN138=0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817 CCIPWETH9_BRIDGE_CHAIN138=0xcacfd227A040002e49e2e01626363071324f820a SOURCE_CHAIN_SELECTOR=138 diff --git a/services/relay/.env.avax-cw b/services/relay/.env.avax-cw index f1ed3b5..766c0f5 100644 --- a/services/relay/.env.avax-cw +++ b/services/relay/.env.avax-cw @@ -1,5 +1,6 @@ # Forward relay profile for non-prefunded AVAX cW minting. -RPC_URL_138=http://192.168.11.211:8545 +# Use the public 138 RPC for relay polling so a Core deploy-RPC restart does not strand this lane. +RPC_URL_138=https://rpc-http-pub.d-bis.org CCIP_ROUTER_CHAIN138=0x42dab7b888dd382bd5adcf9e038dbf1fd03b4817 SOURCE_BRIDGE_ADDRESS=0x152ed3e9912161b76bdfd368d0c84b7c31c10de7 SOURCE_CHAIN_SELECTOR=138 diff --git a/services/relay/.env.avax-to-138 b/services/relay/.env.avax-to-138 index a5c58c7..ba465ad 100644 --- a/services/relay/.env.avax-to-138 +++ b/services/relay/.env.avax-to-138 @@ -5,6 +5,7 @@ SOURCE_CHAIN_SELECTOR=6433500567565415381 SOURCE_RPC_URL=https://api.avax.network/ext/bc/C/rpc SOURCE_ROUTER_ADDRESS=0x1773125b280d296354f4f4b958a7cfc4e5975b60 SOURCE_BRIDGE_ADDRESS=0x635002c5fb227160cd2eac926d1baa61847f3c75 +SOURCE_LOGS_MAX_BLOCK_RANGE=2048 DEST_CHAIN_NAME=Chain 138 DEST_CHAIN_ID=138 diff --git a/services/relay/.env.mainnet-weth b/services/relay/.env.mainnet-weth index 16b93db..2911af2 100644 --- a/services/relay/.env.mainnet-weth +++ b/services/relay/.env.mainnet-weth @@ -24,7 +24,9 @@ MAX_RETRIES=3 RETRY_DELAY=5000 # Keep the WETH lane observably alive but safe until the Mainnet release bridge is funded again. -RELAY_SHEDDING=1 +RELAY_SHEDDING=0 +RELAY_DELIVERY_ENABLED=1 +RELAY_ENFORCE_BRIDGE_TOKEN_BALANCE=1 # Park the known oversized WETH release message until the Mainnet bridge inventory is restored. # 2026-04-05 purge: drop the historical WETH backlog that reloaded into the paused queue so diff --git a/services/relay/src/MessageQueue.js b/services/relay/src/MessageQueue.js index 44ed7e1..7718e69 100644 --- a/services/relay/src/MessageQueue.js +++ b/services/relay/src/MessageQueue.js @@ -9,6 +9,8 @@ export class MessageQueue { this.processed = new Set(); this.failed = new Set(); this.retryCounts = new Map(); + this.messageStore = new Map(); + this.inFlight = new Map(); } async add(messageData) { @@ -27,6 +29,7 @@ export class MessageQueue { return; } + this.messageStore.set(messageId, messageData); // Add to queue this.queue.push(messageData); this.logger.info(`Added message ${messageId} to queue. Queue size: ${this.queue.length}`); @@ -37,17 +40,25 @@ export class MessageQueue { return null; } - return this.queue.shift(); + const messageData = this.queue.shift(); + if (messageData && messageData.messageId) { + this.inFlight.set(messageData.messageId, messageData); + } + return messageData; } async markProcessed(messageId) { this.processed.add(messageId); this.retryCounts.delete(messageId); + this.inFlight.delete(messageId); + this.messageStore.delete(messageId); this.logger.info(`Message ${messageId} marked as processed`); } async markFailed(messageId) { this.failed.add(messageId); + this.retryCounts.delete(messageId); + this.inFlight.delete(messageId); this.logger.error(`Message ${messageId} marked as failed`); } @@ -58,10 +69,22 @@ export class MessageQueue { async retry(messageId) { const count = this.retryCounts.get(messageId) || 0; this.retryCounts.set(messageId, count + 1); - - // Find message in queue or re-add it - // In a production system, you'd store the original message data - this.logger.info(`Message ${messageId} retry count: ${count + 1}`); + + const existingIndex = this.queue.findIndex(m => m.messageId === messageId); + if (existingIndex >= 0) { + this.logger.info(`Message ${messageId} retry count: ${count + 1}`); + return; + } + + const messageData = this.inFlight.get(messageId) || this.messageStore.get(messageId); + if (!messageData) { + this.logger.warn(`Cannot requeue ${messageId}; original message payload is unavailable`); + return; + } + + this.inFlight.delete(messageId); + this.queue.push(messageData); + this.logger.info(`Message ${messageId} requeued. Retry count: ${count + 1}. Queue size: ${this.queue.length}`); } getStats() { @@ -72,4 +95,3 @@ export class MessageQueue { }; } } - diff --git a/services/relay/src/RelayService.js b/services/relay/src/RelayService.js index e43d1f0..4647b11 100644 --- a/services/relay/src/RelayService.js +++ b/services/relay/src/RelayService.js @@ -4,7 +4,7 @@ */ import { ethers } from 'ethers'; -import { MessageSentABI, RelayRouterABI, RelayBridgeABI } from './abis.js'; +import { MessageSentABI, RelayRouterABI, RelayBridgeABI, ERC20ABI } from './abis.js'; import { MessageQueue } from './MessageQueue.js'; import { isRelayShedding, @@ -161,7 +161,7 @@ export class RelayService { const lastSuccessMs = this.lastRelaySuccess && this.lastRelaySuccess.at ? Date.parse(this.lastRelaySuccess.at) : 0; if ( this.lastError && - this.lastError.scope === 'relay_message' && + (this.lastError.scope === 'relay_message' || this.lastError.scope === 'bridge_inventory') && Number.isFinite(lastErrorMs) && lastErrorMs > 0 && lastErrorMs >= lastSuccessMs @@ -172,6 +172,35 @@ export class RelayService { return 'operational'; } + async ensureTargetBridgeInventory(messageId, targetBridge, tokenAmounts) { + if (process.env.RELAY_ENFORCE_BRIDGE_TOKEN_BALANCE !== '1') { + return { ok: true }; + } + + for (const tokenAmount of tokenAmounts) { + const tokenAddress = ethers.getAddress(tokenAmount.token); + const requiredAmount = typeof tokenAmount.amount === 'bigint' + ? tokenAmount.amount + : BigInt(tokenAmount.amount.toString()); + + const tokenContract = new ethers.Contract(tokenAddress, ERC20ABI, this.destinationProvider); + const availableAmount = await tokenContract.balanceOf(targetBridge); + if (availableAmount < requiredAmount) { + const shortfall = requiredAmount - availableAmount; + return { + ok: false, + token: tokenAddress, + requiredAmount, + availableAmount, + shortfall, + message: `Insufficient bridge inventory for ${messageId}: ${tokenAddress} available=${availableAmount.toString()} required=${requiredAmount.toString()} shortfall=${shortfall.toString()}` + }; + } + } + + return { ok: true }; + } + getHealthSnapshot() { const queueStats = this.messageQueue.getStats(); const status = this.getHealthStatus(); @@ -359,7 +388,13 @@ export class RelayService { return ( msg.includes('maximum RPC range') || msg.includes('exceeds maximum') || - (msg.includes('-32000') && msg.includes('range')) + msg.includes('requested too many blocks') || + msg.includes('maximum is set to') || + (msg.includes('-32000') && ( + msg.includes('range') || + msg.includes('too many blocks') || + msg.includes('maximum is set to') + )) ); } @@ -771,6 +806,27 @@ export class RelayService { amountType: Number(ta.amountType) // Ensure it's a number (uint8) }; }); + + const inventoryCheck = await this.ensureTargetBridgeInventory( + messageId, + targetBridge, + mappedTokenAmounts + ); + if (!inventoryCheck.ok) { + const inventoryError = new Error(inventoryCheck.message); + this.logger.warn(inventoryCheck.message); + this.recordError('bridge_inventory', inventoryError, { + message_id: messageId, + target_bridge: targetBridge, + token: inventoryCheck.token, + available_amount: inventoryCheck.availableAmount.toString(), + required_amount: inventoryCheck.requiredAmount.toString(), + shortfall: inventoryCheck.shortfall.toString() + }); + await this.messageQueue.retry(messageId); + await new Promise(resolve => setTimeout(resolve, this.config.retry.retryDelay)); + return null; + } // Optional normalization for legacy bridges that decode 4-field payloads: // (recipient, amount, sender, nonce). TwoWayTokenBridgeL1/L2 decode 2-field payloads @@ -840,7 +896,7 @@ export class RelayService { target_bridge: targetBridge, tx_hash: receipt.hash }; - if (this.lastError && this.lastError.scope === 'relay_message') { + if (this.lastError && (this.lastError.scope === 'relay_message' || this.lastError.scope === 'bridge_inventory')) { this.lastError = null; } diff --git a/services/relay/src/abis.js b/services/relay/src/abis.js index f172d1f..5fe665f 100644 --- a/services/relay/src/abis.js +++ b/services/relay/src/abis.js @@ -19,3 +19,7 @@ export const RelayBridgeABI = [ "function processed(bytes32) view returns (bool)", "function processedTransfers(bytes32) view returns (bool)" ]; + +export const ERC20ABI = [ + "function balanceOf(address account) view returns (uint256)" +]; diff --git a/services/relay/test.js b/services/relay/test.js index 06ffc6f..625b27b 100644 --- a/services/relay/test.js +++ b/services/relay/test.js @@ -11,6 +11,7 @@ import { existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { ethers } from 'ethers'; +import { MessageQueue } from './src/MessageQueue.js'; import { RelayService } from './src/RelayService.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -92,5 +93,20 @@ const rejected = relay.evaluateMessageScope({ }); assert(rejected.inScope === false, 'WETH message should be rejected by the cW worker scope'); +const queue = new MessageQueue(logger); +const queuedMessage = { + messageId: '0xtest-message', + sender: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7', + receiver: cwReceiver, + data: '0x', + tokenAmounts: [] +}; +await queue.add(queuedMessage); +const inFlightMessage = await queue.getNext(); +assert(inFlightMessage?.messageId === queuedMessage.messageId, 'getNext should return the queued message'); +await queue.retry(queuedMessage.messageId); +const retriedMessage = await queue.getNext(); +assert(retriedMessage?.messageId === queuedMessage.messageId, 'retry should requeue the original message payload'); + console.log('OK: relay service structure valid'); process.exit(0); diff --git a/services/token-aggregation/src/api/middleware/cache.ts b/services/token-aggregation/src/api/middleware/cache.ts index 2d0b76a..151f71b 100644 --- a/services/token-aggregation/src/api/middleware/cache.ts +++ b/services/token-aggregation/src/api/middleware/cache.ts @@ -8,6 +8,17 @@ interface CacheEntry { const cache = new Map(); const DEFAULT_TTL = 60 * 1000; // 1 minute +/** Never cache generic API error envelopes (avoids poisoning cache if status/body ever disagree). */ +function looksLikeGenericErrorPayload(body: unknown): boolean { + if (body == null || typeof body !== 'object' || Array.isArray(body)) return false; + const o = body as Record; + if (typeof o.error !== 'string') return false; + // Success shapes we must not treat as errors + if ('pools' in o || 'tokens' in o || 'data' in o || 'chains' in o || 'tree' in o || 'quote' in o) return false; + if (o.status === 'healthy') return false; + return true; +} + export function cacheMiddleware(ttl: number = DEFAULT_TTL) { return (req: Request, res: Response, next: NextFunction) => { const bypassCache = @@ -29,7 +40,9 @@ export function cacheMiddleware(ttl: number = DEFAULT_TTL) { // Override json method to cache response res.json = function (body: unknown) { - if (!bypassCache) { + // Only cache successful payloads. Otherwise a 500 body gets replayed on cache hit with HTTP 200. + const okStatus = res.statusCode >= 200 && res.statusCode < 300; + if (!bypassCache && okStatus && !looksLikeGenericErrorPayload(body)) { cache.set(key, { data: body, expiresAt: Date.now() + ttl, diff --git a/services/token-aggregation/src/api/routes/tokens.ts b/services/token-aggregation/src/api/routes/tokens.ts index af74ebe..de7dde5 100644 --- a/services/token-aggregation/src/api/routes/tokens.ts +++ b/services/token-aggregation/src/api/routes/tokens.ts @@ -283,38 +283,57 @@ router.get('/tokens/:address/pools', cacheMiddleware(60 * 1000), async (req: Req return res.status(400).json({ error: 'chainId is required' }); } - const pools = await getPoolsByTokenWithFallback(chainId, address); + let pools: LiquidityPool[]; + try { + pools = await getPoolsByTokenWithFallback(chainId, address); + } catch (error) { + logger.error('Error resolving pools list:', error); + pools = []; + } - res.json({ - pools: await Promise.all( - pools.map(async (pool) => { - const { token0, token1 } = await resolvePoolTokenDisplays(tokenRepo, chainId, pool.token0Address, pool.token1Address); - return { - address: pool.poolAddress, - dex: pool.dexType, - token0: { - address: pool.token0Address, - symbol: token0.symbol, - name: token0.name, - source: token0.source, - }, - token1: { - address: pool.token1Address, - symbol: token1.symbol, - name: token1.name, - source: token1.source, - }, - reserves: { - token0: pool.reserve0, - token1: pool.reserve1, - }, - tvl: pool.totalLiquidityUsd, - volume24h: pool.volume24h, - feeTier: pool.feeTier, - }; - }) - ), - }); + const settled = await Promise.allSettled( + pools.map(async (pool) => { + const { token0, token1 } = await resolvePoolTokenDisplays(tokenRepo, chainId, pool.token0Address, pool.token1Address); + return { + address: pool.poolAddress, + dex: String(pool.dexType ?? ''), + token0: { + address: pool.token0Address, + symbol: token0.symbol, + name: token0.name, + source: token0.source, + }, + token1: { + address: pool.token1Address, + symbol: token1.symbol, + name: token1.name, + source: token1.source, + }, + reserves: { + token0: pool.reserve0, + token1: pool.reserve1, + }, + tvl: pool.totalLiquidityUsd, + volume24h: pool.volume24h, + feeTier: pool.feeTier, + }; + }) + ); + + const poolsOut = []; + for (const row of settled) { + if (row.status === 'fulfilled') { + poolsOut.push(row.value); + } else { + logger.warn('Skipping pool row in /tokens/:address/pools:', row.reason); + } + } + + // BigInt (e.g. from live RPC paths) breaks res.json; stringify replacer keeps Mission Control / E2E stable. + const payload = JSON.parse( + JSON.stringify({ pools: poolsOut }, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)) + ) as { pools: typeof poolsOut }; + res.json(payload); } catch (error) { logger.error('Error fetching pools:', error); res.status(500).json({ error: 'Internal server error' }); diff --git a/services/token-aggregation/src/services/route-decision-tree.ts b/services/token-aggregation/src/services/route-decision-tree.ts index 93cab46..020c867 100644 --- a/services/token-aggregation/src/services/route-decision-tree.ts +++ b/services/token-aggregation/src/services/route-decision-tree.ts @@ -101,14 +101,22 @@ export interface RouteDecisionTreeResponse { missingQuoteTokenPools: MissingQuoteTokenPool[]; } +function safeBigInt(raw: string | undefined): bigint { + try { + return BigInt(String(raw || '0').trim() || '0'); + } catch { + return 0n; + } +} + function normalizedTvlUsd(pool: LiquidityPool): number { let tvl = Math.max(0, pool.totalLiquidityUsd || 0); if (pool.chainId === CHAIN_138 && pool.dexType === 'dodo') { const estimated = estimateChain138DodoLiquidityUsd({ token0Address: pool.token0Address, token1Address: pool.token1Address, - reserve0: BigInt(pool.reserve0 || '0'), - reserve1: BigInt(pool.reserve1 || '0'), + reserve0: safeBigInt(pool.reserve0), + reserve1: safeBigInt(pool.reserve1), }).totalLiquidityUsd; if (estimated > 0) { tvl = Math.max(tvl, estimated); diff --git a/test/bridge/trustless/DEXIntegration.t.sol b/test/bridge/trustless/DEXIntegration.t.sol index 983a929..137b61e 100644 --- a/test/bridge/trustless/DEXIntegration.t.sol +++ b/test/bridge/trustless/DEXIntegration.t.sol @@ -14,7 +14,7 @@ contract DEXIntegrationTest is Test { LiquidityPoolETH public liquidityPool; address public constant WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - address public constant UNISWAP_V3_ROUTER = address(0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45); + address public constant UNISWAP_V3_ROUTER = address(0xE592427A0AEce92De3Edee1F18E0157C05861564); address public constant CURVE_3POOL = address(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); address public constant ONEINCH_ROUTER = address(0x1111111254EEB25477B68fb85Ed929f73A960582); address public constant USDT = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); @@ -59,4 +59,3 @@ contract DEXIntegrationTest is Test { // Note: Actual swap tests would require forking mainnet or using mocks // This is a placeholder for integration tests } - diff --git a/test/bridge/trustless/ForkTests.t.sol b/test/bridge/trustless/ForkTests.t.sol index 64d1727..968a55c 100644 --- a/test/bridge/trustless/ForkTests.t.sol +++ b/test/bridge/trustless/ForkTests.t.sol @@ -27,7 +27,7 @@ interface IERC20Token { */ contract ForkTests is Test { // Ethereum Mainnet addresses - address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + address constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address constant CURVE_3POOL = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; address constant ONEINCH_ROUTER = 0x1111111254EEB25477B68fb85Ed929f73A960582; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; diff --git a/test/flash/AaveQuotePushFlashReceiverMainnetFork.t.sol b/test/flash/AaveQuotePushFlashReceiverMainnetFork.t.sol index 121d689..ab132ed 100644 --- a/test/flash/AaveQuotePushFlashReceiverMainnetFork.t.sol +++ b/test/flash/AaveQuotePushFlashReceiverMainnetFork.t.sol @@ -69,7 +69,8 @@ contract AaveQuotePushFlashReceiverMainnetForkTest is Test { receiver = new AaveQuotePushFlashReceiver(AAVE_POOL_MAINNET); unwinder = new AaveForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 112, 100); - deal(USDC, address(unwinder), 100_000_000); // 100 USDC quote inventory for unwind payouts + // PMM + unwind sizing can require materially more than 100 USDC on a live reserve snapshot. + deal(USDC, address(unwinder), 50_000_000_000); // 50k USDC (6 decimals) for mock unwind payouts } function testFork_aaveQuotePush_usesRealAaveAndRealMainnetPmm() public skipIfNoFork { diff --git a/test/flash/DODOIntegrationExternalUnwinderMainnetFork.t.sol b/test/flash/DODOIntegrationExternalUnwinderMainnetFork.t.sol index 0d726c5..7bb5500 100644 --- a/test/flash/DODOIntegrationExternalUnwinderMainnetFork.t.sol +++ b/test/flash/DODOIntegrationExternalUnwinderMainnetFork.t.sol @@ -39,7 +39,8 @@ contract DODOIntegrationExternalUnwinderMainnetForkTest is Test { function testFork_cWUSDCToUSDC_unwindsThroughMainnetDodoIntegration() public skipIfNoFork { uint256 amountIn = 1_000_000; // 1 cWUSDC - deal(CWUSDC, address(unwinder), amountIn); + deal(CWUSDC, address(this), amountIn); + IERC20(CWUSDC).approve(address(unwinder), amountIn); uint256 before = IERC20(USDC).balanceOf(address(this)); uint256 amountOut = unwinder.unwind(CWUSDC, USDC, amountIn, 1, abi.encode(POOL_CWUSDC_USDC)); diff --git a/test/flash/QuotePushFlashWorkflowBorrowerMainnetFork.t.sol b/test/flash/QuotePushFlashWorkflowBorrowerMainnetFork.t.sol index 2e80dce..e53a487 100644 --- a/test/flash/QuotePushFlashWorkflowBorrowerMainnetFork.t.sol +++ b/test/flash/QuotePushFlashWorkflowBorrowerMainnetFork.t.sol @@ -76,8 +76,8 @@ contract QuotePushFlashWorkflowBorrowerMainnetForkTest is Test { unwinder = new ForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 112, 100); // Seed the local lender and unwind venue with enough quote on the fork. - deal(USDC, address(vault), 100_000_000); // 100 USDC - deal(USDC, address(unwinder), 100_000_000); // 100 USDC + deal(USDC, address(vault), 50_000_000_000); + deal(USDC, address(unwinder), 50_000_000_000); } function testFork_quotePush_usesLiveMainnetPmmLegAndRepays() public skipIfNoFork { diff --git a/test/flash/UniswapV3ExternalUnwinderFork.t.sol b/test/flash/UniswapV3ExternalUnwinderFork.t.sol index 9165730..f754e6c 100644 --- a/test/flash/UniswapV3ExternalUnwinderFork.t.sol +++ b/test/flash/UniswapV3ExternalUnwinderFork.t.sol @@ -6,7 +6,8 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {UniswapV3ExternalUnwinder} from "../../contracts/flash/UniswapV3ExternalUnwinder.sol"; contract UniswapV3ExternalUnwinderForkTest is Test { - address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + // SwapRouter (not SwapRouter02): exactInputSingle selector matches `ISwapRouter` in this repo. + address constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a; @@ -38,7 +39,8 @@ contract UniswapV3ExternalUnwinderForkTest is Test { } function testFork_knownRoute_WETHToUSDC_singleHopWorks() public skipIfNoFork { - deal(WETH, address(unwinder), 1 ether); + deal(WETH, address(this), 1 ether); + IERC20(WETH).approve(address(unwinder), type(uint256).max); uint256 before = IERC20(USDC).balanceOf(address(this)); uint256 amountOut = unwinder.unwind(WETH, USDC, 1 ether, 1, abi.encode(uint24(3000))); @@ -49,7 +51,8 @@ contract UniswapV3ExternalUnwinderForkTest is Test { } function testFork_cWUSDCToUSDC_routeUnavailableOnUniswapV3() public skipIfNoFork { - deal(CWUSDC, address(unwinder), 1_000_000); + deal(CWUSDC, address(this), 1_000_000); + IERC20(CWUSDC).approve(address(unwinder), 1_000_000); vm.expectRevert(); unwinder.unwind(CWUSDC, USDC, 1_000_000, 1, abi.encode(uint24(3000)));