From 843cdbf71cc0551231e511d98cdd67a2932ef9d0 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Sat, 18 Apr 2026 12:05:34 -0700 Subject: [PATCH] feat: expand non-evm relay and route planning support --- .../bridge/adapters/non-evm/SolanaAdapter.sol | 209 ++++++ .../bridge/adapters/non-evm/TronAdapter.sol | 181 +++++ contracts/dex/DODOPMMIntegration.sol | 10 +- .../RecoveredWave1PoolFamily2682.sol | 13 + .../recovered/RecoveredWave1PoolFamily668.sol | 13 + contracts/vendor/sushiswap-v2/LICENSE | 674 ++++++++++++++++++ .../vendor/sushiswap-v2/UniswapV2ERC20.sol | 95 +++ .../vendor/sushiswap-v2/UniswapV2Factory.sol | 62 ++ .../vendor/sushiswap-v2/UniswapV2Pair.sol | 214 ++++++ .../vendor/sushiswap-v2/UniswapV2Router02.sol | 447 ++++++++++++ .../vendor/sushiswap-v2/interfaces/IERC20.sol | 19 + .../interfaces/IUniswapV2Callee.sol | 7 + .../interfaces/IUniswapV2ERC20.sol | 25 + .../interfaces/IUniswapV2Factory.sol | 21 + .../interfaces/IUniswapV2Pair.sol | 54 ++ .../interfaces/IUniswapV2Router01.sol | 97 +++ .../interfaces/IUniswapV2Router02.sol | 46 ++ .../vendor/sushiswap-v2/interfaces/IWETH.sol | 9 + .../vendor/sushiswap-v2/libraries/Math.sol | 25 + .../sushiswap-v2/libraries/SafeMath.sol | 19 + .../sushiswap-v2/libraries/TransferHelper.sol | 29 + .../sushiswap-v2/libraries/UQ112x112.sol | 22 + .../libraries/UniswapV2Library.sol | 84 +++ .../vendor/uniswap-v2-core/UniswapV2ERC20.sol | 94 +++ .../uniswap-v2-core/UniswapV2Factory.sol | 49 ++ .../vendor/uniswap-v2-core/UniswapV2Pair.sol | 201 ++++++ .../uniswap-v2-core/interfaces/IERC20.sol | 17 + .../interfaces/IUniswapV2Callee.sol | 5 + .../interfaces/IUniswapV2ERC20.sol | 23 + .../interfaces/IUniswapV2Factory.sol | 17 + .../interfaces/IUniswapV2Pair.sol | 52 ++ .../vendor/uniswap-v2-core/libraries/Math.sol | 23 + .../uniswap-v2-core/libraries/SafeMath.sol | 17 + .../uniswap-v2-core/libraries/UQ112x112.sol | 20 + .../UniswapV2Router02.sol | 446 ++++++++++++ .../interfaces/IERC20.sol | 17 + .../interfaces/IUniswapV2Router01.sol | 95 +++ .../interfaces/IUniswapV2Router02.sol | 44 ++ .../uniswap-v2-periphery/interfaces/IWETH.sol | 7 + .../libraries/SafeMath.sol | 17 + .../libraries/UniswapV2Library.sol | 82 +++ .../wrapped-lp-public/Chain138LPLocker.sol | 73 ++ .../PublicChainMintController.sol | 46 ++ contracts/wrapped-lp-public/README.md | 92 +++ contracts/wrapped-lp-public/WLPNAVOracle.sol | 52 ++ .../wrapped-lp-public/WLPReceiptToken.sol | 41 ++ .../WLPRedemptionGateway.sol | 33 + .../wrapped-lp-public/WrappedLPNAVVault.sol | 41 ++ .../interfaces/IWLPProgramEvents.sol | 14 + deployments/chain138/sushiswap-native.json | 18 + deployments/chain138/uniswap-v2-native.json | 17 + .../integration/ORACLE_AND_KEEPER_CHAIN138.md | 2 + hardhat.config.js | 3 + lib/dodo-contractV2 | 2 +- script/deploy/chains/DeployAllAdapters.s.sol | 6 + .../deploy/chains/DeploySolanaAdapter.s.sol | 42 ++ .../DeployWrappedLPLockerChain138.s.sol | 26 + .../DeployWrappedLPProgram.s.sol | 72 ++ .../DeployWrappedLPPublicChain.s.sol | 65 ++ scripts/chain138/deploy-sushiswap-native.js | 188 +++++ scripts/chain138/deploy-uniswap-v2-native.js | 186 +++++ .../chain138/seed-uni-v2-weth-quote-pairs.js | 274 +++++++ scripts/deployment/c138-cw-bridge-75-split.sh | 291 ++++++++ .../cw-l1-bootstrap-gru-v2-ccip-routes.sh | 96 +++ ...cw-l1-bridge-gru-v2-90pct-lock-and-send.sh | 106 +++ .../cw-l1-configure-destination-8-chains.sh | 118 +++ ...ploy-compliant-fiat-tether-iso-chain138.sh | 105 +++ scripts/deployment/deploy-pmm-all-l2s.sh | 9 +- .../seed-chain138-uni-v2-weth-quote-pairs.sh | 11 + scripts/deployment/verify-wemix-bridges.sh | 119 ++++ scripts/forge/scope.sh | 8 +- scripts/mint-compliant-fiat-tether-iso-138.sh | 70 ++ scripts/verify-tezos-etherlink-support.js | 48 +- services/etherlink-relay/src/config.js | 16 +- services/non-evm-relay/lifecycle.ts | 53 ++ services/solana-relay/.env.example | 11 + services/solana-relay/package.json | 13 + .../solana-relay/src/SolanaRelayService.ts | 116 +++ services/solana-relay/src/relay-worker.mjs | 239 +++++++ .../stellar-relay/src/StellarRelayService.ts | 22 + services/tezos-relay/src/config.js | 16 +- services/token-aggregation/README.md | 19 +- services/token-aggregation/docs/DEPLOYMENT.md | 15 + .../src/adapters/dexscreener-adapter.test.ts | 99 +++ .../src/adapters/dexscreener-adapter.ts | 138 ++-- .../src/api/routes/report.test.ts | 87 +++ .../src/api/routes/report.ts | 45 +- .../src/api/routes/tokens.test.ts | 219 ++++++ .../src/api/routes/tokens.ts | 62 +- .../src/config/canonical-tokens.test.ts | 20 + .../src/config/canonical-tokens.ts | 24 + .../src/config/dex-factories.ts | 153 ++-- .../config/gru-v2-deployment-pools.test.ts | 83 +++ .../src/config/gru-v2-deployment-pools.ts | 197 +++++ .../src/config/provider-capabilities.ts | 140 ++++ .../src/config/routing-policies.ts | 2 +- .../src/database/repositories/admin-repo.ts | 2 +- .../src/database/repositories/pool-repo.ts | 2 +- .../src/indexer/chain-indexer.ts | 12 +- .../src/indexer/pool-indexer.ts | 21 +- .../aggregator-route-matrix-generator.ts | 8 + .../services/best-execution-planner.test.ts | 2 +- .../src/services/best-execution-planner.ts | 16 +- .../services/canonical-price-oracle.test.ts | 99 +++ .../src/services/canonical-price-oracle.ts | 193 +++++ .../services/chain138-dodo-liquidity.test.ts | 34 +- .../src/services/chain138-dodo-liquidity.ts | 78 +- .../src/services/planner-v2-types.ts | 11 +- .../src/services/route-graph-builder.ts | 4 + .../src/services/valuation-precedence.test.ts | 195 +++++ .../src/services/valuation-precedence.ts | 276 +++++++ services/tron-relay/src/TronRelayService.ts | 22 + test/wrapped-lp-public/WrappedLPProgram.t.sol | 125 ++++ 113 files changed, 8542 insertions(+), 222 deletions(-) create mode 100644 contracts/bridge/adapters/non-evm/SolanaAdapter.sol create mode 100644 contracts/bridge/adapters/non-evm/TronAdapter.sol create mode 100644 contracts/dex/recovered/RecoveredWave1PoolFamily2682.sol create mode 100644 contracts/dex/recovered/RecoveredWave1PoolFamily668.sol create mode 100644 contracts/vendor/sushiswap-v2/LICENSE create mode 100644 contracts/vendor/sushiswap-v2/UniswapV2ERC20.sol create mode 100644 contracts/vendor/sushiswap-v2/UniswapV2Factory.sol create mode 100644 contracts/vendor/sushiswap-v2/UniswapV2Pair.sol create mode 100644 contracts/vendor/sushiswap-v2/UniswapV2Router02.sol create mode 100644 contracts/vendor/sushiswap-v2/interfaces/IERC20.sol create mode 100644 contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Callee.sol create mode 100644 contracts/vendor/sushiswap-v2/interfaces/IUniswapV2ERC20.sol create mode 100644 contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Factory.sol create mode 100644 contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Pair.sol create mode 100644 contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router01.sol create mode 100644 contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router02.sol create mode 100644 contracts/vendor/sushiswap-v2/interfaces/IWETH.sol create mode 100644 contracts/vendor/sushiswap-v2/libraries/Math.sol create mode 100644 contracts/vendor/sushiswap-v2/libraries/SafeMath.sol create mode 100644 contracts/vendor/sushiswap-v2/libraries/TransferHelper.sol create mode 100644 contracts/vendor/sushiswap-v2/libraries/UQ112x112.sol create mode 100644 contracts/vendor/sushiswap-v2/libraries/UniswapV2Library.sol create mode 100644 contracts/vendor/uniswap-v2-core/UniswapV2ERC20.sol create mode 100644 contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol create mode 100644 contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol create mode 100644 contracts/vendor/uniswap-v2-core/interfaces/IERC20.sol create mode 100644 contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Callee.sol create mode 100644 contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2ERC20.sol create mode 100644 contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Factory.sol create mode 100644 contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Pair.sol create mode 100644 contracts/vendor/uniswap-v2-core/libraries/Math.sol create mode 100644 contracts/vendor/uniswap-v2-core/libraries/SafeMath.sol create mode 100644 contracts/vendor/uniswap-v2-core/libraries/UQ112x112.sol create mode 100644 contracts/vendor/uniswap-v2-periphery/UniswapV2Router02.sol create mode 100644 contracts/vendor/uniswap-v2-periphery/interfaces/IERC20.sol create mode 100644 contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router01.sol create mode 100644 contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router02.sol create mode 100644 contracts/vendor/uniswap-v2-periphery/interfaces/IWETH.sol create mode 100644 contracts/vendor/uniswap-v2-periphery/libraries/SafeMath.sol create mode 100644 contracts/vendor/uniswap-v2-periphery/libraries/UniswapV2Library.sol create mode 100644 contracts/wrapped-lp-public/Chain138LPLocker.sol create mode 100644 contracts/wrapped-lp-public/PublicChainMintController.sol create mode 100644 contracts/wrapped-lp-public/README.md create mode 100644 contracts/wrapped-lp-public/WLPNAVOracle.sol create mode 100644 contracts/wrapped-lp-public/WLPReceiptToken.sol create mode 100644 contracts/wrapped-lp-public/WLPRedemptionGateway.sol create mode 100644 contracts/wrapped-lp-public/WrappedLPNAVVault.sol create mode 100644 contracts/wrapped-lp-public/interfaces/IWLPProgramEvents.sol create mode 100644 deployments/chain138/sushiswap-native.json create mode 100644 deployments/chain138/uniswap-v2-native.json create mode 100644 script/deploy/chains/DeploySolanaAdapter.s.sol create mode 100644 script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol create mode 100644 script/wrapped-lp-public/DeployWrappedLPProgram.s.sol create mode 100644 script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol create mode 100644 scripts/chain138/deploy-sushiswap-native.js create mode 100644 scripts/chain138/deploy-uniswap-v2-native.js create mode 100644 scripts/chain138/seed-uni-v2-weth-quote-pairs.js create mode 100755 scripts/deployment/c138-cw-bridge-75-split.sh create mode 100755 scripts/deployment/cw-l1-bootstrap-gru-v2-ccip-routes.sh create mode 100755 scripts/deployment/cw-l1-bridge-gru-v2-90pct-lock-and-send.sh create mode 100755 scripts/deployment/cw-l1-configure-destination-8-chains.sh create mode 100755 scripts/deployment/deploy-compliant-fiat-tether-iso-chain138.sh create mode 100755 scripts/deployment/seed-chain138-uni-v2-weth-quote-pairs.sh create mode 100755 scripts/deployment/verify-wemix-bridges.sh create mode 100755 scripts/mint-compliant-fiat-tether-iso-138.sh create mode 100644 services/non-evm-relay/lifecycle.ts create mode 100644 services/solana-relay/.env.example create mode 100644 services/solana-relay/package.json create mode 100644 services/solana-relay/src/SolanaRelayService.ts create mode 100644 services/solana-relay/src/relay-worker.mjs create mode 100644 services/stellar-relay/src/StellarRelayService.ts create mode 100644 services/token-aggregation/src/adapters/dexscreener-adapter.test.ts create mode 100644 services/token-aggregation/src/api/routes/tokens.test.ts create mode 100644 services/token-aggregation/src/config/gru-v2-deployment-pools.test.ts create mode 100644 services/token-aggregation/src/config/gru-v2-deployment-pools.ts create mode 100644 services/token-aggregation/src/services/canonical-price-oracle.test.ts create mode 100644 services/token-aggregation/src/services/canonical-price-oracle.ts create mode 100644 services/token-aggregation/src/services/valuation-precedence.test.ts create mode 100644 services/token-aggregation/src/services/valuation-precedence.ts create mode 100644 services/tron-relay/src/TronRelayService.ts create mode 100644 test/wrapped-lp-public/WrappedLPProgram.t.sol diff --git a/contracts/bridge/adapters/non-evm/SolanaAdapter.sol b/contracts/bridge/adapters/non-evm/SolanaAdapter.sol new file mode 100644 index 0000000..87f1c34 --- /dev/null +++ b/contracts/bridge/adapters/non-evm/SolanaAdapter.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "../../interfaces/IChainAdapter.sol"; + +/** + * @title SolanaAdapter + * @notice Bridge adapter for Solana mainnet-beta using an off-chain relay/oracle path + * @dev Tracks idempotent fulfillment ids so the relay can resume safely after restarts. + */ +contract SolanaAdapter is IChainAdapter, AccessControl, ReentrancyGuard { + using SafeERC20 for IERC20; + + bytes32 public constant BRIDGE_OPERATOR_ROLE = keccak256("BRIDGE_OPERATOR_ROLE"); + bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); + + bool public isActive; + uint256 public minFinalitySlots; + + mapping(bytes32 => BridgeRequest) public bridgeRequests; + mapping(bytes32 => string) public solanaTxSignatures; + mapping(bytes32 => uint256) public confirmedSlots; + mapping(bytes32 => bytes32) public fulfillmentIdsByRequest; + mapping(bytes32 => bool) public usedFulfillmentIds; + mapping(address => uint256) public nonces; + + event SolanaBridgeInitiated( + bytes32 indexed requestId, + address indexed sender, + address indexed token, + uint256 amount, + string destination, + bytes recipient + ); + + event SolanaBridgeConfirmed( + bytes32 indexed requestId, + string txSignature, + uint256 finalizedSlot, + bytes32 indexed fulfillmentId + ); + + event SolanaBridgeMarkedFailed(bytes32 indexed requestId, string reason); + event SolanaBridgeRecovered(bytes32 indexed requestId, BridgeStatus newStatus, string note); + + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(BRIDGE_OPERATOR_ROLE, admin); + _grantRole(ORACLE_ROLE, admin); + isActive = true; + minFinalitySlots = 32; + } + + function getChainType() external pure override returns (string memory) { + return "Solana"; + } + + function getChainIdentifier() external pure override returns (uint256 chainId, string memory identifier) { + return (0, "Solana-Mainnet"); + } + + function validateDestination(bytes calldata destination) external pure override returns (bool) { + uint256 length = destination.length; + return length >= 32 && length <= 44; + } + + function bridge( + address token, + uint256 amount, + bytes calldata destination, + bytes calldata recipient + ) external payable override nonReentrant returns (bytes32 requestId) { + require(isActive, "Adapter inactive"); + require(amount > 0, "Zero amount"); + require(this.validateDestination(destination), "Invalid Solana destination"); + + requestId = keccak256( + abi.encodePacked( + msg.sender, + token, + amount, + destination, + recipient, + nonces[msg.sender]++, + block.timestamp + ) + ); + + if (token == address(0)) { + require(msg.value >= amount, "Insufficient ETH"); + } else { + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + } + + bridgeRequests[requestId] = BridgeRequest({ + sender: msg.sender, + token: token, + amount: amount, + destinationData: destination, + requestId: requestId, + status: BridgeStatus.Locked, + createdAt: block.timestamp, + completedAt: 0 + }); + + emit SolanaBridgeInitiated( + requestId, + msg.sender, + token, + amount, + string(destination), + recipient + ); + + return requestId; + } + + function confirmTransaction( + bytes32 requestId, + string calldata txSignature, + uint256 finalizedSlot, + bytes32 fulfillmentId + ) external onlyRole(ORACLE_ROLE) { + BridgeRequest storage request = bridgeRequests[requestId]; + require(request.status == BridgeStatus.Locked, "Invalid status"); + require(bytes(txSignature).length > 0, "Missing Solana signature"); + require(finalizedSlot >= minFinalitySlots, "Insufficient finality"); + require(fulfillmentId != bytes32(0), "Missing fulfillment id"); + require(!usedFulfillmentIds[fulfillmentId], "Fulfillment already used"); + + usedFulfillmentIds[fulfillmentId] = true; + fulfillmentIdsByRequest[requestId] = fulfillmentId; + solanaTxSignatures[requestId] = txSignature; + confirmedSlots[requestId] = finalizedSlot; + request.status = BridgeStatus.Confirmed; + request.completedAt = block.timestamp; + + emit SolanaBridgeConfirmed(requestId, txSignature, finalizedSlot, fulfillmentId); + } + + function markFailed(bytes32 requestId, string calldata reason) external onlyRole(ORACLE_ROLE) { + BridgeRequest storage request = bridgeRequests[requestId]; + require(request.status == BridgeStatus.Locked, "Invalid status"); + + request.status = BridgeStatus.Failed; + request.completedAt = block.timestamp; + + emit SolanaBridgeMarkedFailed(requestId, reason); + } + + function recoverBridge( + bytes32 requestId, + BridgeStatus newStatus, + string calldata note + ) external onlyRole(BRIDGE_OPERATOR_ROLE) { + BridgeRequest storage request = bridgeRequests[requestId]; + require(request.requestId != bytes32(0), "Unknown request"); + require( + request.status == BridgeStatus.Failed || request.status == BridgeStatus.Confirmed, + "Recovery not allowed" + ); + + request.status = newStatus; + request.completedAt = block.timestamp; + + emit SolanaBridgeRecovered(requestId, newStatus, note); + } + + function getBridgeStatus(bytes32 requestId) external view override returns (BridgeRequest memory) { + return bridgeRequests[requestId]; + } + + function cancelBridge(bytes32 requestId) external override nonReentrant returns (bool) { + BridgeRequest storage request = bridgeRequests[requestId]; + require(request.status == BridgeStatus.Pending || request.status == BridgeStatus.Locked, "Cannot cancel"); + require(msg.sender == request.sender, "Not request sender"); + + if (request.token == address(0)) { + payable(request.sender).transfer(request.amount); + } else { + IERC20(request.token).safeTransfer(request.sender, request.amount); + } + + request.status = BridgeStatus.Cancelled; + request.completedAt = block.timestamp; + return true; + } + + function estimateFee( + address, + uint256, + bytes calldata + ) external pure override returns (uint256 fee) { + return 1000000000000000; + } + + function setIsActive(bool _isActive) external onlyRole(DEFAULT_ADMIN_ROLE) { + isActive = _isActive; + } + + function setMinFinalitySlots(uint256 slots) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(slots > 0, "Zero finality"); + minFinalitySlots = slots; + } +} diff --git a/contracts/bridge/adapters/non-evm/TronAdapter.sol b/contracts/bridge/adapters/non-evm/TronAdapter.sol new file mode 100644 index 0000000..eefb2d9 --- /dev/null +++ b/contracts/bridge/adapters/non-evm/TronAdapter.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "../../interfaces/IChainAdapter.sol"; + +/** + * @title TronAdapter + * @notice Bridge adapter for Tron using an off-chain relay/oracle path + * @dev Keeps fulfillment ids to make relay retries idempotent. + */ +contract TronAdapter is IChainAdapter, AccessControl, ReentrancyGuard { + using SafeERC20 for IERC20; + + bytes32 public constant BRIDGE_OPERATOR_ROLE = keccak256("BRIDGE_OPERATOR_ROLE"); + bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); + + bool public isActive; + uint256 public minFinalityBlocks; + + mapping(bytes32 => BridgeRequest) public bridgeRequests; + mapping(bytes32 => string) public txHashes; + mapping(bytes32 => uint256) public finalizedBlocks; + mapping(bytes32 => bytes32) public fulfillmentIdsByRequest; + mapping(bytes32 => bool) public usedFulfillmentIds; + mapping(address => uint256) public nonces; + + event TronBridgeInitiated( + bytes32 indexed requestId, + address indexed sender, + address indexed token, + uint256 amount, + string destination + ); + + event TronBridgeConfirmed( + bytes32 indexed requestId, + string txHash, + uint256 finalizedBlock, + bytes32 indexed fulfillmentId + ); + + event TronBridgeMarkedFailed(bytes32 indexed requestId, string reason); + + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(BRIDGE_OPERATOR_ROLE, admin); + _grantRole(ORACLE_ROLE, admin); + isActive = true; + minFinalityBlocks = 20; + } + + function getChainType() external pure override returns (string memory) { + return "Tron"; + } + + function getChainIdentifier() external pure override returns (uint256 chainId, string memory identifier) { + return (0, "Tron-Mainnet"); + } + + function validateDestination(bytes calldata destination) external pure override returns (bool) { + uint256 length = destination.length; + return length >= 34 && length <= 36; + } + + function bridge( + address token, + uint256 amount, + bytes calldata destination, + bytes calldata recipient + ) external payable override nonReentrant returns (bytes32 requestId) { + require(isActive, "Adapter inactive"); + require(amount > 0, "Zero amount"); + require(this.validateDestination(destination), "Invalid Tron destination"); + + requestId = keccak256( + abi.encodePacked( + msg.sender, + token, + amount, + destination, + recipient, + nonces[msg.sender]++, + block.timestamp + ) + ); + + if (token == address(0)) { + require(msg.value >= amount, "Insufficient ETH"); + } else { + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + } + + bridgeRequests[requestId] = BridgeRequest({ + sender: msg.sender, + token: token, + amount: amount, + destinationData: destination, + requestId: requestId, + status: BridgeStatus.Locked, + createdAt: block.timestamp, + completedAt: 0 + }); + + emit TronBridgeInitiated(requestId, msg.sender, token, amount, string(destination)); + return requestId; + } + + function confirmTransaction( + bytes32 requestId, + string calldata txHash, + uint256 finalizedBlock, + bytes32 fulfillmentId + ) external onlyRole(ORACLE_ROLE) { + BridgeRequest storage request = bridgeRequests[requestId]; + require(request.status == BridgeStatus.Locked, "Invalid status"); + require(bytes(txHash).length > 0, "Missing Tron tx hash"); + require(finalizedBlock >= minFinalityBlocks, "Insufficient finality"); + require(fulfillmentId != bytes32(0), "Missing fulfillment id"); + require(!usedFulfillmentIds[fulfillmentId], "Fulfillment already used"); + + usedFulfillmentIds[fulfillmentId] = true; + fulfillmentIdsByRequest[requestId] = fulfillmentId; + txHashes[requestId] = txHash; + finalizedBlocks[requestId] = finalizedBlock; + request.status = BridgeStatus.Confirmed; + request.completedAt = block.timestamp; + + emit TronBridgeConfirmed(requestId, txHash, finalizedBlock, fulfillmentId); + } + + function markFailed(bytes32 requestId, string calldata reason) external onlyRole(ORACLE_ROLE) { + BridgeRequest storage request = bridgeRequests[requestId]; + require(request.status == BridgeStatus.Locked, "Invalid status"); + + request.status = BridgeStatus.Failed; + request.completedAt = block.timestamp; + + emit TronBridgeMarkedFailed(requestId, reason); + } + + function getBridgeStatus(bytes32 requestId) external view override returns (BridgeRequest memory) { + return bridgeRequests[requestId]; + } + + function cancelBridge(bytes32 requestId) external override nonReentrant returns (bool) { + BridgeRequest storage request = bridgeRequests[requestId]; + require(request.status == BridgeStatus.Pending || request.status == BridgeStatus.Locked, "Cannot cancel"); + require(msg.sender == request.sender, "Not request sender"); + + if (request.token == address(0)) { + payable(request.sender).transfer(request.amount); + } else { + IERC20(request.token).safeTransfer(request.sender, request.amount); + } + + request.status = BridgeStatus.Cancelled; + request.completedAt = block.timestamp; + return true; + } + + function estimateFee( + address, + uint256, + bytes calldata + ) external pure override returns (uint256 fee) { + return 1000000000000000; + } + + function setIsActive(bool _isActive) external onlyRole(DEFAULT_ADMIN_ROLE) { + isActive = _isActive; + } + + function setMinFinalityBlocks(uint256 blocks_) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(blocks_ > 0, "Zero finality"); + minFinalityBlocks = blocks_; + } +} diff --git a/contracts/dex/DODOPMMIntegration.sol b/contracts/dex/DODOPMMIntegration.sol index 13c406d..c1a6bbb 100644 --- a/contracts/dex/DODOPMMIntegration.sol +++ b/contracts/dex/DODOPMMIntegration.sol @@ -637,10 +637,16 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard { require(IDODOPMMPool(pool)._BASE_TOKEN_() == expectedBaseToken, "DODOPMMIntegration: unexpected base token"); require(IDODOPMMPool(pool)._QUOTE_TOKEN_() == expectedQuoteToken, "DODOPMMIntegration: unexpected quote token"); - IDODOPMMPool(pool).getVaultReserve(); - IDODOPMMPool(pool).getMidPrice(); + (uint256 baseReserve, uint256 quoteReserve) = IDODOPMMPool(pool).getVaultReserve(); IDODOPMMPool(pool)._BASE_RESERVE_(); IDODOPMMPool(pool)._QUOTE_RESERVE_(); + + // Freshly created pools have zero reserves until the first buyShares/addLiquidity call. + // Skip getMidPrice on empty pools because some DODO deployments revert during PMM math + // before the initial seed has established target reserves. + if (baseReserve > 0 && quoteReserve > 0) { + IDODOPMMPool(pool).getMidPrice(); + } } function _setStandardPoolSurface( diff --git a/contracts/dex/recovered/RecoveredWave1PoolFamily2682.sol b/contracts/dex/recovered/RecoveredWave1PoolFamily2682.sol new file mode 100644 index 0000000..efbb656 --- /dev/null +++ b/contracts/dex/recovered/RecoveredWave1PoolFamily2682.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @notice Recovered verbatim Wave 1 runtime family artifact. +/// @dev Generated by scripts/verify/recover-wave1-pool-source-families.py from live chain bytecode. +contract RecoveredWave1PoolFamily2682 { + constructor() { + bytes memory runtime = hex"6040608081526004908136101561001557600080fd5b600090813560e01c928363199ab5fe146103da578363217a4b70146103b257836336223ce91461038a5783634a248d2a146103635783634c85b425146101ee5783636bdb678a146101ce57508263796da7af146100e05782637d7215041461010a5782639da771f414610197578263ab44a7a3146101b4578263bbf5ce7814610197578263c55dae6314610171578263d4b970461461014a578263d7a2e4c914610127578263dfdf2a721461010a578263ec2fd46d146100e557505063ee27c689146100e057600080fd5b6103f7565b34610106578160031936011261010657602090516706f05b59d3b200008152f35b5080fd5b346101065781600319360112610106576020906002549051908152f35b3461010657602036600319011261010657602090610143610626565b9051908152f35b3461010657816003193601126101065760015490516001600160a01b039091168152602090f35b34610106578160031936011261010657905490516001600160a01b039091168152602090f35b346101065781600319360112610106576020906003549051908152f35b346101065781600319360112610106576020905160038152f35b346101ea57826003193601126101ea5760209250549051908152f35b8280fd5b90346101ea576020908160031936011261035f576001600160a01b0383358181160361035b57808554169382519184836024816370a0823160e01b998a825230878301525afa92831561035157908591889461031c575b5060015416956024855180988193825230868301525afa9485156103125786956102db575b506060955061027b60025483610481565b9461028860035482610481565b928060025581600355151590816102d1575b506102c1575b506102b36102ae82866104bb565b610889565b928251948552840152820152f35b6102c961076b565b9055856102a0565b905015158761029a565b9094508381813d831161030b575b6102f38183610449565b81010312610307576060955051938661026a565b8580fd5b503d6102e9565b83513d88823e3d90fd5b8281939295503d831161034a575b6103348183610449565b81010312610346578490519288610245565b8680fd5b503d61032a565b84513d89823e3d90fd5b8480fd5b8380fd5b5034610106578160031936011261010657905490516001600160a01b039091168152602090f35b5090346103af57806003193601126103af575060025460035482519182526020820152f35b80fd5b503461010657816003193601126101065760015490516001600160a01b039091168152602090f35b5034610106576020366003190112610106576020906101436104fb565b3461041a57600036600319011261041a57602061041261041f565b604051908152f35b600080fd5b60025415801561043f575b6104395761043661076b565b90565b60045490565b506003541561042a565b90601f8019910116810190811067ffffffffffffffff82111761046b57604052565b634e487b7160e01b600052604160045260246000fd5b9190820391821161048e57565b634e487b7160e01b600052601160045260246000fd5b9061270d9182810292818404149015171561048e57565b8181029291811591840414171561048e57565b9190820180921161048e57565b81156104e5570490565b634e487b7160e01b600052601260045260246000fd5b600080546040516370a0823160e01b81523060048201529291906001600160a01b03906020908590602490829085165afa93841561061b5782946105e8575b506105486002548095610481565b91821580156105de575b6105d057610584612710610565856104a4565b049561057e60035497610578818a6104bb565b926104ce565b906104db565b948086116105d6575b5084156105d05750906105a9846105b1936001541633906108d3565b6002546104ce565b6002556105c082600354610481565b6003556105cb61076b565b600455565b93505050565b94503861058d565b5060035415610552565b9093506020813d8211610613575b8161060360209383610449565b810103126101065751923861053a565b3d91506105f6565b6040513d84823e3d90fd5b6001546040516370a0823160e01b815230600482015291906001600160a01b03906020908490602490829085165afa92831561073557600093610702575b506106726003548094610481565b90811580156106f8575b6106e8576106a261271061068f846104a4565b049461057e6002549661057881896104bb565b938085116106f0575b5083156106e857906106c6846106ce936000541633906108d3565b6003546104ce565b6003556106dd82600254610481565b6002556105cb61076b565b506000925050565b9350386106ab565b506002541561067c565b90926020823d821161072d575b8161071c60209383610449565b810103126103af5750519138610664565b3d915061070f565b6040513d6000823e3d90fd5b9081602091031261041a575160ff8116810361041a5790565b60ff16604d811161048e57600a0a90565b6000805460405163313ce56760e01b808252602093926001600160a01b039085908490600490829085165afa92831561087e57908591859461085f575b50600154169160046040518094819382525afa938415610854578394610825575b505060035491670de0b6b3a764000092838102938185041490151715610811575061080461043693926107fe61057e9361075a565b906104bb565b916107fe6002549161075a565b634e487b7160e01b81526011600452602490fd5b610845929450803d1061084d575b61083d8183610449565b810190610741565b9138806107c9565b503d610833565b6040513d85823e3d90fd5b610877919450823d841161084d5761083d8183610449565b92386107a8565b6040513d86823e3d90fd5b9081156108cd57600180830180841161048e57811c90835b8483106108ad57505050565b909193506108c4846108bf81846104db565b6104ce565b821c91906108a1565b60009150565b60405163a9059cbb60e01b602082019081526001600160a01b039384166024830152604480830195909552938152929167ffffffffffffffff91608085018381118682101761046b576040521692600080938192519082875af13d156109d4573d9182116109c0579061096891604051916109586020601f19601f8401160184610449565b82523d84602084013e5b846109e1565b908151918215159283610998575b5050506109805750565b60249060405190635274afe760e01b82526004820152fd5b8192935090602091810103126101065760200151908115918215036103af5750388080610976565b634e487b7160e01b83526041600452602483fd5b6109689150606090610962565b90610a0857508051156109f657805190602001fd5b604051630a12f52160e11b8152600490fd5b81511580610a3b575b610a19575090565b604051639996b31560e01b81526001600160a01b039091166004820152602490fd5b50803b15610a1156fea2646970667358221220bfbb45d7d38da668495affb4fb18c85f1b1b7a8bca2e5cf5f677952e715df40264736f6c63430008140033"; + assembly { + return(add(runtime, 0x20), mload(runtime)) + } + } +} diff --git a/contracts/dex/recovered/RecoveredWave1PoolFamily668.sol b/contracts/dex/recovered/RecoveredWave1PoolFamily668.sol new file mode 100644 index 0000000..4e35057 --- /dev/null +++ b/contracts/dex/recovered/RecoveredWave1PoolFamily668.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @notice Recovered verbatim Wave 1 runtime family artifact. +/// @dev Generated by scripts/verify/recover-wave1-pool-source-families.py from live chain bytecode. +contract RecoveredWave1PoolFamily668 { + constructor() { + bytes memory runtime = hex"604060808152600436101561001357600080fd5b6000803560e01c918263199ab5fe14610227578263217a4b701461012857826336223ce9146102005782634a248d2a1461014f5782634c85b425146101c95782636bdb678a146101ac578263796da7af146100dc5782637d721504146101065782639da771f414610175578263ab44a7a314610192578263bbf5ce7814610175578263c55dae631461014f578263d4b9704614610128578263d7a2e4c914610123578263dfdf2a7214610106578263ec2fd46d146100e157505063ee27c689146100dc57600080fd5b610248565b34610102578160031936011261010257602090516706f05b59d3b200008152f35b5080fd5b346101025781600319360112610102576020906002549051908152f35b610227565b3461010257816003193601126101025760015490516001600160a01b039091168152602090f35b34610102578160031936011261010257905490516001600160a01b039091168152602090f35b346101025781600319360112610102576020906003549051908152f35b346101025781600319360112610102576020905160038152f35b346101025781600319360112610102576020906004549051908152f35b34610102576020366003190112610102576004356001600160a01b0381160361010257606091815191818352816020840152820152f35b90346102245780600319360112610224575060025460035482519182526020820152f35b80fd5b3461024357602036600319011261024357602060405160008152f35b600080fd5b34610243576000366003190112610243576020600454604051908152f3fea2646970667358221220ced0ca29ab61872de6b0da1d116f43badd5e311e57c167714eeca3094a72269d64736f6c63430008140033"; + assembly { + return(add(runtime, 0x20), mload(runtime)) + } + } +} diff --git a/contracts/vendor/sushiswap-v2/LICENSE b/contracts/vendor/sushiswap-v2/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/contracts/vendor/sushiswap-v2/UniswapV2ERC20.sol b/contracts/vendor/sushiswap-v2/UniswapV2ERC20.sol new file mode 100644 index 0000000..7ed393c --- /dev/null +++ b/contracts/vendor/sushiswap-v2/UniswapV2ERC20.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.6.12; + +import './libraries/SafeMath.sol'; + +contract UniswapV2ERC20 { + using SafeMathUniswap for uint; + + string public constant name = 'SushiSwap LP Token'; + string public constant symbol = 'SLP'; + uint8 public constant decimals = 18; + uint public totalSupply; + mapping(address => uint) public balanceOf; + mapping(address => mapping(address => uint)) public allowance; + + bytes32 public DOMAIN_SEPARATOR; + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + mapping(address => uint) public nonces; + + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + constructor() public { + uint chainId; + assembly { + chainId := chainid() + } + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + keccak256(bytes(name)), + keccak256(bytes('1')), + chainId, + address(this) + ) + ); + } + + function _mint(address to, uint value) internal { + totalSupply = totalSupply.add(value); + balanceOf[to] = balanceOf[to].add(value); + emit Transfer(address(0), to, value); + } + + function _burn(address from, uint value) internal { + balanceOf[from] = balanceOf[from].sub(value); + totalSupply = totalSupply.sub(value); + emit Transfer(from, address(0), value); + } + + function _approve(address owner, address spender, uint value) private { + allowance[owner][spender] = value; + emit Approval(owner, spender, value); + } + + function _transfer(address from, address to, uint value) private { + balanceOf[from] = balanceOf[from].sub(value); + balanceOf[to] = balanceOf[to].add(value); + emit Transfer(from, to, value); + } + + function approve(address spender, uint value) external returns (bool) { + _approve(msg.sender, spender, value); + return true; + } + + function transfer(address to, uint value) external returns (bool) { + _transfer(msg.sender, to, value); + return true; + } + + function transferFrom(address from, address to, uint value) external returns (bool) { + if (allowance[from][msg.sender] != uint(-1)) { + allowance[from][msg.sender] = allowance[from][msg.sender].sub(value); + } + _transfer(from, to, value); + return true; + } + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { + require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); + bytes32 digest = keccak256( + abi.encodePacked( + '\x19\x01', + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE'); + _approve(owner, spender, value); + } +} diff --git a/contracts/vendor/sushiswap-v2/UniswapV2Factory.sol b/contracts/vendor/sushiswap-v2/UniswapV2Factory.sol new file mode 100644 index 0000000..f0a56fc --- /dev/null +++ b/contracts/vendor/sushiswap-v2/UniswapV2Factory.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.6.12; + +import './interfaces/IUniswapV2Factory.sol'; +import './UniswapV2Pair.sol'; + +contract UniswapV2Factory is IUniswapV2Factory { + address public override feeTo; + address public override feeToSetter; + address public override migrator; + + mapping(address => mapping(address => address)) public override getPair; + address[] public override allPairs; + + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + constructor(address _feeToSetter) public { + feeToSetter = _feeToSetter; + } + + function allPairsLength() external override view returns (uint) { + return allPairs.length; + } + + function pairCodeHash() external pure returns (bytes32) { + return keccak256(type(UniswapV2Pair).creationCode); + } + + function createPair(address tokenA, address tokenB) external override returns (address pair) { + require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); + require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient + bytes memory bytecode = type(UniswapV2Pair).creationCode; + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + assembly { + pair := create2(0, add(bytecode, 32), mload(bytecode), salt) + } + UniswapV2Pair(pair).initialize(token0, token1); + getPair[token0][token1] = pair; + getPair[token1][token0] = pair; // populate mapping in the reverse direction + allPairs.push(pair); + emit PairCreated(token0, token1, pair, allPairs.length); + } + + function setFeeTo(address _feeTo) external override { + require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); + feeTo = _feeTo; + } + + function setMigrator(address _migrator) external override { + require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); + migrator = _migrator; + } + + function setFeeToSetter(address _feeToSetter) external override { + require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); + feeToSetter = _feeToSetter; + } + +} diff --git a/contracts/vendor/sushiswap-v2/UniswapV2Pair.sol b/contracts/vendor/sushiswap-v2/UniswapV2Pair.sol new file mode 100644 index 0000000..408cde6 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/UniswapV2Pair.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.6.12; + +import './UniswapV2ERC20.sol'; +import './libraries/Math.sol'; +import './libraries/UQ112x112.sol'; +import './interfaces/IERC20.sol'; +import './interfaces/IUniswapV2Factory.sol'; +import './interfaces/IUniswapV2Callee.sol'; + +interface IMigrator { + // Return the desired amount of liquidity token that the migrator wants. + function desiredLiquidity() external view returns (uint256); +} + +contract UniswapV2Pair is UniswapV2ERC20 { + using SafeMathUniswap for uint; + using UQ112x112 for uint224; + + uint public constant MINIMUM_LIQUIDITY = 10**3; + bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); + + address public factory; + address public token0; + address public token1; + + uint112 private reserve0; // uses single storage slot, accessible via getReserves + uint112 private reserve1; // uses single storage slot, accessible via getReserves + uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves + + uint public price0CumulativeLast; + uint public price1CumulativeLast; + uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event + + uint private unlocked = 1; + modifier lock() { + require(unlocked == 1, 'UniswapV2: LOCKED'); + unlocked = 0; + _; + unlocked = 1; + } + + function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) { + _reserve0 = reserve0; + _reserve1 = reserve1; + _blockTimestampLast = blockTimestampLast; + } + + function _safeTransfer(address token, address to, uint value) private { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED'); + } + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + constructor() public { + factory = msg.sender; + } + + // called once by the factory at time of deployment + function initialize(address _token0, address _token1) external { + require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check + token0 = _token0; + token1 = _token1; + } + + // update reserves and, on the first call per block, price accumulators + function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { + require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); + uint32 blockTimestamp = uint32(block.timestamp % 2**32); + uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired + if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { + // * never overflows, and + overflow is desired + price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; + price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; + } + reserve0 = uint112(balance0); + reserve1 = uint112(balance1); + blockTimestampLast = blockTimestamp; + emit Sync(reserve0, reserve1); + } + + // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) + function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { + address feeTo = IUniswapV2Factory(factory).feeTo(); + feeOn = feeTo != address(0); + uint _kLast = kLast; // gas savings + if (feeOn) { + if (_kLast != 0) { + uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1)); + uint rootKLast = Math.sqrt(_kLast); + if (rootK > rootKLast) { + uint numerator = totalSupply.mul(rootK.sub(rootKLast)); + uint denominator = rootK.mul(5).add(rootKLast); + uint liquidity = numerator / denominator; + if (liquidity > 0) _mint(feeTo, liquidity); + } + } + } else if (_kLast != 0) { + kLast = 0; + } + } + + // this low-level function should be called from a contract which performs important safety checks + function mint(address to) external lock returns (uint liquidity) { + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + uint balance0 = IERC20Uniswap(token0).balanceOf(address(this)); + uint balance1 = IERC20Uniswap(token1).balanceOf(address(this)); + uint amount0 = balance0.sub(_reserve0); + uint amount1 = balance1.sub(_reserve1); + + bool feeOn = _mintFee(_reserve0, _reserve1); + uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee + if (_totalSupply == 0) { + address migrator = IUniswapV2Factory(factory).migrator(); + if (msg.sender == migrator) { + liquidity = IMigrator(migrator).desiredLiquidity(); + require(liquidity > 0 && liquidity != uint256(-1), "Bad desired liquidity"); + } else { + require(migrator == address(0), "Must not have migrator"); + liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); + _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens + } + } else { + liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); + } + require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); + _mint(to, liquidity); + + _update(balance0, balance1, _reserve0, _reserve1); + if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date + emit Mint(msg.sender, amount0, amount1); + } + + // this low-level function should be called from a contract which performs important safety checks + function burn(address to) external lock returns (uint amount0, uint amount1) { + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + address _token0 = token0; // gas savings + address _token1 = token1; // gas savings + uint balance0 = IERC20Uniswap(_token0).balanceOf(address(this)); + uint balance1 = IERC20Uniswap(_token1).balanceOf(address(this)); + uint liquidity = balanceOf[address(this)]; + + bool feeOn = _mintFee(_reserve0, _reserve1); + uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee + amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution + amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution + require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); + _burn(address(this), liquidity); + _safeTransfer(_token0, to, amount0); + _safeTransfer(_token1, to, amount1); + balance0 = IERC20Uniswap(_token0).balanceOf(address(this)); + balance1 = IERC20Uniswap(_token1).balanceOf(address(this)); + + _update(balance0, balance1, _reserve0, _reserve1); + if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date + emit Burn(msg.sender, amount0, amount1, to); + } + + // this low-level function should be called from a contract which performs important safety checks + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { + require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); + + uint balance0; + uint balance1; + { // scope for _token{0,1}, avoids stack too deep errors + address _token0 = token0; + address _token1 = token1; + require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); + if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens + if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens + if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); + balance0 = IERC20Uniswap(_token0).balanceOf(address(this)); + balance1 = IERC20Uniswap(_token1).balanceOf(address(this)); + } + uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; + uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; + require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); + { // scope for reserve{0,1}Adjusted, avoids stack too deep errors + uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); + uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); + require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); + } + + _update(balance0, balance1, _reserve0, _reserve1); + emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); + } + + // force balances to match reserves + function skim(address to) external lock { + address _token0 = token0; // gas savings + address _token1 = token1; // gas savings + _safeTransfer(_token0, to, IERC20Uniswap(_token0).balanceOf(address(this)).sub(reserve0)); + _safeTransfer(_token1, to, IERC20Uniswap(_token1).balanceOf(address(this)).sub(reserve1)); + } + + // force reserves to match balances + function sync() external lock { + _update(IERC20Uniswap(token0).balanceOf(address(this)), IERC20Uniswap(token1).balanceOf(address(this)), reserve0, reserve1); + } +} diff --git a/contracts/vendor/sushiswap-v2/UniswapV2Router02.sol b/contracts/vendor/sushiswap-v2/UniswapV2Router02.sol new file mode 100644 index 0000000..c873a09 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/UniswapV2Router02.sol @@ -0,0 +1,447 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.6.12; + +import './libraries/UniswapV2Library.sol'; +import './libraries/SafeMath.sol'; +import './libraries/TransferHelper.sol'; +import './interfaces/IUniswapV2Router02.sol'; +import './interfaces/IUniswapV2Factory.sol'; +import './interfaces/IERC20.sol'; +import './interfaces/IWETH.sol'; + +contract UniswapV2Router02 is IUniswapV2Router02 { + using SafeMathUniswap for uint; + + address public immutable override factory; + address public immutable override WETH; + + modifier ensure(uint deadline) { + require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); + _; + } + + constructor(address _factory, address _WETH) public { + factory = _factory; + WETH = _WETH; + } + + receive() external payable { + assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract + } + + // **** ADD LIQUIDITY **** + function _addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin + ) internal virtual returns (uint amountA, uint amountB) { + // create the pair if it doesn't exist yet + if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { + IUniswapV2Factory(factory).createPair(tokenA, tokenB); + } + (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); + if (reserveA == 0 && reserveB == 0) { + (amountA, amountB) = (amountADesired, amountBDesired); + } else { + uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); + if (amountBOptimal <= amountBDesired) { + require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + (amountA, amountB) = (amountADesired, amountBOptimal); + } else { + uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); + assert(amountAOptimal <= amountADesired); + require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + (amountA, amountB) = (amountAOptimal, amountBDesired); + } + } + } + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { + (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); + TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); + liquidity = IUniswapV2Pair(pair).mint(to); + } + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) { + (amountToken, amountETH) = _addLiquidity( + token, + WETH, + amountTokenDesired, + msg.value, + amountTokenMin, + amountETHMin + ); + address pair = UniswapV2Library.pairFor(factory, token, WETH); + TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); + IWETH(WETH).deposit{value: amountETH}(); + assert(IWETH(WETH).transfer(pair, amountETH)); + liquidity = IUniswapV2Pair(pair).mint(to); + // refund dust eth, if any + if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); + } + + // **** REMOVE LIQUIDITY **** + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) { + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair + (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); + (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); + (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); + require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + } + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) { + (amountToken, amountETH) = removeLiquidity( + token, + WETH, + liquidity, + amountTokenMin, + amountETHMin, + address(this), + deadline + ); + TransferHelper.safeTransfer(token, to, amountToken); + IWETH(WETH).withdraw(amountETH); + TransferHelper.safeTransferETH(to, amountETH); + } + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual override returns (uint amountA, uint amountB) { + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + uint value = approveMax ? uint(-1) : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); + } + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual override returns (uint amountToken, uint amountETH) { + address pair = UniswapV2Library.pairFor(factory, token, WETH); + uint value = approveMax ? uint(-1) : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline); + } + + // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) **** + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) public virtual override ensure(deadline) returns (uint amountETH) { + (, amountETH) = removeLiquidity( + token, + WETH, + liquidity, + amountTokenMin, + amountETHMin, + address(this), + deadline + ); + TransferHelper.safeTransfer(token, to, IERC20Uniswap(token).balanceOf(address(this))); + IWETH(WETH).withdraw(amountETH); + TransferHelper.safeTransferETH(to, amountETH); + } + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual override returns (uint amountETH) { + address pair = UniswapV2Library.pairFor(factory, token, WETH); + uint value = approveMax ? uint(-1) : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + amountETH = removeLiquidityETHSupportingFeeOnTransferTokens( + token, liquidity, amountTokenMin, amountETHMin, to, deadline + ); + } + + // **** SWAP **** + // requires the initial amount to have already been sent to the first pair + function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { + for (uint i; i < path.length - 1; i++) { + (address input, address output) = (path[i], path[i + 1]); + (address token0,) = UniswapV2Library.sortTokens(input, output); + uint amountOut = amounts[i + 1]; + (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); + address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; + IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( + amount0Out, amount1Out, to, new bytes(0) + ); + } + } + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external virtual override ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external virtual override ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + virtual + override + payable + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + IWETH(WETH).deposit{value: amounts[0]}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); + _swap(amounts, path, to); + } + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + virtual + override + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, address(this)); + IWETH(WETH).withdraw(amounts[amounts.length - 1]); + TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); + } + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + virtual + override + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, address(this)); + IWETH(WETH).withdraw(amounts[amounts.length - 1]); + TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); + } + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + virtual + override + payable + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + IWETH(WETH).deposit{value: amounts[0]}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); + _swap(amounts, path, to); + // refund dust eth, if any + if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]); + } + + // **** SWAP (supporting fee-on-transfer tokens) **** + // requires the initial amount to have already been sent to the first pair + function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual { + for (uint i; i < path.length - 1; i++) { + (address input, address output) = (path[i], path[i + 1]); + (address token0,) = UniswapV2Library.sortTokens(input, output); + IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)); + uint amountInput; + uint amountOutput; + { // scope to avoid stack too deep errors + (uint reserve0, uint reserve1,) = pair.getReserves(); + (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + amountInput = IERC20Uniswap(input).balanceOf(address(pair)).sub(reserveInput); + amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput); + } + (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0)); + address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; + pair.swap(amount0Out, amount1Out, to, new bytes(0)); + } + } + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external virtual override ensure(deadline) { + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn + ); + uint balanceBefore = IERC20Uniswap(path[path.length - 1]).balanceOf(to); + _swapSupportingFeeOnTransferTokens(path, to); + require( + IERC20Uniswap(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, + 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' + ); + } + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) + external + virtual + override + payable + ensure(deadline) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + uint amountIn = msg.value; + IWETH(WETH).deposit{value: amountIn}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn)); + uint balanceBefore = IERC20Uniswap(path[path.length - 1]).balanceOf(to); + _swapSupportingFeeOnTransferTokens(path, to); + require( + IERC20Uniswap(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, + 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' + ); + } + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) + external + virtual + override + ensure(deadline) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn + ); + _swapSupportingFeeOnTransferTokens(path, address(this)); + uint amountOut = IERC20Uniswap(WETH).balanceOf(address(this)); + require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + IWETH(WETH).withdraw(amountOut); + TransferHelper.safeTransferETH(to, amountOut); + } + + // **** LIBRARY FUNCTIONS **** + function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) { + return UniswapV2Library.quote(amountA, reserveA, reserveB); + } + + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) + public + pure + virtual + override + returns (uint amountOut) + { + return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); + } + + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) + public + pure + virtual + override + returns (uint amountIn) + { + return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut); + } + + function getAmountsOut(uint amountIn, address[] memory path) + public + view + virtual + override + returns (uint[] memory amounts) + { + return UniswapV2Library.getAmountsOut(factory, amountIn, path); + } + + function getAmountsIn(uint amountOut, address[] memory path) + public + view + virtual + override + returns (uint[] memory amounts) + { + return UniswapV2Library.getAmountsIn(factory, amountOut, path); + } +} diff --git a/contracts/vendor/sushiswap-v2/interfaces/IERC20.sol b/contracts/vendor/sushiswap-v2/interfaces/IERC20.sol new file mode 100644 index 0000000..fd7b169 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/interfaces/IERC20.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +interface IERC20Uniswap { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); +} diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Callee.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Callee.sol new file mode 100644 index 0000000..a11b1c4 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Callee.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2ERC20.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2ERC20.sol new file mode 100644 index 0000000..41fb595 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2ERC20.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +interface IUniswapV2ERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; +} \ No newline at end of file diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Factory.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Factory.sol new file mode 100644 index 0000000..aea4477 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Factory.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +interface IUniswapV2Factory { + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + function feeTo() external view returns (address); + function feeToSetter() external view returns (address); + function migrator() external view returns (address); + + function getPair(address tokenA, address tokenB) external view returns (address pair); + function allPairs(uint) external view returns (address pair); + function allPairsLength() external view returns (uint); + + function createPair(address tokenA, address tokenB) external returns (address pair); + + function setFeeTo(address) external; + function setFeeToSetter(address) external; + function setMigrator(address) external; +} diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Pair.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Pair.sol new file mode 100644 index 0000000..9c69f70 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Pair.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +interface IUniswapV2Pair { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint); + function price1CumulativeLast() external view returns (uint); + function kLast() external view returns (uint); + + function mint(address to) external returns (uint liquidity); + function burn(address to) external returns (uint amount0, uint amount1); + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} \ No newline at end of file diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router01.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router01.sol new file mode 100644 index 0000000..095f895 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router01.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.6.2; + +interface IUniswapV2Router01 { + function factory() external pure returns (address); + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external payable returns (uint amountToken, uint amountETH, uint liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountToken, uint amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountA, uint amountB); + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountToken, uint amountETH); + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + + function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); +} \ No newline at end of file diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router02.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router02.sol new file mode 100644 index 0000000..4fca80d --- /dev/null +++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router02.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.6.2; + +import './IUniswapV2Router01.sol'; + +interface IUniswapV2Router02 is IUniswapV2Router01 { + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountETH); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external payable; + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; +} \ No newline at end of file diff --git a/contracts/vendor/sushiswap-v2/interfaces/IWETH.sol b/contracts/vendor/sushiswap-v2/interfaces/IWETH.sol new file mode 100644 index 0000000..775da06 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/interfaces/IWETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +interface IWETH { + function deposit() external payable; + function transfer(address to, uint value) external returns (bool); + function withdraw(uint) external; +} \ No newline at end of file diff --git a/contracts/vendor/sushiswap-v2/libraries/Math.sol b/contracts/vendor/sushiswap-v2/libraries/Math.sol new file mode 100644 index 0000000..d183d80 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/libraries/Math.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.6.12; + +// a library for performing various math operations + +library Math { + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } +} diff --git a/contracts/vendor/sushiswap-v2/libraries/SafeMath.sol b/contracts/vendor/sushiswap-v2/libraries/SafeMath.sol new file mode 100644 index 0000000..1cc199b --- /dev/null +++ b/contracts/vendor/sushiswap-v2/libraries/SafeMath.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.6.12; + +// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math) + +library SafeMathUniswap { + function add(uint x, uint y) internal pure returns (uint z) { + require((z = x + y) >= x, 'ds-math-add-overflow'); + } + + function sub(uint x, uint y) internal pure returns (uint z) { + require((z = x - y) <= x, 'ds-math-sub-underflow'); + } + + function mul(uint x, uint y) internal pure returns (uint z) { + require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow'); + } +} diff --git a/contracts/vendor/sushiswap-v2/libraries/TransferHelper.sol b/contracts/vendor/sushiswap-v2/libraries/TransferHelper.sol new file mode 100644 index 0000000..d179a67 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/libraries/TransferHelper.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.6.0; + +// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false +library TransferHelper { + function safeApprove(address token, address to, uint value) internal { + // bytes4(keccak256(bytes('approve(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: APPROVE_FAILED'); + } + + function safeTransfer(address token, address to, uint value) internal { + // bytes4(keccak256(bytes('transfer(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED'); + } + + function safeTransferFrom(address token, address from, address to, uint value) internal { + // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED'); + } + + function safeTransferETH(address to, uint value) internal { + (bool success,) = to.call{value:value}(new bytes(0)); + require(success, 'TransferHelper: ETH_TRANSFER_FAILED'); + } +} diff --git a/contracts/vendor/sushiswap-v2/libraries/UQ112x112.sol b/contracts/vendor/sushiswap-v2/libraries/UQ112x112.sol new file mode 100644 index 0000000..9af8f74 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/libraries/UQ112x112.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity =0.6.12; + +// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) + +// range: [0, 2**112 - 1] +// resolution: 1 / 2**112 + +library UQ112x112 { + uint224 constant Q112 = 2**112; + + // encode a uint112 as a UQ112x112 + function encode(uint112 y) internal pure returns (uint224 z) { + z = uint224(y) * Q112; // never overflows + } + + // divide a UQ112x112 by a uint112, returning a UQ112x112 + function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { + z = x / uint224(y); + } +} diff --git a/contracts/vendor/sushiswap-v2/libraries/UniswapV2Library.sol b/contracts/vendor/sushiswap-v2/libraries/UniswapV2Library.sol new file mode 100644 index 0000000..14cffb0 --- /dev/null +++ b/contracts/vendor/sushiswap-v2/libraries/UniswapV2Library.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +import '../interfaces/IUniswapV2Pair.sol'; + +import "./SafeMath.sol"; + +library UniswapV2Library { + using SafeMathUniswap for uint; + + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); + } + + // calculates the CREATE2 address for a pair without making any external calls + function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address(uint(keccak256(abi.encodePacked( + hex'ff', + factory, + keccak256(abi.encodePacked(token0, token1)), + hex'e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303' // init code hash + )))); + } + + // fetches and sorts the reserves for a pair + function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { + (address token0,) = sortTokens(tokenA, tokenB); + (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } + + // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset + function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { + require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); + require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + amountB = amountA.mul(reserveB) / reserveA; + } + + // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { + require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint amountInWithFee = amountIn.mul(997); + uint numerator = amountInWithFee.mul(reserveOut); + uint denominator = reserveIn.mul(1000).add(amountInWithFee); + amountOut = numerator / denominator; + } + + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { + require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint numerator = reserveIn.mul(amountOut).mul(1000); + uint denominator = reserveOut.sub(amountOut).mul(997); + amountIn = (numerator / denominator).add(1); + } + + // performs chained getAmountOut calculations on any number of pairs + function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[0] = amountIn; + for (uint i; i < path.length - 1; i++) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); + amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); + } + } + + // performs chained getAmountIn calculations on any number of pairs + function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[amounts.length - 1] = amountOut; + for (uint i = path.length - 1; i > 0; i--) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); + amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); + } + } +} diff --git a/contracts/vendor/uniswap-v2-core/UniswapV2ERC20.sol b/contracts/vendor/uniswap-v2-core/UniswapV2ERC20.sol new file mode 100644 index 0000000..404a8be --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/UniswapV2ERC20.sol @@ -0,0 +1,94 @@ +pragma solidity =0.5.16; + +import './interfaces/IUniswapV2ERC20.sol'; +import './libraries/SafeMath.sol'; + +contract UniswapV2ERC20 is IUniswapV2ERC20 { + using SafeMath for uint; + + string public constant name = 'Uniswap V2'; + string public constant symbol = 'UNI-V2'; + uint8 public constant decimals = 18; + uint public totalSupply; + mapping(address => uint) public balanceOf; + mapping(address => mapping(address => uint)) public allowance; + + bytes32 public DOMAIN_SEPARATOR; + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + mapping(address => uint) public nonces; + + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + constructor() public { + uint chainId; + assembly { + chainId := chainid + } + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + keccak256(bytes(name)), + keccak256(bytes('1')), + chainId, + address(this) + ) + ); + } + + function _mint(address to, uint value) internal { + totalSupply = totalSupply.add(value); + balanceOf[to] = balanceOf[to].add(value); + emit Transfer(address(0), to, value); + } + + function _burn(address from, uint value) internal { + balanceOf[from] = balanceOf[from].sub(value); + totalSupply = totalSupply.sub(value); + emit Transfer(from, address(0), value); + } + + function _approve(address owner, address spender, uint value) private { + allowance[owner][spender] = value; + emit Approval(owner, spender, value); + } + + function _transfer(address from, address to, uint value) private { + balanceOf[from] = balanceOf[from].sub(value); + balanceOf[to] = balanceOf[to].add(value); + emit Transfer(from, to, value); + } + + function approve(address spender, uint value) external returns (bool) { + _approve(msg.sender, spender, value); + return true; + } + + function transfer(address to, uint value) external returns (bool) { + _transfer(msg.sender, to, value); + return true; + } + + function transferFrom(address from, address to, uint value) external returns (bool) { + if (allowance[from][msg.sender] != uint(-1)) { + allowance[from][msg.sender] = allowance[from][msg.sender].sub(value); + } + _transfer(from, to, value); + return true; + } + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { + require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); + bytes32 digest = keccak256( + abi.encodePacked( + '\x19\x01', + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE'); + _approve(owner, spender, value); + } +} diff --git a/contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol b/contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol new file mode 100644 index 0000000..a66ad82 --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol @@ -0,0 +1,49 @@ +pragma solidity =0.5.16; + +import './interfaces/IUniswapV2Factory.sol'; +import './UniswapV2Pair.sol'; + +contract UniswapV2Factory is IUniswapV2Factory { + address public feeTo; + address public feeToSetter; + + mapping(address => mapping(address => address)) public getPair; + address[] public allPairs; + + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + constructor(address _feeToSetter) public { + feeToSetter = _feeToSetter; + } + + function allPairsLength() external view returns (uint) { + return allPairs.length; + } + + function createPair(address tokenA, address tokenB) external returns (address pair) { + require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); + require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient + bytes memory bytecode = type(UniswapV2Pair).creationCode; + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + assembly { + pair := create2(0, add(bytecode, 32), mload(bytecode), salt) + } + IUniswapV2Pair(pair).initialize(token0, token1); + getPair[token0][token1] = pair; + getPair[token1][token0] = pair; // populate mapping in the reverse direction + allPairs.push(pair); + emit PairCreated(token0, token1, pair, allPairs.length); + } + + function setFeeTo(address _feeTo) external { + require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); + feeTo = _feeTo; + } + + function setFeeToSetter(address _feeToSetter) external { + require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); + feeToSetter = _feeToSetter; + } +} diff --git a/contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol b/contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol new file mode 100644 index 0000000..f87a1db --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol @@ -0,0 +1,201 @@ +pragma solidity =0.5.16; + +import './interfaces/IUniswapV2Pair.sol'; +import './UniswapV2ERC20.sol'; +import './libraries/Math.sol'; +import './libraries/UQ112x112.sol'; +import './interfaces/IERC20.sol'; +import './interfaces/IUniswapV2Factory.sol'; +import './interfaces/IUniswapV2Callee.sol'; + +contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { + using SafeMath for uint; + using UQ112x112 for uint224; + + uint public constant MINIMUM_LIQUIDITY = 10**3; + bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); + + address public factory; + address public token0; + address public token1; + + uint112 private reserve0; // uses single storage slot, accessible via getReserves + uint112 private reserve1; // uses single storage slot, accessible via getReserves + uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves + + uint public price0CumulativeLast; + uint public price1CumulativeLast; + uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event + + uint private unlocked = 1; + modifier lock() { + require(unlocked == 1, 'UniswapV2: LOCKED'); + unlocked = 0; + _; + unlocked = 1; + } + + function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) { + _reserve0 = reserve0; + _reserve1 = reserve1; + _blockTimestampLast = blockTimestampLast; + } + + function _safeTransfer(address token, address to, uint value) private { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED'); + } + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + constructor() public { + factory = msg.sender; + } + + // called once by the factory at time of deployment + function initialize(address _token0, address _token1) external { + require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check + token0 = _token0; + token1 = _token1; + } + + // update reserves and, on the first call per block, price accumulators + function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { + require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); + uint32 blockTimestamp = uint32(block.timestamp % 2**32); + uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired + if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { + // * never overflows, and + overflow is desired + price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; + price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; + } + reserve0 = uint112(balance0); + reserve1 = uint112(balance1); + blockTimestampLast = blockTimestamp; + emit Sync(reserve0, reserve1); + } + + // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) + function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { + address feeTo = IUniswapV2Factory(factory).feeTo(); + feeOn = feeTo != address(0); + uint _kLast = kLast; // gas savings + if (feeOn) { + if (_kLast != 0) { + uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1)); + uint rootKLast = Math.sqrt(_kLast); + if (rootK > rootKLast) { + uint numerator = totalSupply.mul(rootK.sub(rootKLast)); + uint denominator = rootK.mul(5).add(rootKLast); + uint liquidity = numerator / denominator; + if (liquidity > 0) _mint(feeTo, liquidity); + } + } + } else if (_kLast != 0) { + kLast = 0; + } + } + + // this low-level function should be called from a contract which performs important safety checks + function mint(address to) external lock returns (uint liquidity) { + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + uint balance0 = IERC20(token0).balanceOf(address(this)); + uint balance1 = IERC20(token1).balanceOf(address(this)); + uint amount0 = balance0.sub(_reserve0); + uint amount1 = balance1.sub(_reserve1); + + bool feeOn = _mintFee(_reserve0, _reserve1); + uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee + if (_totalSupply == 0) { + liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); + _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens + } else { + liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); + } + require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); + _mint(to, liquidity); + + _update(balance0, balance1, _reserve0, _reserve1); + if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date + emit Mint(msg.sender, amount0, amount1); + } + + // this low-level function should be called from a contract which performs important safety checks + function burn(address to) external lock returns (uint amount0, uint amount1) { + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + address _token0 = token0; // gas savings + address _token1 = token1; // gas savings + uint balance0 = IERC20(_token0).balanceOf(address(this)); + uint balance1 = IERC20(_token1).balanceOf(address(this)); + uint liquidity = balanceOf[address(this)]; + + bool feeOn = _mintFee(_reserve0, _reserve1); + uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee + amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution + amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution + require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); + _burn(address(this), liquidity); + _safeTransfer(_token0, to, amount0); + _safeTransfer(_token1, to, amount1); + balance0 = IERC20(_token0).balanceOf(address(this)); + balance1 = IERC20(_token1).balanceOf(address(this)); + + _update(balance0, balance1, _reserve0, _reserve1); + if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date + emit Burn(msg.sender, amount0, amount1, to); + } + + // this low-level function should be called from a contract which performs important safety checks + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { + require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); + (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings + require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); + + uint balance0; + uint balance1; + { // scope for _token{0,1}, avoids stack too deep errors + address _token0 = token0; + address _token1 = token1; + require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); + if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens + if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens + if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); + balance0 = IERC20(_token0).balanceOf(address(this)); + balance1 = IERC20(_token1).balanceOf(address(this)); + } + uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; + uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; + require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); + { // scope for reserve{0,1}Adjusted, avoids stack too deep errors + uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); + uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); + require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); + } + + _update(balance0, balance1, _reserve0, _reserve1); + emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); + } + + // force balances to match reserves + function skim(address to) external lock { + address _token0 = token0; // gas savings + address _token1 = token1; // gas savings + _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0)); + _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1)); + } + + // force reserves to match balances + function sync() external lock { + _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1); + } +} diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IERC20.sol b/contracts/vendor/uniswap-v2-core/interfaces/IERC20.sol new file mode 100644 index 0000000..c1e8c3e --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/interfaces/IERC20.sol @@ -0,0 +1,17 @@ +pragma solidity >=0.5.0; + +interface IERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); +} diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Callee.sol b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Callee.sol new file mode 100644 index 0000000..f3910ab --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Callee.sol @@ -0,0 +1,5 @@ +pragma solidity >=0.5.0; + +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2ERC20.sol b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2ERC20.sol new file mode 100644 index 0000000..8718931 --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2ERC20.sol @@ -0,0 +1,23 @@ +pragma solidity >=0.5.0; + +interface IUniswapV2ERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; +} diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Factory.sol b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Factory.sol new file mode 100644 index 0000000..e73dc59 --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Factory.sol @@ -0,0 +1,17 @@ +pragma solidity >=0.5.0; + +interface IUniswapV2Factory { + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + function feeTo() external view returns (address); + function feeToSetter() external view returns (address); + + function getPair(address tokenA, address tokenB) external view returns (address pair); + function allPairs(uint) external view returns (address pair); + function allPairsLength() external view returns (uint); + + function createPair(address tokenA, address tokenB) external returns (address pair); + + function setFeeTo(address) external; + function setFeeToSetter(address) external; +} diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Pair.sol b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Pair.sol new file mode 100644 index 0000000..762dd4d --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Pair.sol @@ -0,0 +1,52 @@ +pragma solidity >=0.5.0; + +interface IUniswapV2Pair { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint); + function price1CumulativeLast() external view returns (uint); + function kLast() external view returns (uint); + + function mint(address to) external returns (uint liquidity); + function burn(address to) external returns (uint amount0, uint amount1); + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} diff --git a/contracts/vendor/uniswap-v2-core/libraries/Math.sol b/contracts/vendor/uniswap-v2-core/libraries/Math.sol new file mode 100644 index 0000000..1cab10d --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/libraries/Math.sol @@ -0,0 +1,23 @@ +pragma solidity =0.5.16; + +// a library for performing various math operations + +library Math { + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } +} diff --git a/contracts/vendor/uniswap-v2-core/libraries/SafeMath.sol b/contracts/vendor/uniswap-v2-core/libraries/SafeMath.sol new file mode 100644 index 0000000..f2fbe16 --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/libraries/SafeMath.sol @@ -0,0 +1,17 @@ +pragma solidity =0.5.16; + +// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math) + +library SafeMath { + function add(uint x, uint y) internal pure returns (uint z) { + require((z = x + y) >= x, 'ds-math-add-overflow'); + } + + function sub(uint x, uint y) internal pure returns (uint z) { + require((z = x - y) <= x, 'ds-math-sub-underflow'); + } + + function mul(uint x, uint y) internal pure returns (uint z) { + require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow'); + } +} diff --git a/contracts/vendor/uniswap-v2-core/libraries/UQ112x112.sol b/contracts/vendor/uniswap-v2-core/libraries/UQ112x112.sol new file mode 100644 index 0000000..a453f72 --- /dev/null +++ b/contracts/vendor/uniswap-v2-core/libraries/UQ112x112.sol @@ -0,0 +1,20 @@ +pragma solidity =0.5.16; + +// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) + +// range: [0, 2**112 - 1] +// resolution: 1 / 2**112 + +library UQ112x112 { + uint224 constant Q112 = 2**112; + + // encode a uint112 as a UQ112x112 + function encode(uint112 y) internal pure returns (uint224 z) { + z = uint224(y) * Q112; // never overflows + } + + // divide a UQ112x112 by a uint112, returning a UQ112x112 + function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { + z = x / uint224(y); + } +} diff --git a/contracts/vendor/uniswap-v2-periphery/UniswapV2Router02.sol b/contracts/vendor/uniswap-v2-periphery/UniswapV2Router02.sol new file mode 100644 index 0000000..23ff4b0 --- /dev/null +++ b/contracts/vendor/uniswap-v2-periphery/UniswapV2Router02.sol @@ -0,0 +1,446 @@ +pragma solidity =0.6.6; + +import '../uniswap-v2-core/interfaces/IUniswapV2Factory.sol'; +import '../sushiswap-v2/libraries/TransferHelper.sol'; + +import './interfaces/IUniswapV2Router02.sol'; +import './libraries/UniswapV2Library.sol'; +import './libraries/SafeMath.sol'; +import './interfaces/IERC20.sol'; +import './interfaces/IWETH.sol'; + +contract UniswapV2Router02 is IUniswapV2Router02 { + using SafeMath for uint; + + address public immutable override factory; + address public immutable override WETH; + + modifier ensure(uint deadline) { + require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); + _; + } + + constructor(address _factory, address _WETH) public { + factory = _factory; + WETH = _WETH; + } + + receive() external payable { + assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract + } + + // **** ADD LIQUIDITY **** + function _addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin + ) internal virtual returns (uint amountA, uint amountB) { + // create the pair if it doesn't exist yet + if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { + IUniswapV2Factory(factory).createPair(tokenA, tokenB); + } + (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); + if (reserveA == 0 && reserveB == 0) { + (amountA, amountB) = (amountADesired, amountBDesired); + } else { + uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); + if (amountBOptimal <= amountBDesired) { + require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + (amountA, amountB) = (amountADesired, amountBOptimal); + } else { + uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); + assert(amountAOptimal <= amountADesired); + require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + (amountA, amountB) = (amountAOptimal, amountBDesired); + } + } + } + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { + (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); + TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); + liquidity = IUniswapV2Pair(pair).mint(to); + } + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) { + (amountToken, amountETH) = _addLiquidity( + token, + WETH, + amountTokenDesired, + msg.value, + amountTokenMin, + amountETHMin + ); + address pair = UniswapV2Library.pairFor(factory, token, WETH); + TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); + IWETH(WETH).deposit{value: amountETH}(); + assert(IWETH(WETH).transfer(pair, amountETH)); + liquidity = IUniswapV2Pair(pair).mint(to); + // refund dust eth, if any + if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); + } + + // **** REMOVE LIQUIDITY **** + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) { + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair + (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); + (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); + (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); + require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + } + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) { + (amountToken, amountETH) = removeLiquidity( + token, + WETH, + liquidity, + amountTokenMin, + amountETHMin, + address(this), + deadline + ); + TransferHelper.safeTransfer(token, to, amountToken); + IWETH(WETH).withdraw(amountETH); + TransferHelper.safeTransferETH(to, amountETH); + } + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual override returns (uint amountA, uint amountB) { + address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); + uint value = approveMax ? uint(-1) : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); + } + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual override returns (uint amountToken, uint amountETH) { + address pair = UniswapV2Library.pairFor(factory, token, WETH); + uint value = approveMax ? uint(-1) : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline); + } + + // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) **** + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) public virtual override ensure(deadline) returns (uint amountETH) { + (, amountETH) = removeLiquidity( + token, + WETH, + liquidity, + amountTokenMin, + amountETHMin, + address(this), + deadline + ); + TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this))); + IWETH(WETH).withdraw(amountETH); + TransferHelper.safeTransferETH(to, amountETH); + } + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external virtual override returns (uint amountETH) { + address pair = UniswapV2Library.pairFor(factory, token, WETH); + uint value = approveMax ? uint(-1) : liquidity; + IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); + amountETH = removeLiquidityETHSupportingFeeOnTransferTokens( + token, liquidity, amountTokenMin, amountETHMin, to, deadline + ); + } + + // **** SWAP **** + // requires the initial amount to have already been sent to the first pair + function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { + for (uint i; i < path.length - 1; i++) { + (address input, address output) = (path[i], path[i + 1]); + (address token0,) = UniswapV2Library.sortTokens(input, output); + uint amountOut = amounts[i + 1]; + (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); + address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; + IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( + amount0Out, amount1Out, to, new bytes(0) + ); + } + } + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external virtual override ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external virtual override ensure(deadline) returns (uint[] memory amounts) { + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, to); + } + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + virtual + override + payable + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + IWETH(WETH).deposit{value: amounts[0]}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); + _swap(amounts, path, to); + } + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + virtual + override + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, address(this)); + IWETH(WETH).withdraw(amounts[amounts.length - 1]); + TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); + } + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + virtual + override + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] + ); + _swap(amounts, path, address(this)); + IWETH(WETH).withdraw(amounts[amounts.length - 1]); + TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); + } + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + virtual + override + payable + ensure(deadline) + returns (uint[] memory amounts) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); + require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); + IWETH(WETH).deposit{value: amounts[0]}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); + _swap(amounts, path, to); + // refund dust eth, if any + if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]); + } + + // **** SWAP (supporting fee-on-transfer tokens) **** + // requires the initial amount to have already been sent to the first pair + function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual { + for (uint i; i < path.length - 1; i++) { + (address input, address output) = (path[i], path[i + 1]); + (address token0,) = UniswapV2Library.sortTokens(input, output); + IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)); + uint amountInput; + uint amountOutput; + { // scope to avoid stack too deep errors + (uint reserve0, uint reserve1,) = pair.getReserves(); + (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput); + amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput); + } + (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0)); + address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; + pair.swap(amount0Out, amount1Out, to, new bytes(0)); + } + } + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external virtual override ensure(deadline) { + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn + ); + uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); + _swapSupportingFeeOnTransferTokens(path, to); + require( + IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, + 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' + ); + } + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) + external + virtual + override + payable + ensure(deadline) + { + require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); + uint amountIn = msg.value; + IWETH(WETH).deposit{value: amountIn}(); + assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn)); + uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); + _swapSupportingFeeOnTransferTokens(path, to); + require( + IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, + 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' + ); + } + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) + external + virtual + override + ensure(deadline) + { + require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); + TransferHelper.safeTransferFrom( + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn + ); + _swapSupportingFeeOnTransferTokens(path, address(this)); + uint amountOut = IERC20(WETH).balanceOf(address(this)); + require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + IWETH(WETH).withdraw(amountOut); + TransferHelper.safeTransferETH(to, amountOut); + } + + // **** LIBRARY FUNCTIONS **** + function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) { + return UniswapV2Library.quote(amountA, reserveA, reserveB); + } + + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) + public + pure + virtual + override + returns (uint amountOut) + { + return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); + } + + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) + public + pure + virtual + override + returns (uint amountIn) + { + return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut); + } + + function getAmountsOut(uint amountIn, address[] memory path) + public + view + virtual + override + returns (uint[] memory amounts) + { + return UniswapV2Library.getAmountsOut(factory, amountIn, path); + } + + function getAmountsIn(uint amountOut, address[] memory path) + public + view + virtual + override + returns (uint[] memory amounts) + { + return UniswapV2Library.getAmountsIn(factory, amountOut, path); + } +} diff --git a/contracts/vendor/uniswap-v2-periphery/interfaces/IERC20.sol b/contracts/vendor/uniswap-v2-periphery/interfaces/IERC20.sol new file mode 100644 index 0000000..c1e8c3e --- /dev/null +++ b/contracts/vendor/uniswap-v2-periphery/interfaces/IERC20.sol @@ -0,0 +1,17 @@ +pragma solidity >=0.5.0; + +interface IERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); +} diff --git a/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router01.sol b/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router01.sol new file mode 100644 index 0000000..4619967 --- /dev/null +++ b/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router01.sol @@ -0,0 +1,95 @@ +pragma solidity >=0.6.2; + +interface IUniswapV2Router01 { + function factory() external pure returns (address); + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external payable returns (uint amountToken, uint amountETH, uint liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountToken, uint amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountA, uint amountB); + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountToken, uint amountETH); + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + + function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); +} diff --git a/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router02.sol b/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router02.sol new file mode 100644 index 0000000..1fc7b0a --- /dev/null +++ b/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router02.sol @@ -0,0 +1,44 @@ +pragma solidity >=0.6.2; + +import './IUniswapV2Router01.sol'; + +interface IUniswapV2Router02 is IUniswapV2Router01 { + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountETH); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external payable; + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; +} diff --git a/contracts/vendor/uniswap-v2-periphery/interfaces/IWETH.sol b/contracts/vendor/uniswap-v2-periphery/interfaces/IWETH.sol new file mode 100644 index 0000000..e05fb77 --- /dev/null +++ b/contracts/vendor/uniswap-v2-periphery/interfaces/IWETH.sol @@ -0,0 +1,7 @@ +pragma solidity >=0.5.0; + +interface IWETH { + function deposit() external payable; + function transfer(address to, uint value) external returns (bool); + function withdraw(uint) external; +} diff --git a/contracts/vendor/uniswap-v2-periphery/libraries/SafeMath.sol b/contracts/vendor/uniswap-v2-periphery/libraries/SafeMath.sol new file mode 100644 index 0000000..ba6fc21 --- /dev/null +++ b/contracts/vendor/uniswap-v2-periphery/libraries/SafeMath.sol @@ -0,0 +1,17 @@ +pragma solidity =0.6.6; + +// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math) + +library SafeMath { + function add(uint x, uint y) internal pure returns (uint z) { + require((z = x + y) >= x, 'ds-math-add-overflow'); + } + + function sub(uint x, uint y) internal pure returns (uint z) { + require((z = x - y) <= x, 'ds-math-sub-underflow'); + } + + function mul(uint x, uint y) internal pure returns (uint z) { + require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow'); + } +} diff --git a/contracts/vendor/uniswap-v2-periphery/libraries/UniswapV2Library.sol b/contracts/vendor/uniswap-v2-periphery/libraries/UniswapV2Library.sol new file mode 100644 index 0000000..e5fb8c7 --- /dev/null +++ b/contracts/vendor/uniswap-v2-periphery/libraries/UniswapV2Library.sol @@ -0,0 +1,82 @@ +pragma solidity >=0.5.0; + +import '../../uniswap-v2-core/interfaces/IUniswapV2Pair.sol'; + +import "./SafeMath.sol"; + +library UniswapV2Library { + using SafeMath for uint; + + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); + } + + // calculates the CREATE2 address for a pair without making any external calls + function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address(uint(keccak256(abi.encodePacked( + hex'ff', + factory, + keccak256(abi.encodePacked(token0, token1)), + hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash + )))); + } + + // fetches and sorts the reserves for a pair + function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { + (address token0,) = sortTokens(tokenA, tokenB); + (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } + + // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset + function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { + require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); + require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + amountB = amountA.mul(reserveB) / reserveA; + } + + // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { + require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint amountInWithFee = amountIn.mul(997); + uint numerator = amountInWithFee.mul(reserveOut); + uint denominator = reserveIn.mul(1000).add(amountInWithFee); + amountOut = numerator / denominator; + } + + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { + require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); + uint numerator = reserveIn.mul(amountOut).mul(1000); + uint denominator = reserveOut.sub(amountOut).mul(997); + amountIn = (numerator / denominator).add(1); + } + + // performs chained getAmountOut calculations on any number of pairs + function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[0] = amountIn; + for (uint i; i < path.length - 1; i++) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); + amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); + } + } + + // performs chained getAmountIn calculations on any number of pairs + function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { + require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); + amounts = new uint[](path.length); + amounts[amounts.length - 1] = amountOut; + for (uint i = path.length - 1; i > 0; i--) { + (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); + amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); + } + } +} diff --git a/contracts/wrapped-lp-public/Chain138LPLocker.sol b/contracts/wrapped-lp-public/Chain138LPLocker.sol new file mode 100644 index 0000000..3e95d02 --- /dev/null +++ b/contracts/wrapped-lp-public/Chain138LPLocker.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IWLPProgramEvents.sol"; + +/** + * @title Chain138LPLocker + * @notice Escrows DODO PMM LP ERC20 on Chain 138; releases only to BRIDGE_RELEASE_ROLE (Option A). + * @dev Per-lock `lockRef` is used as the cross-chain idempotency key for destination mint. + * One deployment typically corresponds to one `lpToken` (one pool’s LP token). + */ +contract Chain138LPLocker is AccessControl, IWLPProgramEvents { + using SafeERC20 for IERC20; + + bytes32 public constant BRIDGE_RELEASE_ROLE = keccak256("BRIDGE_RELEASE_ROLE"); + + IERC20 public immutable lpToken; + + struct Deposit { + address depositor; + uint256 amount; + bool released; + } + + uint256 public lockCounter; + uint256 public totalEscrowed; + + mapping(bytes32 => Deposit) public deposits; + + constructor(address lpToken_, address admin) { + require(lpToken_ != address(0) && admin != address(0), "Locker: zero"); + lpToken = IERC20(lpToken_); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @notice Lock LP into escrow; `lockRef` must be relayed to the public chain for mint. + */ + function deposit(uint256 amount) external returns (bytes32 lockRef) { + require(amount > 0, "Locker: zero amount"); + lpToken.safeTransferFrom(msg.sender, address(this), amount); + lockRef = keccak256(abi.encode(block.chainid, address(this), lockCounter++, msg.sender, amount)); + deposits[lockRef] = Deposit({depositor: msg.sender, amount: amount, released: false}); + totalEscrowed += amount; + emit LPLocked(lockRef, msg.sender, amount, address(lpToken)); + } + + /** + * @notice Release one full lock to `to` after redemption message (or operational unwind). + */ + function releaseLock(bytes32 lockRef, address to) external onlyRole(BRIDGE_RELEASE_ROLE) { + Deposit storage d = deposits[lockRef]; + require(d.amount > 0 && !d.released, "Locker: bad lock"); + d.released = true; + uint256 amt = d.amount; + totalEscrowed -= amt; + lpToken.safeTransfer(to, amt); + emit LPReleased(lockRef, to, amt, address(lpToken)); + } + + /** + * @notice Aggregate release for pro-rata / FIFO policies (requires off-chain ordering). + */ + function releaseAmount(address to, uint256 amount) external onlyRole(BRIDGE_RELEASE_ROLE) { + require(amount > 0 && amount <= totalEscrowed, "Locker: amount"); + totalEscrowed -= amount; + lpToken.safeTransfer(to, amount); + emit LPReleased(bytes32(0), to, amount, address(lpToken)); + } +} diff --git a/contracts/wrapped-lp-public/PublicChainMintController.sol b/contracts/wrapped-lp-public/PublicChainMintController.sol new file mode 100644 index 0000000..9787a52 --- /dev/null +++ b/contracts/wrapped-lp-public/PublicChainMintController.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./WLPReceiptToken.sol"; +import "./interfaces/IWLPProgramEvents.sol"; + +/** + * @title PublicChainMintController + * @notice Destination-chain controller: mints `wLP` once per `lockRef` (replay protection). + * @dev Relayer must verify Chain 138 `LPLocked` event / attestation off-chain or via future ZK proof. + */ +contract PublicChainMintController is AccessControl, IWLPProgramEvents { + bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE"); + + WLPReceiptToken public immutable wlp; + /// @notice Optional Chain 138 locker address for documentation / future cross-verify hooks. + address public immutable chain138Locker; + + mapping(bytes32 => bool) public mintedForLock; + bool public mintPaused; + + constructor(address wlp_, address chain138Locker_, address admin) { + require(wlp_ != address(0) && admin != address(0), "MintCtl: zero"); + wlp = WLPReceiptToken(wlp_); + chain138Locker = chain138Locker_; + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @notice Idempotent mint keyed by `lockRef` (must match Chain 138 locker emission). + */ + function mintForLock(bytes32 lockRef, address recipient, uint256 amount) external onlyRole(RELAYER_ROLE) { + require(!mintPaused, "MintCtl: paused"); + require(lockRef != bytes32(0), "MintCtl: zero lockRef"); + require(recipient != address(0) && amount > 0, "MintCtl: bad args"); + require(!mintedForLock[lockRef], "MintCtl: replay"); + mintedForLock[lockRef] = true; + wlp.mint(recipient, amount); + emit WLPMinted(lockRef, recipient, amount); + } + + function setMintPaused(bool paused) external onlyRole(DEFAULT_ADMIN_ROLE) { + mintPaused = paused; + } +} diff --git a/contracts/wrapped-lp-public/README.md b/contracts/wrapped-lp-public/README.md new file mode 100644 index 0000000..b8e7e2a --- /dev/null +++ b/contracts/wrapped-lp-public/README.md @@ -0,0 +1,92 @@ +# Wrapped LP / Public Chain Program (Scaffold) + +Solidity for **Option A** (lock → mint `wLP` → redeem) and **Option B** (ERC-4626 vault + NAV oracle). This is a **new vertical**: **not** an extension of `CWMultiTokenBridgeL1/L2` or catalog **cW*** CCIP paths. + +--- + +## Documentation (exhaustive index) + +| Doc | Description | +|-----|-------------| +| [WRAPPED_LP_PROGRAM_REFERENCE.md](../../../docs/04-configuration/WRAPPED_LP_PROGRAM_REFERENCE.md) | **Master index**, invariants, glossary, phase gates | +| [ADR_WRAPPED_LP_VAULT_PUBLIC_CHAIN.md](../../../docs/03-deployment/ADR_WRAPPED_LP_VAULT_PUBLIC_CHAIN.md) | Architecture decisions, alternatives | +| [WRAPPED_LP_MESSAGE_SCHEMA.md](../../../docs/04-configuration/WRAPPED_LP_MESSAGE_SCHEMA.md) | `lockRef`, payloads, CCIP, redemption | +| [WRAPPED_LP_ORACLE_STACK.md](../../../docs/04-configuration/WRAPPED_LP_ORACLE_STACK.md) | Two-price model, keepers, lending | +| [WRAPPED_LP_LENDING_GOVERNANCE.md](../../../docs/04-configuration/WRAPPED_LP_LENDING_GOVERNANCE.md) | Venues, listing, risk matrix | +| [WRAPPED_LP_OPERATIONS_RUNBOOK.md](../../../docs/03-deployment/WRAPPED_LP_OPERATIONS_RUNBOOK.md) | SLOs, RACI, incidents | +| [WRAPPED_LP_AUDIT_CHECKLIST.md](../../../docs/03-deployment/WRAPPED_LP_AUDIT_CHECKLIST.md) | Auditor scope, threats | + +--- + +## Contracts + +| File | Role | +|------|------| +| `Chain138LPLocker.sol` | Escrow LP on 138; `lockRef`; `releaseLock` / `releaseAmount` | +| `WLPReceiptToken.sol` | Public-chain `wLP` (MINTER/BURNER) | +| `PublicChainMintController.sol` | Idempotent `mintForLock`; `mintPaused` | +| `WLPRedemptionGateway.sol` | Burn + `RedemptionRequested` | +| `WrappedLPNAVVault.sol` | ERC-4626 + `depositCap` | +| `WLPNAVOracle.sol` | USD-style feed, heartbeat, breaker, `isStale()` | +| `interfaces/IWLPProgramEvents.sol` | Shared events | + +--- + +## Build / test (scoped Forge) + +Full-repo `forge test` may fail on legacy **0.5.x/0.6.x** vendor trees; always use **scope**: + +```bash +cd smom-dbis-138 +bash scripts/forge/scope.sh build wrapped-lp-public +bash scripts/forge/scope.sh test wrapped-lp-public --match-path 'test/wrapped-lp-public/*.t.sol' +``` + +--- + +## Deployment + +| Script | Use | +|--------|-----| +| [DeployWrappedLPLockerChain138.s.sol](../../script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol) | **Chain 138 only** — `Chain138LPLocker` | +| [DeployWrappedLPPublicChain.s.sol](../../script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol) | **Public chain** — wLP, mint controller, gateway, oracle, optional vault | +| [DeployWrappedLPProgram.s.sol](../../script/wrapped-lp-public/DeployWrappedLPProgram.s.sol) | **Single-chain / Anvil** — full stack on one RPC (not production two-chain) | + +Production: two `forge script` runs (138 RPC then destination RPC). See [WRAPPED_LP_DEPLOYMENT_RUNBOOK.md](../../../docs/03-deployment/WRAPPED_LP_DEPLOYMENT_RUNBOOK.md). + +### Post-deploy role wiring (required) + +| Grant | To | +|-------|-----| +| `WLPReceiptToken.MINTER_ROLE` | `PublicChainMintController` | +| `WLPReceiptToken.BURNER_ROLE` | `WLPRedemptionGateway` | +| `PublicChainMintController.RELAYER_ROLE` | Relayer worker(s) | +| `Chain138LPLocker.BRIDGE_RELEASE_ROLE` | Relayer (138 release path) | +| `WLPNAVOracle.KEEPER_ROLE` | Price keeper | + +Use **multisig** for `DEFAULT_ADMIN_ROLE` on production. + +### Environment variables (deploy script) + +| Variable | Purpose | +|----------|---------| +| `LP_TOKEN` | DODO LP ERC20 on 138 | +| `ADMIN` | Multisig / admin | +| `PRIVATE_KEY` | Broadcast key (lab only) | +| `USDC` | Optional; if set, deploys `WrappedLPNAVVault` | + +--- + +## Limitations (read before mainnet) + +1. **Relayer trust:** Mint path assumes **honest** relayer until ZK/attestation hardening. +2. **Fungible wLP:** Secondary trading breaks **simple** per-lock redemption; see message schema doc. +3. **Vault:** `totalAssets()` follows **ERC-20 balance**; off-chain strategy on 138 is **operational**. + +--- + +## Repo context + +- PMM pools: `docs/11-references/DEPLOYED_TOKENS_BRIDGES_LPS_AND_ROUTING_STATUS.md` §4.1 +- cW bridge **pattern only**: `docs/07-ccip/CW_BRIDGE_APPROACH.md` +- GRU policy **orthogonality**: `docs/04-configuration/GRU_REFERENCE_PRIMACY_AND_MESH_EXECUTION_MODEL.md` diff --git a/contracts/wrapped-lp-public/WLPNAVOracle.sol b/contracts/wrapped-lp-public/WLPNAVOracle.sol new file mode 100644 index 0000000..9020014 --- /dev/null +++ b/contracts/wrapped-lp-public/WLPNAVOracle.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * @title WLPNAVOracle + * @notice Chainlink-style read interface for wLP / vault share price in USD (8 decimals) for lending adapters. + * @dev Keeper updates `latestAnswer` + `updatedAt`. Consumers must check `isStale()` before liquidations. + */ +contract WLPNAVOracle is AccessControl { + bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE"); + + /// @notice Price with 8 decimals (Chainlink USD convention). + int256 public latestAnswer; + uint256 public updatedAt; + uint256 public heartbeat; + bool public circuitBreaker; + + event AnswerUpdated(int256 indexed answer, uint256 updatedAt, address indexed keeper); + event HeartbeatUpdated(uint256 heartbeat); + event CircuitBreakerSet(bool tripped); + + constructor(address admin, uint256 heartbeat_) { + require(admin != address(0), "NAV: zero admin"); + heartbeat = heartbeat_; + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + function setHeartbeat(uint256 h) external onlyRole(DEFAULT_ADMIN_ROLE) { + heartbeat = h; + emit HeartbeatUpdated(h); + } + + function setCircuitBreaker(bool on) external onlyRole(DEFAULT_ADMIN_ROLE) { + circuitBreaker = on; + emit CircuitBreakerSet(on); + } + + function submitAnswer(int256 answer) external onlyRole(KEEPER_ROLE) { + require(!circuitBreaker, "NAV: breaker"); + latestAnswer = answer; + updatedAt = block.timestamp; + emit AnswerUpdated(answer, updatedAt, msg.sender); + } + + function isStale() external view returns (bool) { + if (circuitBreaker) return true; + if (heartbeat == 0) return false; + return block.timestamp > updatedAt + heartbeat; + } +} diff --git a/contracts/wrapped-lp-public/WLPReceiptToken.sol b/contracts/wrapped-lp-public/WLPReceiptToken.sol new file mode 100644 index 0000000..00a8ab9 --- /dev/null +++ b/contracts/wrapped-lp-public/WLPReceiptToken.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * @title WLPReceiptToken + * @notice Fungible receipt token for Chain 138 LP exposure minted on a public chain (Option A). + * @dev Only addresses with MINTER_ROLE may mint; BURNER_ROLE for redemption gateway / bridge burn paths. + * Not a cW* token; separate from CompliantWrappedToken governance surface. + */ +contract WLPReceiptToken is ERC20, AccessControl { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + + uint8 private immutable _decimalsOverride; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + address admin + ) ERC20(name_, symbol_) { + require(admin != address(0), "WLP: zero admin"); + _decimalsOverride = decimals_; + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + function decimals() public view override returns (uint8) { + return _decimalsOverride; + } + + function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external onlyRole(BURNER_ROLE) { + _burn(from, amount); + } +} diff --git a/contracts/wrapped-lp-public/WLPRedemptionGateway.sol b/contracts/wrapped-lp-public/WLPRedemptionGateway.sol new file mode 100644 index 0000000..dfb2780 --- /dev/null +++ b/contracts/wrapped-lp-public/WLPRedemptionGateway.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./WLPReceiptToken.sol"; +import "./interfaces/IWLPProgramEvents.sol"; + +/** + * @title WLPRedemptionGateway + * @notice Burns wLP from user and emits `RedemptionRequested` for relayers to release LP on Chain 138. + * @dev Relayer observes event, submits `Chain138LPLocker.release*` on 138. Fungible wLP + secondary trading + * may break 1:1 depositor-only redemption; production may require NFT receipts or curated policy. + */ +contract WLPRedemptionGateway is AccessControl, IWLPProgramEvents { + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + + WLPReceiptToken public immutable wlp; + + constructor(address wlp_, address admin) { + require(wlp_ != address(0) && admin != address(0), "Gateway: zero"); + wlp = WLPReceiptToken(wlp_); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @notice User burns wLP; relayer fulfills LP release on 138 per operational policy. + */ + function requestRedeem(uint256 wlpAmount) external { + require(wlpAmount > 0, "Gateway: zero"); + wlp.burn(msg.sender, wlpAmount); + emit RedemptionRequested(msg.sender, wlpAmount); + } +} diff --git a/contracts/wrapped-lp-public/WrappedLPNAVVault.sol b/contracts/wrapped-lp-public/WrappedLPNAVVault.sol new file mode 100644 index 0000000..615b68f --- /dev/null +++ b/contracts/wrapped-lp-public/WrappedLPNAVVault.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * @title WrappedLPNAVVault + * @notice Option B scaffold: standard ERC-4626 vault over an underlying asset (e.g. USDC on mainnet). + * @dev Strategy deployment of vault assets into Chain 138 LP is **operational** (outside this contract). + * Use `depositCap` for soft-launch; seed initial deposit to mitigate donation attacks per OZ ERC4626 docs. + */ +contract WrappedLPNAVVault is ERC4626, AccessControl { + bytes32 public constant CAP_ADMIN_ROLE = keccak256("CAP_ADMIN_ROLE"); + + uint256 public depositCap; + + constructor( + IERC20 asset_, + string memory name_, + string memory symbol_, + address admin + ) ERC20(name_, symbol_) ERC4626(asset_) { + require(admin != address(0), "Vault: zero admin"); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(CAP_ADMIN_ROLE, admin); + } + + function maxDeposit(address) public view override returns (uint256) { + uint256 cap = depositCap; + if (cap == 0) return type(uint256).max; + uint256 a = totalAssets(); + if (a >= cap) return 0; + return cap - a; + } + + function setDepositCap(uint256 cap) external onlyRole(CAP_ADMIN_ROLE) { + depositCap = cap; + } +} diff --git a/contracts/wrapped-lp-public/interfaces/IWLPProgramEvents.sol b/contracts/wrapped-lp-public/interfaces/IWLPProgramEvents.sol new file mode 100644 index 0000000..39fce71 --- /dev/null +++ b/contracts/wrapped-lp-public/interfaces/IWLPProgramEvents.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title IWLPProgramEvents + * @notice Canonical events for wrapped-LP / bridge attestation flows (Option A). + * @dev `lockRef` is the idempotency key for destination mint (replay protection). + */ +interface IWLPProgramEvents { + event LPLocked(bytes32 indexed lockRef, address indexed depositor, uint256 amount, address indexed lpToken); + event LPReleased(bytes32 indexed lockRef, address indexed to, uint256 amount, address indexed lpToken); + event WLPMinted(bytes32 indexed lockRef, address indexed recipient, uint256 amount); + event RedemptionRequested(address indexed holder, uint256 wlpAmount); +} diff --git a/deployments/chain138/sushiswap-native.json b/deployments/chain138/sushiswap-native.json new file mode 100644 index 0000000..e138353 --- /dev/null +++ b/deployments/chain138/sushiswap-native.json @@ -0,0 +1,18 @@ +{ + "chainId": 138, + "deployer": "0x4A666F96fC8764181194447A7dFdb7d471b301C8", + "feeToSetter": "0x4A666F96fC8764181194447A7dFdb7d471b301C8", + "deployedAtBlock": 4041495, + "factory": "0x2871207ff0d56089D70c0134d33f1291B6Fce0BE", + "router": "0xB37b93D38559f53b62ab020A14919f2630a1aE34", + "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "usdt": "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", + "usdc": "0x71D6687F38b93CCad569Fa6352c876eea967201b", + "cusdt": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", + "cusdc": "0xf22258f57794CC8E06237084b353Ab30fFfa640b", + "pairs": { + "wethUsdt": "0x8d848941ee8Daca6fc22A9B8a11Ed706Ad60A80D", + "wethUsdc": "0xaf000a8ea892502B4d3eB7BD0127268468339DE7", + "cusdtCusdc": "0x43D93d520079DCB6Fe97Abf6e7607129893b0e3E" + } +} diff --git a/deployments/chain138/uniswap-v2-native.json b/deployments/chain138/uniswap-v2-native.json new file mode 100644 index 0000000..4c08481 --- /dev/null +++ b/deployments/chain138/uniswap-v2-native.json @@ -0,0 +1,17 @@ +{ + "chainId": 138, + "deployer": "0x4A666F96fC8764181194447A7dFdb7d471b301C8", + "deployedAtBlock": 4041469, + "factory": "0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279", + "router": "0x3019A7fDc76ba7F64F18d78e66842760037ee638", + "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "usdt": "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", + "usdc": "0x71D6687F38b93CCad569Fa6352c876eea967201b", + "cusdt": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", + "cusdc": "0xf22258f57794CC8E06237084b353Ab30fFfa640b", + "pairs": { + "wethUsdt": "0xda2A5AC9F4892EAd47093A374779Ba790f23DC84", + "wethUsdc": "0xCD255F4f6c4E861490b3C7E0300613318B870f36", + "cusdtCusdc": "0x7bd707294AACb47ab109780be8a096700741886d" + } +} diff --git a/docs/integration/ORACLE_AND_KEEPER_CHAIN138.md b/docs/integration/ORACLE_AND_KEEPER_CHAIN138.md index 4707e5f..9a671f0 100644 --- a/docs/integration/ORACLE_AND_KEEPER_CHAIN138.md +++ b/docs/integration/ORACLE_AND_KEEPER_CHAIN138.md @@ -49,6 +49,8 @@ References: [DEPLOYMENT_COMPLETE_GUIDE.md](../deployment/DEPLOYMENT_COMPLETE_GUI **Policy:** Run off-chain ticks every **6 seconds** so oracle pushes, `PriceFeedKeeper` upkeep, and PMM/WETH read paths stay warm—supporting peg alignment with `DODOPMMIntegration` / optional `ReserveSystem`. +**One-shot on-chain alignment (WETH10 mock + D3Oracle + `trackAsset`):** parent repo `scripts/deployment/fix-chain138-pricing-feeds.sh` (see `docs/04-configuration/CHAIN138_PRICING_FEEDS_LIVE.md`). + | Component | Role | |-----------|------| | `scripts/reserve/pmm-mesh-6s-automation.sh` | Loop: `getPoolPriceOrOracle` on `PMM_MESH_POLL_POOLS`, WETH9/WETH10 `totalSupply` reads, conditional `performUpkeep`, `update-oracle-price.sh`. | diff --git a/hardhat.config.js b/hardhat.config.js index 699406d..5d1c250 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -28,6 +28,9 @@ const sharedAccounts = resolveAccounts(); module.exports = { solidity: { compilers: [ + { version: "0.5.16", settings: { optimizer: { enabled: true, runs: 200 } } }, + { version: "0.6.6", settings: { optimizer: { enabled: true, runs: 200 } } }, + { version: "0.6.12", settings: { optimizer: { enabled: true, runs: 200 } } }, { version: "0.8.20", settings: { optimizer: { enabled: true, runs: 200 }, viaIR: true } }, { version: "0.8.22", settings: { optimizer: { enabled: true, runs: 200 }, viaIR: true } }, ], diff --git a/lib/dodo-contractV2 b/lib/dodo-contractV2 index d946606..2e0a364 160000 --- a/lib/dodo-contractV2 +++ b/lib/dodo-contractV2 @@ -1 +1 @@ -Subproject commit d946606870b64110218820da44becf2b3e196c8a +Subproject commit 2e0a36474323b5803d71e6de8e9336b2d350a10e diff --git a/script/deploy/chains/DeployAllAdapters.s.sol b/script/deploy/chains/DeployAllAdapters.s.sol index 345dde4..843c865 100644 --- a/script/deploy/chains/DeployAllAdapters.s.sol +++ b/script/deploy/chains/DeployAllAdapters.s.sol @@ -8,7 +8,9 @@ import "../../../contracts/bridge/adapters/evm/XDCAdapter.sol"; import "../../../contracts/bridge/adapters/evm/AlltraAdapter.sol"; import "../../../contracts/bridge/adapters/non-evm/XRPLAdapter.sol"; import "../../../contracts/bridge/adapters/non-evm/StellarAdapter.sol"; +import "../../../contracts/bridge/adapters/non-evm/SolanaAdapter.sol"; import "../../../contracts/bridge/adapters/non-evm/TezosAdapter.sol"; +import "../../../contracts/bridge/adapters/non-evm/TronAdapter.sol"; import "../../../contracts/bridge/adapters/hyperledger/FireflyAdapter.sol"; import "../../../contracts/bridge/adapters/hyperledger/CactiAdapter.sol"; import "../../../contracts/bridge/adapters/hyperledger/FabricAdapter.sol"; @@ -39,7 +41,9 @@ contract DeployAllAdapters is Script { // Deploy non-EVM adapters XRPLAdapter xrplAdapter = new XRPLAdapter(deployer); StellarAdapter stellarAdapter = new StellarAdapter(deployer); + SolanaAdapter solanaAdapter = new SolanaAdapter(deployer); TezosAdapter tezosAdapter = new TezosAdapter(deployer); + TronAdapter tronAdapter = new TronAdapter(deployer); // Deploy Hyperledger adapters FireflyAdapter fireflyAdapter = new FireflyAdapter(deployer, "alltra-bridge"); @@ -59,7 +63,9 @@ contract DeployAllAdapters is Script { registry.registerNonEVMChain("XRPL-Mainnet", ChainRegistry.ChainType.XRPL, address(xrplAdapter), "https://xrpscan.com", 1, 4, true, ""); registry.registerNonEVMChain("Stellar-Mainnet", ChainRegistry.ChainType.Stellar, address(stellarAdapter), "https://stellarchain.io", 1, 5, true, ""); + registry.registerNonEVMChain("Solana-Mainnet", ChainRegistry.ChainType.Solana, address(solanaAdapter), "https://solscan.io", 32, 1, true, ""); registry.registerNonEVMChain("Tezos-Mainnet", ChainRegistry.ChainType.Other, address(tezosAdapter), "https://tzkt.io", 1, 30, true, ""); + registry.registerNonEVMChain("Tron-Mainnet", ChainRegistry.ChainType.Tron, address(tronAdapter), "https://tronscan.org", 20, 3, true, ""); registry.registerNonEVMChain("Firefly-Orchestration", ChainRegistry.ChainType.Firefly, address(fireflyAdapter), "", 1, 1, true, ""); registry.registerNonEVMChain("Cacti-Interoperability", ChainRegistry.ChainType.Cacti, address(cactiAdapter), "", 1, 1, true, ""); registry.registerNonEVMChain("Fabric-bridge-channel", ChainRegistry.ChainType.Fabric, address(fabricAdapter), "", 1, 1, true, ""); diff --git a/script/deploy/chains/DeploySolanaAdapter.s.sol b/script/deploy/chains/DeploySolanaAdapter.s.sol new file mode 100644 index 0000000..6119d84 --- /dev/null +++ b/script/deploy/chains/DeploySolanaAdapter.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Script.sol"; +import "../../../contracts/registry/ChainRegistry.sol"; +import "../../../contracts/bridge/adapters/non-evm/SolanaAdapter.sol"; + +contract DeploySolanaAdapter is Script { + function run() external { + uint256 pk = vm.envUint("PRIVATE_KEY"); + address admin = vm.addr(pk); + address chainRegistry = vm.envAddress("CHAIN_REGISTRY_ADDRESS"); + + ChainRegistry registry = ChainRegistry(chainRegistry); + + vm.startBroadcast(pk); + + SolanaAdapter solanaAdapter = new SolanaAdapter(admin); + ChainRegistry.ChainMetadata memory existing = registry.getNonEVMChain("Solana-Mainnet"); + + if (existing.adapter == address(0)) { + registry.registerNonEVMChain( + "Solana-Mainnet", + ChainRegistry.ChainType.Solana, + address(solanaAdapter), + "https://solscan.io", + 32, + 1, + true, + "" + ); + } else { + registry.updateAdapter(0, "Solana-Mainnet", address(solanaAdapter)); + registry.setChainActive(0, "Solana-Mainnet", true); + } + + vm.stopBroadcast(); + + console.log("SolanaAdapter:", address(solanaAdapter)); + console.log("ChainRegistry:", chainRegistry); + } +} diff --git a/script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol b/script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol new file mode 100644 index 0000000..7b42db1 --- /dev/null +++ b/script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console2} from "forge-std/Script.sol"; + +import {Chain138LPLocker} from "../../contracts/wrapped-lp-public/Chain138LPLocker.sol"; + +/** + * @title DeployWrappedLPLockerChain138 + * @notice Deploy **only** `Chain138LPLocker` on Chain 138. Use `--rpc-url` pointing at Core RPC. + * @dev Env: `LP_TOKEN`, `ADMIN`, `PRIVATE_KEY`. Verify on Blockscout after deploy. + */ +contract DeployWrappedLPLockerChain138 is Script { + function run() external { + address lpToken = vm.envAddress("LP_TOKEN"); + address admin = vm.envAddress("ADMIN"); + uint256 pk = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(pk); + + Chain138LPLocker locker = new Chain138LPLocker(lpToken, admin); + console2.log("Chain138LPLocker", address(locker)); + + vm.stopBroadcast(); + } +} diff --git a/script/wrapped-lp-public/DeployWrappedLPProgram.s.sol b/script/wrapped-lp-public/DeployWrappedLPProgram.s.sol new file mode 100644 index 0000000..7b3b474 --- /dev/null +++ b/script/wrapped-lp-public/DeployWrappedLPProgram.s.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console2} from "forge-std/Script.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {Chain138LPLocker} from "../../contracts/wrapped-lp-public/Chain138LPLocker.sol"; +import {WLPReceiptToken} from "../../contracts/wrapped-lp-public/WLPReceiptToken.sol"; +import {PublicChainMintController} from "../../contracts/wrapped-lp-public/PublicChainMintController.sol"; +import {WLPRedemptionGateway} from "../../contracts/wrapped-lp-public/WLPRedemptionGateway.sol"; +import {WLPNAVOracle} from "../../contracts/wrapped-lp-public/WLPNAVOracle.sol"; +import {WrappedLPNAVVault} from "../../contracts/wrapped-lp-public/WrappedLPNAVVault.sol"; + +/** + * @title DeployWrappedLPProgram + * @notice **Single-chain / local harness:** deploys locker + public stack in one `run()` (only valid when + * both sides target the **same** RPC, e.g. Anvil or fork). For production use: + * - [DeployWrappedLPLockerChain138.s.sol](DeployWrappedLPLockerChain138.s.sol) with Chain 138 RPC + * - [DeployWrappedLPPublicChain.s.sol](DeployWrappedLPPublicChain.s.sol) with destination RPC + * @dev Env: `LP_TOKEN`, `ADMIN`, `USDC` (optional vault), `PRIVATE_KEY`. Optional `WIRE_ROLES=1` and + * `RELAYER_ADDRESS` when `vm.addr(PRIVATE_KEY) == ADMIN`. + */ +contract DeployWrappedLPProgram is Script { + function run() external { + address lpToken = vm.envAddress("LP_TOKEN"); + address admin = vm.envAddress("ADMIN"); + uint256 pk = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(pk); + + Chain138LPLocker locker = new Chain138LPLocker(lpToken, admin); + console2.log("Chain138LPLocker", address(locker)); + + WLPReceiptToken wlp = new WLPReceiptToken("Wrapped LP", "wLP", 18, admin); + console2.log("WLPReceiptToken", address(wlp)); + + PublicChainMintController mintCtl = new PublicChainMintController(address(wlp), address(locker), admin); + console2.log("PublicChainMintController", address(mintCtl)); + + WLPRedemptionGateway gateway = new WLPRedemptionGateway(address(wlp), admin); + console2.log("WLPRedemptionGateway", address(gateway)); + + WLPNAVOracle oracle = new WLPNAVOracle(admin, 3600); + console2.log("WLPNAVOracle", address(oracle)); + + address usdc = vm.envOr("USDC", address(0)); + if (usdc != address(0)) { + WrappedLPNAVVault vault = new WrappedLPNAVVault( + IERC20(usdc), + "Wrapped LP NAV Vault", + "wLPV", + admin + ); + console2.log("WrappedLPNAVVault", address(vault)); + } + + if (vm.envOr("WIRE_ROLES", false)) { + address deployer = vm.addr(pk); + require(deployer == admin, "WIRE_ROLES requires deployer==ADMIN"); + address relayerAddr = vm.envAddress("RELAYER_ADDRESS"); + address keeperAddr = vm.envOr("KEEPER_ADDRESS", relayerAddr); + wlp.grantRole(wlp.MINTER_ROLE(), address(mintCtl)); + wlp.grantRole(wlp.BURNER_ROLE(), address(gateway)); + mintCtl.grantRole(mintCtl.RELAYER_ROLE(), relayerAddr); + locker.grantRole(locker.BRIDGE_RELEASE_ROLE(), relayerAddr); + oracle.grantRole(oracle.KEEPER_ROLE(), keeperAddr); + console2.log("Wired roles (minter/burner/relayer/release/keeper)"); + } + + vm.stopBroadcast(); + } +} diff --git a/script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol b/script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol new file mode 100644 index 0000000..1a2ef04 --- /dev/null +++ b/script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console2} from "forge-std/Script.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {WLPReceiptToken} from "../../contracts/wrapped-lp-public/WLPReceiptToken.sol"; +import {PublicChainMintController} from "../../contracts/wrapped-lp-public/PublicChainMintController.sol"; +import {WLPRedemptionGateway} from "../../contracts/wrapped-lp-public/WLPRedemptionGateway.sol"; +import {WLPNAVOracle} from "../../contracts/wrapped-lp-public/WLPNAVOracle.sol"; +import {WrappedLPNAVVault} from "../../contracts/wrapped-lp-public/WrappedLPNAVVault.sol"; + +/** + * @title DeployWrappedLPPublicChain + * @notice Deploy wLP stack on **public** chain (mainnet or L2). `--rpc-url` must be destination chain. + * @dev Env: `ADMIN`, `PRIVATE_KEY`, `CHAIN138_LOCKER` (locker address for mint controller reference), + * optional `USDC` for vault, optional `WIRE_ROLES=1`, `RELAYER_ADDRESS`, `KEEPER_ADDRESS` (defaults to relayer). + */ +contract DeployWrappedLPPublicChain is Script { + function run() external { + address admin = vm.envAddress("ADMIN"); + address chain138Locker = vm.envAddress("CHAIN138_LOCKER"); + uint256 pk = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(pk); + + WLPReceiptToken wlp = new WLPReceiptToken("Wrapped LP", "wLP", 18, admin); + console2.log("WLPReceiptToken", address(wlp)); + + PublicChainMintController mintCtl = new PublicChainMintController(address(wlp), chain138Locker, admin); + console2.log("PublicChainMintController", address(mintCtl)); + + WLPRedemptionGateway gateway = new WLPRedemptionGateway(address(wlp), admin); + console2.log("WLPRedemptionGateway", address(gateway)); + + WLPNAVOracle oracle = new WLPNAVOracle(admin, 3600); + console2.log("WLPNAVOracle", address(oracle)); + + address usdc = vm.envOr("USDC", address(0)); + if (usdc != address(0)) { + WrappedLPNAVVault vault = new WrappedLPNAVVault( + IERC20(usdc), + "Wrapped LP NAV Vault", + "wLPV", + admin + ); + console2.log("WrappedLPNAVVault", address(vault)); + } + + if (vm.envOr("WIRE_ROLES", false)) { + address deployer = vm.addr(pk); + require(deployer == admin, "WIRE_ROLES requires deployer==ADMIN"); + address relayerAddr = vm.envAddress("RELAYER_ADDRESS"); + address keeperAddr = vm.envOr("KEEPER_ADDRESS", relayerAddr); + + wlp.grantRole(wlp.MINTER_ROLE(), address(mintCtl)); + wlp.grantRole(wlp.BURNER_ROLE(), address(gateway)); + mintCtl.grantRole(mintCtl.RELAYER_ROLE(), relayerAddr); + oracle.grantRole(oracle.KEEPER_ROLE(), keeperAddr); + console2.log("Wired MINTER/BURNER/RELAYER/KEEPER"); + } + + vm.stopBroadcast(); + } +} diff --git a/scripts/chain138/deploy-sushiswap-native.js b/scripts/chain138/deploy-sushiswap-native.js new file mode 100644 index 0000000..07df648 --- /dev/null +++ b/scripts/chain138/deploy-sushiswap-native.js @@ -0,0 +1,188 @@ +const fs = require("fs"); +const path = require("path"); +const hre = require("hardhat"); + +const DEFAULTS = { + weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + usdt: "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", + usdc: "0x71D6687F38b93CCad569Fa6352c876eea967201b", + cusdt: "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", + cusdc: "0xf22258f57794CC8E06237084b353Ab30fFfa640b", +}; + +function envAddress(name, fallback) { + const value = String(process.env[name] || fallback || "").trim(); + if (!value) { + throw new Error(`Missing required address env ${name}`); + } + return value; +} + +function envUnits(name, fallback, decimals) { + return hre.ethers.parseUnits(String(process.env[name] || fallback), decimals); +} + +async function ensureAllowance(token, owner, spender, amount) { + const allowance = await token.allowance(owner, spender); + if (allowance >= amount) return; + const tx = await token.approve(spender, hre.ethers.MaxUint256); + await tx.wait(); +} + +async function transferIfNeeded(token, to, amount) { + if (amount <= 0n) return; + const tx = await token.transfer(to, amount); + await tx.wait(); +} + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0]; + const feeTo = signers[5] || deployer; + const currentBlock = await hre.ethers.provider.getBlockNumber(); + const gasPrice = BigInt(process.env.CHAIN138_SUSHISWAP_GAS_PRICE || (await hre.ethers.provider.send("eth_gasPrice", []))); + const deployOverrides = { + type: 0, + gasPrice, + gasLimit: BigInt(process.env.CHAIN138_SUSHISWAP_DEPLOY_GAS_LIMIT || "9000000"), + }; + const liquidityOverrides = { + type: 0, + gasPrice, + gasLimit: BigInt(process.env.CHAIN138_SUSHISWAP_LIQUIDITY_GAS_LIMIT || "3000000"), + }; + + const weth = envAddress("CHAIN138_NATIVE_WETH9", process.env.WETH9 || DEFAULTS.weth); + const usdt = envAddress("CHAIN138_NATIVE_USDT", process.env.OFFICIAL_USDT_ADDRESS || DEFAULTS.usdt); + const usdc = envAddress("CHAIN138_NATIVE_USDC", process.env.OFFICIAL_USDC_ADDRESS || DEFAULTS.usdc); + const cusdt = envAddress("CHAIN138_COMPLIANT_USDT", process.env.COMPLIANT_USDT_ADDRESS || DEFAULTS.cusdt); + const cusdc = envAddress("CHAIN138_COMPLIANT_USDC", process.env.COMPLIANT_USDC_ADDRESS || DEFAULTS.cusdc); + + const Factory = await hre.ethers.getContractFactory( + "contracts/vendor/sushiswap-v2/UniswapV2Factory.sol:UniswapV2Factory" + ); + const Router = await hre.ethers.getContractFactory( + "contracts/vendor/sushiswap-v2/UniswapV2Router02.sol:UniswapV2Router02" + ); + const Pair = await hre.ethers.getContractFactory( + "contracts/vendor/sushiswap-v2/UniswapV2Pair.sol:UniswapV2Pair" + ); + + let factory; + let router; + + const existingFactory = process.env.CHAIN138_SUSHISWAP_EXISTING_FACTORY?.trim(); + const existingRouter = process.env.CHAIN138_SUSHISWAP_EXISTING_ROUTER?.trim(); + if (existingFactory && existingRouter) { + factory = Factory.attach(existingFactory); + router = Router.attach(existingRouter); + console.log(`[reuse] factory=${existingFactory}`); + console.log(`[reuse] router=${existingRouter}`); + } else { + console.log(`[deploy] SushiSwapFactory gasPrice=${gasPrice}`); + factory = await Factory.deploy(feeTo.address, deployOverrides); + await factory.waitForDeployment(); + console.log(`[ok] factory=${await factory.getAddress()}`); + console.log(`[deploy] SushiSwapRouter02`); + router = await Router.deploy(await factory.getAddress(), weth, deployOverrides); + await router.waitForDeployment(); + console.log(`[ok] router=${await router.getAddress()}`); + } + + const erc20Abi = [ + "function transfer(address to,uint256 amount) returns (bool)", + "function approve(address spender,uint256 amount) returns (bool)", + "function allowance(address owner,address spender) view returns (uint256)", + "function balanceOf(address account) view returns (uint256)", + ]; + const tokenByAddress = { + [weth.toLowerCase()]: new hre.ethers.Contract(weth, erc20Abi, deployer), + [usdt.toLowerCase()]: new hre.ethers.Contract(usdt, erc20Abi, deployer), + [usdc.toLowerCase()]: new hre.ethers.Contract(usdc, erc20Abi, deployer), + [cusdt.toLowerCase()]: new hre.ethers.Contract(cusdt, erc20Abi, deployer), + [cusdc.toLowerCase()]: new hre.ethers.Contract(cusdc, erc20Abi, deployer), + }; + + const seedSpecs = [ + { + key: "wethUsdt", + tokenA: weth, + tokenB: usdt, + amountA: envUnits("CHAIN138_SUSHISWAP_SEED_WETH_USDT_WETH", "25", 18), + amountB: envUnits("CHAIN138_SUSHISWAP_SEED_WETH_USDT_STABLE", "52915", 6), + }, + { + key: "wethUsdc", + tokenA: weth, + tokenB: usdc, + amountA: envUnits("CHAIN138_SUSHISWAP_SEED_WETH_USDC_WETH", "25", 18), + amountB: envUnits("CHAIN138_SUSHISWAP_SEED_WETH_USDC_STABLE", "52915", 6), + }, + { + key: "cusdtCusdc", + tokenA: cusdt, + tokenB: cusdc, + amountA: envUnits("CHAIN138_SUSHISWAP_SEED_CUSDT_CUSDC_CUSDT", "250000", 6), + amountB: envUnits("CHAIN138_SUSHISWAP_SEED_CUSDT_CUSDC_CUSDC", "250000", 6), + }, + ]; + + const deployedPairs = {}; + + for (const spec of seedSpecs) { + console.log(`[seed] ${spec.key}`); + let pairAddress = await factory.getPair(spec.tokenA, spec.tokenB); + if (pairAddress === hre.ethers.ZeroAddress) { + const tx = await factory.createPair(spec.tokenA, spec.tokenB, liquidityOverrides); + await tx.wait(); + pairAddress = await factory.getPair(spec.tokenA, spec.tokenB); + } + + const pair = Pair.attach(pairAddress); + const [reserve0, reserve1] = await pair.getReserves(); + if (reserve0 > 0n || reserve1 > 0n) { + deployedPairs[spec.key] = pairAddress; + console.log(`[skip] ${spec.key} already seeded ${pairAddress}`); + continue; + } + + const currentABalance = await tokenByAddress[spec.tokenA.toLowerCase()].balanceOf(pairAddress); + const currentBBalance = await tokenByAddress[spec.tokenB.toLowerCase()].balanceOf(pairAddress); + const topUpA = spec.amountA > currentABalance ? spec.amountA - currentABalance : 0n; + const topUpB = spec.amountB > currentBBalance ? spec.amountB - currentBBalance : 0n; + + await transferIfNeeded(tokenByAddress[spec.tokenA.toLowerCase()], pairAddress, topUpA); + await transferIfNeeded(tokenByAddress[spec.tokenB.toLowerCase()], pairAddress, topUpB); + + const mintTx = await pair.mint(deployer.address, liquidityOverrides); + await mintTx.wait(); + deployedPairs[spec.key] = pairAddress; + console.log(`[ok] ${spec.key}=${deployedPairs[spec.key]}`); + } + + const output = { + chainId: 138, + deployer: deployer.address, + feeToSetter: feeTo.address, + deployedAtBlock: currentBlock, + factory: await factory.getAddress(), + router: await router.getAddress(), + weth, + usdt, + usdc, + cusdt, + cusdc, + pairs: deployedPairs, + }; + + const outputPath = path.resolve(__dirname, "../../deployments/chain138/sushiswap-native.json"); + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, JSON.stringify(output, null, 2) + "\n"); + + console.log(JSON.stringify(output, null, 2)); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/chain138/deploy-uniswap-v2-native.js b/scripts/chain138/deploy-uniswap-v2-native.js new file mode 100644 index 0000000..6db621f --- /dev/null +++ b/scripts/chain138/deploy-uniswap-v2-native.js @@ -0,0 +1,186 @@ +const fs = require("fs"); +const path = require("path"); +const hre = require("hardhat"); + +const DEFAULTS = { + weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + usdt: "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", + usdc: "0x71D6687F38b93CCad569Fa6352c876eea967201b", + cusdt: "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", + cusdc: "0xf22258f57794CC8E06237084b353Ab30fFfa640b", +}; + +function envAddress(name, fallback) { + const value = String(process.env[name] || fallback || "").trim(); + if (!value) { + throw new Error(`Missing required address env ${name}`); + } + return value; +} + +function envUnits(name, fallback, decimals) { + return hre.ethers.parseUnits(String(process.env[name] || fallback), decimals); +} + +async function ensureAllowance(token, owner, spender, amount) { + const allowance = await token.allowance(owner, spender); + if (allowance >= amount) return; + const tx = await token.approve(spender, hre.ethers.MaxUint256); + await tx.wait(); +} + +async function transferIfNeeded(token, to, amount) { + if (amount <= 0n) return; + const tx = await token.transfer(to, amount); + await tx.wait(); +} + +async function main() { + const [deployer] = await hre.ethers.getSigners(); + const currentBlock = await hre.ethers.provider.getBlockNumber(); + const gasPrice = BigInt(process.env.CHAIN138_NATIVE_V2_GAS_PRICE || (await hre.ethers.provider.send("eth_gasPrice", []))); + const deployOverrides = { + type: 0, + gasPrice, + gasLimit: BigInt(process.env.CHAIN138_NATIVE_V2_DEPLOY_GAS_LIMIT || "9000000"), + }; + const liquidityOverrides = { + type: 0, + gasPrice, + gasLimit: BigInt(process.env.CHAIN138_NATIVE_V2_LIQUIDITY_GAS_LIMIT || "3000000"), + }; + + const weth = envAddress("CHAIN138_NATIVE_WETH9", process.env.WETH9 || DEFAULTS.weth); + const usdt = envAddress("CHAIN138_NATIVE_USDT", process.env.OFFICIAL_USDT_ADDRESS || DEFAULTS.usdt); + const usdc = envAddress("CHAIN138_NATIVE_USDC", process.env.OFFICIAL_USDC_ADDRESS || DEFAULTS.usdc); + const cusdt = envAddress("CHAIN138_COMPLIANT_USDT", process.env.COMPLIANT_USDT_ADDRESS || DEFAULTS.cusdt); + const cusdc = envAddress("CHAIN138_COMPLIANT_USDC", process.env.COMPLIANT_USDC_ADDRESS || DEFAULTS.cusdc); + + const Factory = await hre.ethers.getContractFactory( + "contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol:UniswapV2Factory" + ); + const Router = await hre.ethers.getContractFactory( + "contracts/vendor/uniswap-v2-periphery/UniswapV2Router02.sol:UniswapV2Router02" + ); + const Pair = await hre.ethers.getContractFactory( + "contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol:UniswapV2Pair" + ); + + let factory; + let router; + + const existingFactory = process.env.CHAIN138_UNISWAP_V2_EXISTING_FACTORY?.trim(); + const existingRouter = process.env.CHAIN138_UNISWAP_V2_EXISTING_ROUTER?.trim(); + if (existingFactory && existingRouter) { + factory = Factory.attach(existingFactory); + router = Router.attach(existingRouter); + console.log(`[reuse] factory=${existingFactory}`); + console.log(`[reuse] router=${existingRouter}`); + } else { + console.log(`[deploy] UniswapV2Factory gasPrice=${gasPrice}`); + factory = await Factory.deploy(deployer.address, deployOverrides); + await factory.waitForDeployment(); + console.log(`[ok] factory=${await factory.getAddress()}`); + console.log(`[deploy] UniswapV2Router02`); + router = await Router.deploy(await factory.getAddress(), weth, deployOverrides); + await router.waitForDeployment(); + console.log(`[ok] router=${await router.getAddress()}`); + } + + const erc20Abi = [ + "function transfer(address to,uint256 amount) returns (bool)", + "function approve(address spender,uint256 amount) returns (bool)", + "function allowance(address owner,address spender) view returns (uint256)", + "function balanceOf(address account) view returns (uint256)", + ]; + const tokenByAddress = { + [weth.toLowerCase()]: new hre.ethers.Contract(weth, erc20Abi, deployer), + [usdt.toLowerCase()]: new hre.ethers.Contract(usdt, erc20Abi, deployer), + [usdc.toLowerCase()]: new hre.ethers.Contract(usdc, erc20Abi, deployer), + [cusdt.toLowerCase()]: new hre.ethers.Contract(cusdt, erc20Abi, deployer), + [cusdc.toLowerCase()]: new hre.ethers.Contract(cusdc, erc20Abi, deployer), + }; + + const seedSpecs = [ + { + key: "wethUsdt", + tokenA: weth, + tokenB: usdt, + amountA: envUnits("CHAIN138_UNISWAP_V2_SEED_WETH_USDT_WETH", "25", 18), + amountB: envUnits("CHAIN138_UNISWAP_V2_SEED_WETH_USDT_STABLE", "52915", 6), + }, + { + key: "wethUsdc", + tokenA: weth, + tokenB: usdc, + amountA: envUnits("CHAIN138_UNISWAP_V2_SEED_WETH_USDC_WETH", "25", 18), + amountB: envUnits("CHAIN138_UNISWAP_V2_SEED_WETH_USDC_STABLE", "52915", 6), + }, + { + key: "cusdtCusdc", + tokenA: cusdt, + tokenB: cusdc, + amountA: envUnits("CHAIN138_UNISWAP_V2_SEED_CUSDT_CUSDC_CUSDT", "250000", 6), + amountB: envUnits("CHAIN138_UNISWAP_V2_SEED_CUSDT_CUSDC_CUSDC", "250000", 6), + }, + ]; + + const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); + const deployedPairs = {}; + + for (const spec of seedSpecs) { + console.log(`[seed] ${spec.key}`); + let pairAddress = await factory.getPair(spec.tokenA, spec.tokenB); + if (pairAddress === hre.ethers.ZeroAddress) { + const tx = await factory.createPair(spec.tokenA, spec.tokenB, liquidityOverrides); + await tx.wait(); + pairAddress = await factory.getPair(spec.tokenA, spec.tokenB); + } + + const pair = Pair.attach(pairAddress); + const [reserve0, reserve1] = await pair.getReserves(); + if (reserve0 > 0n || reserve1 > 0n) { + deployedPairs[spec.key] = pairAddress; + console.log(`[skip] ${spec.key} already seeded ${pairAddress}`); + continue; + } + + const currentABalance = await tokenByAddress[spec.tokenA.toLowerCase()].balanceOf(pairAddress); + const currentBBalance = await tokenByAddress[spec.tokenB.toLowerCase()].balanceOf(pairAddress); + const topUpA = spec.amountA > currentABalance ? spec.amountA - currentABalance : 0n; + const topUpB = spec.amountB > currentBBalance ? spec.amountB - currentBBalance : 0n; + + await transferIfNeeded(tokenByAddress[spec.tokenA.toLowerCase()], pairAddress, topUpA); + await transferIfNeeded(tokenByAddress[spec.tokenB.toLowerCase()], pairAddress, topUpB); + + const mintTx = await pair.mint(deployer.address, liquidityOverrides); + await mintTx.wait(); + deployedPairs[spec.key] = pairAddress; + console.log(`[ok] ${spec.key}=${deployedPairs[spec.key]}`); + } + + const output = { + chainId: 138, + deployer: deployer.address, + deployedAtBlock: currentBlock, + factory: await factory.getAddress(), + router: await router.getAddress(), + weth, + usdt, + usdc, + cusdt, + cusdc, + pairs: deployedPairs, + }; + + const outputPath = path.resolve(__dirname, "../../deployments/chain138/uniswap-v2-native.json"); + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, JSON.stringify(output, null, 2) + "\n"); + + console.log(JSON.stringify(output, null, 2)); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/chain138/seed-uni-v2-weth-quote-pairs.js b/scripts/chain138/seed-uni-v2-weth-quote-pairs.js new file mode 100644 index 0000000..47f9446 --- /dev/null +++ b/scripts/chain138/seed-uni-v2-weth-quote-pairs.js @@ -0,0 +1,274 @@ +/** + * Create and seed Uniswap V2 pairs on Chain 138: each canonical asset vs WETH9 (ERC-20) in the pair. + * + * Funding the ETH leg (no explicit wrap in this script): + * - Liquidity is added via UniswapV2Router02.addLiquidityETH: you send native ETH as `msg.value`. + * - The router wraps to WETH9 inside the call and mints LP; this script never calls WETH9.deposit(). + * - Pair reserves are still WETH + token (AMM math cannot store raw ETH in the pair contract). + * + * Prerequisites: + * - PRIVATE_KEY; deployer holds native ETH (for `value` + gas) + tokens (or MINTER_ROLE to mint). + * - Factory + router: CHAIN138_UNISWAP_V2_EXISTING_* or deployments/chain138/uniswap-v2-native.json + * + * Usage (from smom-dbis-138): + * source scripts/load-env.sh + * npx hardhat run scripts/chain138/seed-uni-v2-weth-quote-pairs.js --network chain138 + * + * Tuning: + * CHAIN138_NATIVE_WETH9 — must match router’s WETH (default 0xC02a…) + * CHAIN138_UNI_V2_DEFAULT_WETH — native ETH `value` per new pair (default 0.25) + * CHAIN138_UNI_V2_STABLE_PER_WETH — USD face (6-decimal) per 1 ETH for stables (default 2100) + * CHAIN138_UNI_V2_SLIPPAGE_BPS — min amounts for addLiquidityETH (default 50 = 0.5%) + * CHAIN138_UNI_V2_ETH_GAS_BUFFER_WEI — extra wei required on top of `value` (default ~0.03 ETH for gas) + * Per-pair: CHAIN138_UNI_V2_SEED__WETH and CHAIN138_UNI_V2_SEED__TOKEN + */ +const fs = require("fs"); +const path = require("path"); +const hre = require("hardhat"); + +const WETH9 = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; + +/** Canonical Chain 138 tokens (see docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md). */ +const WETH_QUOTE_TARGETS = [ + { key: "wethLink", symbol: "LINK", address: "0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03", decimals: 18, kind: "link" }, + { key: "wethCusdt", symbol: "cUSDT", address: "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", decimals: 6, kind: "stable6" }, + { key: "wethCusdc", symbol: "cUSDC", address: "0xf22258f57794CC8E06237084b353Ab30fFfa640b", decimals: 6, kind: "stable6" }, + { key: "wethWeth10", symbol: "WETH10", address: "0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f", decimals: 18, kind: "weth10" }, + { key: "wethCeurc", symbol: "cEURC", address: "0x8085961F9cF02b4d800A3c6d386D31da4B34266a", decimals: 6, kind: "stable6" }, + { key: "wethCeurt", symbol: "cEURT", address: "0xdf4b71c61E5912712C1Bdd451416B9aC26949d72", decimals: 6, kind: "stable6" }, + { key: "wethCgbpc", symbol: "cGBPC", address: "0x003960f16D9d34F2e98d62723B6721Fb92074aD2", decimals: 6, kind: "stable6" }, + { key: "wethCgbrt", symbol: "cGBPT", address: "0x350f54e4D23795f86A9c03988c7135357CCaD97c", decimals: 6, kind: "stable6" }, + { key: "wethCaudc", symbol: "cAUDC", address: "0xD51482e567c03899eecE3CAe8a058161FD56069D", decimals: 6, kind: "stable6" }, + { key: "wethCjpyc", symbol: "cJPYC", address: "0xEe269e1226a334182aace90056EE4ee5Cc8A6770", decimals: 6, kind: "stable6" }, + { key: "wethCchfc", symbol: "cCHFC", address: "0x873990849DDa5117d7C644f0aF24370797C03885", decimals: 6, kind: "stable6" }, + { key: "wethCcadc", symbol: "cCADC", address: "0x54dBd40cF05e15906A2C21f600937e96787f5679", decimals: 6, kind: "stable6" }, + { key: "wethCxauc", symbol: "cXAUC", address: "0x290E52a8819A4fbD0714E517225429aA2B70EC6b", decimals: 6, kind: "xau6" }, + { key: "wethCxaut", symbol: "cXAUT", address: "0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E", decimals: 6, kind: "xau6" }, + { key: "wethUsdt", symbol: "USDT", address: "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", decimals: 6, kind: "stable6" }, + { key: "wethUsdc", symbol: "USDC", address: "0x71D6687F38b93CCad569Fa6352c876eea967201b", decimals: 6, kind: "stable6" }, +]; + +const erc20Abi = [ + "function transfer(address to,uint256 amount) returns (bool)", + "function approve(address spender,uint256 amount) returns (bool)", + "function balanceOf(address account) view returns (uint256)", +]; + +const routerAbi = [ + "function addLiquidityETH(address token,uint amountTokenDesired,uint amountTokenMin,uint amountETHMin,address to,uint deadline) payable returns (uint amountToken, uint amountETH, uint liquidity)", +]; + +const mintAbi = ["function mint(address to,uint256 amount)"]; + +function defaultWethFloat() { + return parseFloat(process.env.CHAIN138_UNI_V2_DEFAULT_WETH || "0.25"); +} + +function stablePerWethUsd() { + return parseFloat(process.env.CHAIN138_UNI_V2_STABLE_PER_WETH || "2100"); +} + +async function ensureMinted(tokenAddr, signer, need) { + const c = await hre.ethers.getContractAt(erc20Abi, tokenAddr, signer); + const bal = await c.balanceOf(signer.address); + if (bal >= need) return; + const short = need - bal; + const m = await hre.ethers.getContractAt(mintAbi, tokenAddr, signer); + await (await m.mint(signer.address, short)).wait(); +} + +async function ensureBalance(tokenAddr, signer, need, meta) { + const c = await hre.ethers.getContractAt(erc20Abi, tokenAddr, signer); + const bal = await c.balanceOf(signer.address); + if (bal >= need) return; + try { + await ensureMinted(tokenAddr, signer, need); + } catch (e) { + throw new Error( + `${meta.symbol}: need ${hre.ethers.formatUnits(need - bal, meta.decimals)} more; mint failed: ${e.message}` + ); + } +} + +/** Ensure wallet has enough native ETH for addLiquidityETH `value` plus a gas buffer (this script does not wrap). */ +async function ensureNativeEth(signer, valueWei) { + const buffer = BigInt(process.env.CHAIN138_UNI_V2_ETH_GAS_BUFFER_WEI || "30000000000000000"); + const need = valueWei + buffer; + const bal = await signer.provider.getBalance(signer.address); + if (bal < need) { + throw new Error( + `Native ETH: need at least ${hre.ethers.formatEther(need)} (${hre.ethers.formatEther(valueWei)} for pool + buffer); have ${hre.ethers.formatEther(bal)}` + ); + } +} + +function computeAmounts(meta) { + const envW = process.env[`CHAIN138_UNI_V2_SEED_${meta.key}_WETH`]; + const envT = process.env[`CHAIN138_UNI_V2_SEED_${meta.key}_TOKEN`]; + + const ethWei = + envW !== undefined && envW !== "" + ? hre.ethers.parseEther(String(envW)) + : hre.ethers.parseEther(String(defaultWethFloat())); + + const wethEth = parseFloat(hre.ethers.formatEther(ethWei)); + + let tokenStr; + if (envT !== undefined && envT !== "") { + tokenStr = String(envT); + } else { + switch (meta.kind) { + case "stable6": + tokenStr = String(Math.max(1, Math.round(stablePerWethUsd() * wethEth))); + break; + case "link": + tokenStr = String(Math.max(1, Math.round(100 * wethEth))); + break; + case "weth10": + tokenStr = hre.ethers.formatEther(ethWei); + break; + case "xau6": + tokenStr = String(Math.max(1, Math.round(1 * Math.max(wethEth, 0.01)))); + break; + default: + tokenStr = "1"; + } + } + + const tokenAmt = hre.ethers.parseUnits(tokenStr, meta.decimals); + return { ethWei, tokenAmt }; +} + +function minAmounts(amount, bps) { + return (amount * (10000n - bps)) / 10000n; +} + +async function main() { + const signers = await hre.ethers.getSigners(); + if (!signers?.length) { + throw new Error("chain138: no signer — set PRIVATE_KEY (32-byte hex) for network chain138 in env"); + } + const deployer = signers[0]; + const gasPrice = BigInt(process.env.CHAIN138_NATIVE_V2_GAS_PRICE || (await hre.ethers.provider.send("eth_gasPrice", []))); + const txOverrides = { + type: 0, + gasPrice, + gasLimit: BigInt(process.env.CHAIN138_NATIVE_V2_LIQUIDITY_GAS_LIMIT || "3000000"), + }; + + const deploymentPath = path.resolve(__dirname, "../../deployments/chain138/uniswap-v2-native.json"); + let factory = (process.env.CHAIN138_UNISWAP_V2_EXISTING_FACTORY || "").trim(); + let routerAddr = (process.env.CHAIN138_UNISWAP_V2_EXISTING_ROUTER || "").trim(); + if (fs.existsSync(deploymentPath)) { + const j = JSON.parse(fs.readFileSync(deploymentPath, "utf8")); + if (!factory) factory = j.factory; + if (!routerAddr) routerAddr = j.router; + } + if (!factory) { + throw new Error("Set CHAIN138_UNISWAP_V2_EXISTING_FACTORY or add deployments/chain138/uniswap-v2-native.json"); + } + if (!routerAddr) { + throw new Error("Set CHAIN138_UNISWAP_V2_EXISTING_ROUTER or add router to deployments/chain138/uniswap-v2-native.json"); + } + + const Factory = await hre.ethers.getContractFactory( + "contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol:UniswapV2Factory" + ); + const Pair = await hre.ethers.getContractFactory( + "contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol:UniswapV2Pair" + ); + + const factoryC = Factory.attach(factory); + const wethAddr = (process.env.CHAIN138_NATIVE_WETH9 || process.env.CHAIN138_WETH9_ADDRESS || WETH9).trim(); + const router = await hre.ethers.getContractAt(routerAbi, routerAddr, deployer); + + console.log( + `[eth] add liquidity via router addLiquidityETH (native ETH \`value\`; no WETH9.deposit() in this script) router=${routerAddr}` + ); + console.log(`[weth9] pair reserve token: ${wethAddr} (router wraps ETH inside addLiquidityETH)`); + + const slipBps = BigInt(process.env.CHAIN138_UNI_V2_SLIPPAGE_BPS || "50"); + const skipKeys = new Set( + (process.env.CHAIN138_UNI_V2_SKIP_KEYS || "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + ); + + /** @type {{ key: string, pair: string, status: string }[]} */ + const report = []; + + for (const meta of WETH_QUOTE_TARGETS) { + if (skipKeys.has(meta.key)) { + report.push({ key: meta.key, pair: "", status: "skip_env" }); + console.log(`[skip] ${meta.symbol} (${meta.key}) via CHAIN138_UNI_V2_SKIP_KEYS`); + continue; + } + const tokenAddr = meta.address; + if (tokenAddr.toLowerCase() === wethAddr.toLowerCase()) continue; + + const { ethWei, tokenAmt } = computeAmounts(meta); + + let pairAddress = await factoryC.getPair(wethAddr, tokenAddr); + if (pairAddress === hre.ethers.ZeroAddress) { + const tx = await factoryC.createPair(wethAddr, tokenAddr, txOverrides); + await tx.wait(); + pairAddress = await factoryC.getPair(wethAddr, tokenAddr); + } + + const pair = Pair.connect(deployer).attach(pairAddress); + const [r0, r1] = await pair.getReserves(); + if (r0 > 0n || r1 > 0n) { + report.push({ key: meta.key, pair: pairAddress, status: "skip_existing_liquidity" }); + console.log(`[skip] ${meta.symbol} already funded ${pairAddress}`); + continue; + } + + const tokenC = await hre.ethers.getContractAt(erc20Abi, tokenAddr, deployer); + + await ensureNativeEth(deployer, ethWei); + await ensureBalance(tokenAddr, deployer, tokenAmt, meta); + + const allowance = await tokenC.allowance(deployer.address, routerAddr); + if (allowance < tokenAmt) { + await (await tokenC.approve(routerAddr, hre.ethers.MaxUint256)).wait(); + } + + const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); + const tokenMin = minAmounts(tokenAmt, slipBps); + const ethMin = minAmounts(ethWei, slipBps); + + console.log(`[eth] addLiquidityETH ${meta.symbol}: value=${hre.ethers.formatEther(ethWei)} ETH, token=${tokenAmt.toString()}`); + const tx = await router.addLiquidityETH( + tokenAddr, + tokenAmt, + tokenMin, + ethMin, + deployer.address, + deadline, + { ...txOverrides, value: ethWei } + ); + await tx.wait(); + + report.push({ key: meta.key, pair: pairAddress, status: "seeded" }); + console.log(`[ok] ${meta.symbol} ${pairAddress}`); + } + + const out = { + chainId: 138, + deployer: deployer.address, + factory, + router: routerAddr, + weth9: wethAddr, + results: report, + }; + const outPath = path.resolve(__dirname, "../../deployments/chain138/uni-v2-weth-quote-seed.json"); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, JSON.stringify(out, null, 2) + "\n"); + console.log(JSON.stringify(out, null, 2)); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/deployment/c138-cw-bridge-75-split.sh b/scripts/deployment/c138-cw-bridge-75-split.sh new file mode 100755 index 0000000..c3236e3 --- /dev/null +++ b/scripts/deployment/c138-cw-bridge-75-split.sh @@ -0,0 +1,291 @@ +#!/usr/bin/env bash +# Plan + verify Chain 138 → 10 networks: move 75% of each c* balance split evenly (7.5% per network). +# Uses CWMultiTokenBridgeL1 on 138 (CW_L1_BRIDGE_CHAIN138) when routes are configured. +# +# Modes: +# --plan-only Write JSON table + human summary (default) +# --check-routes cast call supportedCanonicalToken + destinations per token×chain +# --emit-cmds Print approve + lockAndSend cast lines (dry; review before running) +# --help +# +# Env: PRIVATE_KEY, RPC_URL_138, CW_L1_BRIDGE_CHAIN138, smom-dbis-138/.env +# Optional: RECIPIENT_ADDRESS (default: deployer), OUT_JSON (default: reports/status/c138-bridge-75-split-latest.json) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)" +cd "$SMOM_ROOT" + +MODE="plan" +while [[ $# -gt 0 ]]; do + case "$1" in + --plan-only) MODE="plan" ;; + --check-routes) MODE="check" ;; + --emit-cmds) MODE="emit" ;; + --help|-h) + grep '^#' "$0" | head -20 + exit 0 + ;; + *) echo "Unknown: $1"; exit 1 ;; + esac + shift || true +done + +if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then + # shellcheck disable=SC1090 + PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" +elif [[ -f .env ]]; then + set -a && source .env && set +a +fi + +RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}" +OUT_JSON="${OUT_JSON:-$SMOM_ROOT/reports/status/c138-bridge-75-split-latest.json}" +BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}" +DEPLOYER="" +if [[ -n "${PRIVATE_KEY:-}" ]]; then + DEPLOYER="$(cast wallet address "$PRIVATE_KEY" 2>/dev/null || true)" +fi +RECIPIENT="${RECIPIENT_ADDRESS:-$DEPLOYER}" +export RPC OUT_JSON DEPLOYER RECIPIENT BRIDGE + +# CCIP chain selectors (Chainlink CCIP mainnet directory / repo BRIDGE_CONFIGURATION.md). Verify before prod. +declare -A SELECTOR=( + [Mainnet]=5009297550715157269 + [Optimism]=3734403246176062136 + [Cronos]=1456215246176062136 + [BSC]=11344663589394136015 + [Gnosis]=465200170687744372 + [Polygon]=4051577828743386545 + [Base]=15971525489660198786 + [Arbitrum]=4949039107694359620 + [Celo]=1346049177634351622 + [Avalanche]=6433500567565415381 +) + +# name:address (Compliant / canonical c* on 138) +read -r -d '' TOKEN_ROWS << 'EOF' || true +cUSDT:0x93E66202A11B1772E55407B32B44e5Cd8eda7f22 +cUSDC:0xf22258f57794CC8E06237084b353Ab30fFfa640b +cUSDT_V2:0x8d342d321DdEe97D0c5011DAF8ca0B59DA617D29 +cUSDC_V2:0x1ac3F4942a71E86A9682D91837E1E71b7BACdF99 +cEURC:0x8085961F9cF02b4d800A3c6d386D31da4B34266a +cEURT:0xdf4b71c61E5912712C1Bdd451416B9aC26949d72 +cGBPC:0x003960f16D9d34F2e98d62723B6721Fb92074aD2 +cGBPT:0x350f54e4D23795f86A9c03988c7135357CCaD97c +cAUDC:0xD51482e567c03899eecE3CAe8a058161FD56069D +cJPYC:0xEe269e1226a334182aace90056EE4ee5Cc8A6770 +cCHFC:0x873990849DDa5117d7C644f0aF24370797C03885 +cCADC:0x54dBd40cF05e15906A2C21f600937e96787f5679 +cAUDT:0xC034b8Ff3088f644D492E95619720ba8fB582933 +cJPYT:0x54fb3A6b16163D8cFa48EAff79205D1309B1a9A1 +cCHFT:0xd91f31725444dD1F53FA6dE236A5e90a8281d970 +cCADT:0xAb456be5Db1E1069F55F75E8c8fecAa6a71D1c8F +cXAUC:0x290E52a8819A4fbD0714E517225429aA2B70EC6b +cXAUT:0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E +cAUSDT:0x5fdDF65733e3d590463F68f93Cf16E8c04081271 +EOF + +export TOKEN_ROWS +mkdir -p "$(dirname "$OUT_JSON")" + +python3 << PY +import json, subprocess, os, re, sys + +rpc = os.environ.get("RPC", "http://192.168.11.211:8545") +deployer = os.environ.get("DEPLOYER", "") +recipient = os.environ.get("RECIPIENT", deployer) +bridge = os.environ.get("BRIDGE", "") + +selectors = { + "Mainnet": 5009297550715157269, + "Optimism": 3734403246176062136, + "Cronos": 1456215246176062136, + "BSC": 11344663589394136015, + "Gnosis": 465200170687744372, + "Polygon": 4051577828743386545, + "Base": 15971525489660198786, + "Arbitrum": 4949039107694359620, + "Celo": 1346049177634351622, + "Avalanche": 6433500567565415381, +} +n_chains = len(selectors) + +rows = [] +for line in os.environ.get("TOKEN_ROWS", "").strip().split("\n"): + if not line.strip() or line.startswith("#"): + continue + sym, addr = line.split(":", 1) + rows.append((sym.strip(), addr.strip())) + +def balance_of(addr): + if not deployer: + return None + r = subprocess.run( + ["cast", "call", addr, "balanceOf(address)(uint256)", deployer, "--rpc-url", rpc], + capture_output=True, text=True, + ) + if r.returncode != 0: + return None + m = re.match(r"^\s*(\d+)", r.stdout.strip()) + return int(m.group(1)) if m else None + +plan = { + "schema": "c138-bridge-75-split/v1", + "rpc_url": rpc, + "deployer": deployer, + "recipient": recipient, + "cw_l1_bridge": bridge, + "split": "75% of balance divided evenly across 10 networks (7.5% per chain, integer base units)", + "tokens": [], +} + +for sym, addr in rows: + bal = balance_of(addr) + if bal is None: + plan["tokens"].append({"symbol": sym, "address": addr, "error": "balance_of_failed"}) + continue + q75 = bal * 75 // 100 + per = q75 // n_chains + rem = q75 % n_chains + entry = { + "symbol": sym, + "address": addr, + "balance_wei": str(bal), + "pct_75_wei": str(q75), + "per_chain_wei": str(per), + "remainder_wei": str(rem), + "chains": {}, + } + for cname, sel in selectors.items(): + entry["chains"][cname] = {"selector": str(sel), "amount_wei": str(per)} + plan["tokens"].append(entry) + +path = os.environ.get("OUT_JSON", "") +with open(path, "w") as f: + json.dump(plan, f, indent=2) + +print(json.dumps({"written": path, "tokens": len(plan["tokens"])})) +PY + +export RPC DEPLOYER RECIPIENT BRIDGE OUT_JSON +export TOKEN_ROWS="$TOKEN_ROWS" + +python3 - <<'PY' +import json, os +with open(os.environ["OUT_JSON"]) as f: + p = json.load(f) +print("\n=== c* 75% / 10 networks (per-chain amount, 6 dp human for fiat-style) ===\n") +print(f"Deployer: {p.get('deployer','?')}\nRecipient: {p.get('recipient','?')}\nBridge: {p.get('cw_l1_bridge') or '(unset)'}\n") +for t in p["tokens"]: + if "error" in t: + print(f"{t['symbol']}: {t['error']}") + continue + sym = t["symbol"] + per = int(t["per_chain_wei"]) + if sym.startswith("cXAU"): + hu = per / 1e6 + print(f"{sym:<10} per chain: {hu:,.6f} troy oz (wei={t['per_chain_wei']})") + else: + hu = per / 1e6 + print(f"{sym:<10} per chain: {hu:,.6f} tokens (wei={t['per_chain_wei']})") +print("\nJSON:", os.environ["OUT_JSON"]) +PY + +if [[ "$MODE" == "plan" ]]; then + exit 0 +fi + +[[ -n "$BRIDGE" ]] || { echo "Set CW_L1_BRIDGE_CHAIN138"; exit 1; } +code=$(cast code "$BRIDGE" --rpc-url "$RPC" 2>/dev/null || echo "0x") +[[ -n "$code" && "$code" != "0x" ]] || { echo "No contract at CW_L1_BRIDGE_CHAIN138=$BRIDGE"; exit 1; } + +if [[ "$MODE" == "check" ]]; then + echo "" + echo "=== Route checks: $BRIDGE ===" + while IFS= read -r line; do + [[ -z "$line" ]] && continue + sym="${line%%:*}" + addr="${line#*:}" + if sup_raw=$(cast call "$BRIDGE" "supportedCanonicalToken(address)(bool)" "$addr" --rpc-url "$RPC" 2>/dev/null); then + echo "$sym supported=$sup_raw" + else + echo "$sym supported=(query reverted — non-CW ABI or older build; rely on destinations below)" + fi + for net in Mainnet Optimism Cronos BSC Gnosis Polygon Base Arbitrum Celo Avalanche; do + sel="${SELECTOR[$net]}" + dest=$(cast call "$BRIDGE" "destinations(address,uint64)(address,bool)" "$addr" "$sel" --rpc-url "$RPC" 2>/dev/null || echo "ERR") + echo " $net ($sel): $dest" + done + done <<< "$TOKEN_ROWS" + exit 0 +fi + +if [[ "$MODE" == "emit" ]]; then + [[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required for --emit-cmds"; exit 1; } + [[ -n "$RECIPIENT" ]] || { echo "RECIPIENT_ADDRESS or deployer required"; exit 1; } + LINK_TOKEN="${LINK_TOKEN_CHAIN138:-${LINK_TOKEN:-}}" + [[ -n "$LINK_TOKEN" ]] || { echo "Set LINK_TOKEN or LINK_TOKEN_CHAIN138 for fee approval lines"; exit 1; } + export LINK_TOKEN + echo "" + echo "=== Review-only cast snippets (feeToken=LINK on this bridge: approve LINK, approve token, lockAndSend) ===" + OUT_CAST="${OUT_CAST:-$SMOM_ROOT/reports/status/c138-bridge-75-split-cast-commands.sh}" + export OUT_CAST + { + echo "#!/usr/bin/env bash" + echo "# Generated: c138-cw-bridge-75-split.sh --emit-cmds" + echo "# Review destinations + reserve verifier before running. Fund LINK for fees." + echo "set -euo pipefail" + # Quoted heredoc: do not let bash expand \$PRIVATE_KEY into the generated file. + python3 <<'PY' +import json, os, subprocess +rpc = os.environ["RPC"] +bridge = os.environ["BRIDGE"] +recipient = os.environ["RECIPIENT"] +link = os.environ["LINK_TOKEN"] +with open(os.environ["OUT_JSON"]) as f: + plan = json.load(f) +selectors = { + "Mainnet": 5009297550715157269, + "Optimism": 3734403246176062136, + "Cronos": 1456215246176062136, + "BSC": 11344663589394136015, + "Gnosis": 465200170687744372, + "Polygon": 4051577828743386545, + "Base": 15971525489660198786, + "Arbitrum": 4949039107694359620, + "Celo": 1346049177634351622, + "Avalanche": 6433500567565415381, +} +for t in plan["tokens"]: + if "error" in t or int(t.get("per_chain_wei", 0)) == 0: + continue + sym, token, amt = t["symbol"], t["address"], t["per_chain_wei"] + for cname, sel in selectors.items(): + chk = subprocess.run( + ["cast", "call", bridge, "destinations(address,uint64)(address,bool)", token, str(sel), "--rpc-url", rpc], + capture_output=True, text=True, + ) + if chk.returncode != 0 or "true" not in chk.stdout: + continue + fee = subprocess.run( + ["cast", "call", bridge, + "calculateFee(address,uint64,address,uint256)(uint256)", + token, str(sel), recipient, amt, + "--rpc-url", rpc], + capture_output=True, text=True, + ) + fq = fee.stdout.strip().split()[0] if fee.returncode == 0 else "0" + print("") + print(f"# {sym} -> {cname} selector={sel} amount={amt} fee_wei={fq}") + print(f"cast send {link} \"approve(address,uint256)\" {bridge} {fq} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 120000") + print(f"cast send {token} \"approve(address,uint256)\" {bridge} {amt} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 400000") + print(f"cast send {bridge} \"lockAndSend(address,uint64,address,uint256)\" {token} {sel} {recipient} {amt} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 4000000") +PY + } > "$OUT_CAST" + chmod +x "$OUT_CAST" + echo "Wrote enabled-routes-only commands: $OUT_CAST" + wc -l "$OUT_CAST" + exit 0 +fi diff --git a/scripts/deployment/cw-l1-bootstrap-gru-v2-ccip-routes.sh b/scripts/deployment/cw-l1-bootstrap-gru-v2-ccip-routes.sh new file mode 100755 index 0000000..360248e --- /dev/null +++ b/scripts/deployment/cw-l1-bootstrap-gru-v2-ccip-routes.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Register GRU v2 (CompliantFiatTokenV2) addresses on CWMultiTokenBridgeL1 and +# configureDestination for the same 10 CCIP lanes as cUSDT/cUSDC (receivers from CW_BRIDGE_*). +# +# Note: The CWMultiTokenBridgeL1 deployed at CW_L1_BRIDGE_CHAIN138 on Chain 138 does **not** +# include configureSupportedCanonicalToken in runtime bytecode (older build). Do not send that +# call — it always reverts. Destinations + lockAndSend are the supported path. +# +# Usage: +# bash scripts/deployment/cw-l1-bootstrap-gru-v2-ccip-routes.sh [--dry-run] +# +# Env: PROJECT_ROOT load via proxmox scripts/lib/load-project-env.sh or smom-dbis-138/.env +# CW_L1_BRIDGE_CHAIN138, RPC_URL_138, PRIVATE_KEY (deployer = bridge admin) +# CW_BRIDGE_MAINNET, CW_BRIDGE_OPTIMISM, CW_BRIDGE_CRONOS, CW_BRIDGE_BSC, +# CW_BRIDGE_GNOSIS, CW_BRIDGE_POLYGON, CW_BRIDGE_BASE, CW_BRIDGE_ARBITRUM, +# CW_BRIDGE_CELO, CW_BRIDGE_AVALANCHE + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)" +cd "$SMOM_ROOT" + +DRY_RUN=0 +[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1 + +if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then + # shellcheck disable=SC1090 + PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" +elif [[ -f .env ]]; then + set -a && source .env && set +a +fi + +BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}" +RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}" + +[[ -n "$BRIDGE" && "$BRIDGE" != "0x0000000000000000000000000000000000000000" ]] || { + echo "Set CW_L1_BRIDGE_CHAIN138" >&2 + exit 1 +} +[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required" >&2; exit 1; } + +# GRU v2 on Chain 138 (wallet-token-submissions.chain138.json) +V2=( + "0x243e6581Dc8a98d98B92265858b322b193555C81" + "0x2bAFA83d8fF8BaE9505511998987D0659791605B" + "0x707508D223103f5D2d9EFBc656302c9d48878b29" + "0xee17c18E10E55ce23F7457D018aAa2Fb1E64B281" + "0xfb37aFd415B70C5cEDc9bA58a72D517207b769Bb" + "0x2c751bBE4f299b989b3A8c333E0A966cdcA6Fd98" + "0x60B7FB8e0DD0Be8595AD12Fe80AE832861Be747c" + "0xe799033c87fE0CE316DAECcefBE3134CC74b76a9" + "0xF0F0F81bE3D033D8586bAfd2293e37eE2f615647" + "0x89477E982847023aaB5C3492082cd1bB4b1b9Ef1" +) + +# selector|receiver (must match c138-cw-bridge-75-split.sh / BRIDGE_CONFIGURATION) +declare -a ROUTES=( + "5009297550715157269|${CW_BRIDGE_MAINNET:-}" + "3734403246176062136|${CW_BRIDGE_OPTIMISM:-}" + "1456215246176062136|${CW_BRIDGE_CRONOS:-}" + "11344663589394136015|${CW_BRIDGE_BSC:-}" + "465200170687744372|${CW_BRIDGE_GNOSIS:-}" + "4051577828743386545|${CW_BRIDGE_POLYGON:-}" + "15971525489660198786|${CW_BRIDGE_BASE:-}" + "4949039107694359620|${CW_BRIDGE_ARBITRUM:-}" + "1346049177634351622|${CW_BRIDGE_CELO:-}" + "6433500567565415381|${CW_BRIDGE_AVALANCHE:-}" +) + +send() { + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "[dry-run] $*" + return 0 + fi + "$@" +} + +echo "=== configureDestination (10 GRU v2 tokens x up to 10 chains) ===" +for t in "${V2[@]}"; do + for row in "${ROUTES[@]}"; do + sel="${row%%|*}" + recv="${row#*|}" + if [[ -z "$recv" || "$recv" == "0x0000000000000000000000000000000000000000" ]]; then + echo "SKIP selector=$sel (receiver unset)" + continue + fi + echo "-> token=$t selector=$sel recv=$recv" + send cast send "$BRIDGE" "configureDestination(address,uint64,address,bool)" "$t" "$sel" "$recv" true \ + --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit 500000 + done +done + +echo "" +echo "Done. Verify with: cast call $BRIDGE \"destinations(address,uint64)(address,bool)\" --rpc-url $RPC" diff --git a/scripts/deployment/cw-l1-bridge-gru-v2-90pct-lock-and-send.sh b/scripts/deployment/cw-l1-bridge-gru-v2-90pct-lock-and-send.sh new file mode 100755 index 0000000..57843b6 --- /dev/null +++ b/scripts/deployment/cw-l1-bridge-gru-v2-90pct-lock-and-send.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# After cw-l1-bootstrap-gru-v2-ccip-routes.sh: approve LINK + tokens, then lockAndSend +# 90% / 10 per-chain amounts to RECIPIENT_ADDRESS (default Ulysse EVM). +# +# Usage: +# RECIPIENT_ADDRESS=0x... bash scripts/deployment/cw-l1-bridge-gru-v2-90pct-lock-and-send.sh [--dry-run] +# +# Env: CW_L1_BRIDGE_CHAIN138, RPC_URL_138, PRIVATE_KEY +# LINK: fee token on bridge (defaults to feeToken() on bridge) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)" +cd "$SMOM_ROOT" + +DRY_RUN=0 +[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1 + +if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then + PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" +elif [[ -f .env ]]; then + set -a && source .env && set +a +fi + +BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}" +RPC="${RPC_URL_138:-http://192.168.11.211:8545}" +RECIPIENT="${RECIPIENT_ADDRESS:-0xCbe669ECe9e3Da7dD1688207e545EDDA9a64A514}" + +[[ -n "$BRIDGE" ]] || { echo "Set CW_L1_BRIDGE_CHAIN138" >&2; exit 1; } +[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required" >&2; exit 1; } + +LINK_TOKEN="${LINK_TOKEN_CHAIN138:-${LINK_TOKEN:-}}" +if [[ -z "$LINK_TOKEN" ]]; then + LINK_TOKEN=$(cast call "$BRIDGE" "feeToken()(address)" --rpc-url "$RPC" 2>/dev/null | grep -oE '0x[a-fA-F0-9]{40}' | head -1) +fi +[[ -n "$LINK_TOKEN" && "$LINK_TOKEN" != "0x0000000000000000000000000000000000000000" ]] || { + echo "Could not resolve LINK fee token; set LINK_TOKEN_CHAIN138" >&2 + exit 1 +} + +MAX_U256="115792089237316195423570985008687907853269984665640564039457584007913129639935" + +# token|per_chain_wei (90% / 10 floors from operator plan) +declare -a PAIRS=( + "0x243e6581Dc8a98d98B92265858b322b193555C81|3822172740000" + "0x2bAFA83d8fF8BaE9505511998987D0659791605B|2778945930000" + "0x707508D223103f5D2d9EFBc656302c9d48878b29|4123219860000" + "0xee17c18E10E55ce23F7457D018aAa2Fb1E64B281|4744441350000" + "0xfb37aFd415B70C5cEDc9bA58a72D517207b769Bb|6522556410000" + "0x2c751bBE4f299b989b3A8c333E0A966cdcA6Fd98|3557466000000" + "0x60B7FB8e0DD0Be8595AD12Fe80AE832861Be747c|6810124410000" + "0xe799033c87fE0CE316DAECcefBE3134CC74b76a9|2471337720000" + "0xF0F0F81bE3D033D8586bAfd2293e37eE2f615647|2421904773" + "0x89477E982847023aaB5C3492082cd1bB4b1b9Ef1|2421904773" +) + +# Same 10 selectors as bootstrap script (order must match) +declare -a SELECTORS=( + 5009297550715157269 + 3734403246176062136 + 1456215246176062136 + 11344663589394136015 + 465200170687744372 + 4051577828743386545 + 15971525489660198786 + 4949039107694359620 + 1346049177634351622 + 6433500567565415381 +) + +send() { + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "[dry-run] $*" + return 0 + fi + "$@" +} + +echo "Bridge=$BRIDGE recipient=$RECIPIENT LINK=$LINK_TOKEN" +echo "=== Approvals: LINK + each GRU v2 token to bridge (max) ===" +send cast send "$LINK_TOKEN" "approve(address,uint256)" "$BRIDGE" "$MAX_U256" \ + --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit 120000 + +for row in "${PAIRS[@]}"; do + token="${row%%|*}" + echo "approve token $token" + send cast send "$token" "approve(address,uint256)" "$BRIDGE" "$MAX_U256" \ + --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit 120000 +done + +echo "" +echo "=== lockAndSend (10 tokens x 10 chains) ===" +for row in "${PAIRS[@]}"; do + token="${row%%|*}" + amt="${row#*|}" + for sel in "${SELECTORS[@]}"; do + echo "lockAndSend token=$token selector=$sel amount=$amt" + send cast send "$BRIDGE" "lockAndSend(address,uint64,address,uint256)" \ + "$token" "$sel" "$RECIPIENT" "$amt" \ + --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit 5000000 + done +done + +echo "Done." diff --git a/scripts/deployment/cw-l1-configure-destination-8-chains.sh b/scripts/deployment/cw-l1-configure-destination-8-chains.sh new file mode 100755 index 0000000..7f6b9ca --- /dev/null +++ b/scripts/deployment/cw-l1-configure-destination-8-chains.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Admin template: CWMultiTokenBridgeL1.configureDestination for the eight chains +# that are not Mainnet / Avalanche (those typically already wired for cUSDT/cUSDC). +# +# Uses receiver addresses from CW_BRIDGE_OPTIMISM, CW_BRIDGE_CRONOS, CW_BRIDGE_BSC, +# CW_BRIDGE_GNOSIS, CW_BRIDGE_POLYGON, CW_BRIDGE_BASE, CW_BRIDGE_ARBITRUM, CW_BRIDGE_CELO +# (see smom-dbis-138/.env). CCIP selectors match docs/deployment/BRIDGE_CONFIGURATION.md +# and Chainlink CCIP directory — verify before mainnet use. +# +# Target contract: CW_L1_BRIDGE_CHAIN138 — onlyAdmin. +# +# Usage: +# bash scripts/deployment/cw-l1-configure-destination-8-chains.sh [--print-only] +# # default writes reports/status/cw-l1-configure-destination-8-chains.cast.sh +# +# Env: +# CW_L1_BRIDGE_CHAIN138, RPC_URL_138 (or CHAIN138_RPC), PRIVATE_KEY (only if you execute the .sh) +# CUSDT_ADDRESS_138, CUSDC_ADDRESS_138 (or override CANONICAL_TOKENS="0x...,0x...") +# +# Does not broadcast unless you run the generated file. Never commit private keys. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)" +cd "$SMOM_ROOT" + +PRINT_ONLY=0 +[[ "${1:-}" == "--print-only" ]] && PRINT_ONLY=1 + +if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then + # shellcheck disable=SC1090 + PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" +elif [[ -f .env ]]; then + set -a && source .env && set +a +fi + +BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}" +RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}" +OUT_SH="${OUT_SH:-$SMOM_ROOT/reports/status/cw-l1-configure-destination-8-chains.cast.sh}" + +if [[ -z "$BRIDGE" || "$BRIDGE" == "0x0000000000000000000000000000000000000000" ]]; then + echo "Set CW_L1_BRIDGE_CHAIN138 in .env" >&2 + exit 1 +fi + +# Remaining eight (exclude Mainnet + Avalanche) +declare -a ROWS=( + "Optimism|3734403246176062136|${CW_BRIDGE_OPTIMISM:-}" + "Cronos|1456215246176062136|${CW_BRIDGE_CRONOS:-}" + "BSC|11344663589394136015|${CW_BRIDGE_BSC:-}" + "Gnosis|465200170687744372|${CW_BRIDGE_GNOSIS:-}" + "Polygon|4051577828743386545|${CW_BRIDGE_POLYGON:-}" + "Base|15971525489660198786|${CW_BRIDGE_BASE:-}" + "Arbitrum|4949039107694359620|${CW_BRIDGE_ARBITRUM:-}" + "Celo|1346049177634351622|${CW_BRIDGE_CELO:-}" +) + +TOKENS_STR="${CANONICAL_TOKENS:-}" +if [[ -z "$TOKENS_STR" ]]; then + T1="${CUSDT_ADDRESS_138:-${COMPLIANT_USDT_ADDRESS:-}}" + T2="${CUSDC_ADDRESS_138:-${COMPLIANT_USDC_ADDRESS:-}}" + TOKENS_STR="${T1},${T2}" +fi + +IFS=',' read -r -a TOKEN_ADDRS <<< "$(echo "$TOKENS_STR" | tr -d '[:space:]')" +[[ -n "${TOKEN_ADDRS[0]:-}" && "${TOKEN_ADDRS[0]}" == 0x* ]] || { echo "Set CUSDT_ADDRESS_138 or CANONICAL_TOKENS" >&2; exit 1; } + +gen() { + local sym="$1" token="$2" + echo "" + echo "# --- $sym $token ---" + for row in "${ROWS[@]}"; do + IFS='|' read -r name sel recv <<< "$row" + if [[ -z "$recv" || "$recv" == "0x0000000000000000000000000000000000000000" ]]; then + echo "# SKIP $name: CW_BRIDGE_* receiver unset" + continue + fi + echo "# $sym -> $name (selector $sel) receiver=$recv" + echo "cast send \"$BRIDGE\" \\" + echo " \"configureDestination(address,uint64,address,bool)\" \\" + echo " \"$token\" \\" + echo " \"$sel\" \\" + echo " \"$recv\" \\" + echo " true \\" + echo " --rpc-url \"$RPC\" \\" + echo " --private-key \"\$PRIVATE_KEY\" \\" + echo " --legacy \\" + echo " --gas-limit 500000" + echo "" + echo "# Verify: cast call \"$BRIDGE\" \"destinations(address,uint64)(address,bool)\" \"$token\" \"$sel\" --rpc-url \"$RPC\"" + echo "" + done +} + +{ + echo "#!/usr/bin/env bash" + echo "# Generated: cw-l1-configure-destination-8-chains.sh" + echo "# Admin-only: configureDestination on CW L1 for eight chains (not Mainnet/Avalanche)." + echo "# Requires: admin key = deployer or bridge admin; source .env with PRIVATE_KEY." + echo "set -euo pipefail" + echo "" + + if [[ -n "${TOKEN_ADDRS[1]:-}" && "${TOKEN_ADDRS[1]}" == 0x* ]]; then + gen "cUSDT" "${TOKEN_ADDRS[0]}" + gen "cUSDC" "${TOKEN_ADDRS[1]}" + else + gen "TOKEN" "${TOKEN_ADDRS[0]}" + fi +} > "$OUT_SH" +chmod +x "$OUT_SH" + +echo "Wrote: $OUT_SH" +echo "" +if [[ "$PRINT_ONLY" -eq 1 ]] || [[ -t 1 ]]; then + cat "$OUT_SH" +fi diff --git a/scripts/deployment/deploy-compliant-fiat-tether-iso-chain138.sh b/scripts/deployment/deploy-compliant-fiat-tether-iso-chain138.sh new file mode 100755 index 0000000..fc1b4bd --- /dev/null +++ b/scripts/deployment/deploy-compliant-fiat-tether-iso-chain138.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Deploy Tether-style ISO-4217 complements (cAUDT, cJPYT, cCHFT, cCADT) on Chain 138. +# +# Why not CREATE2 via CREATE2Factory (0x750E4a8adCe9f0e67A420aBE91342DC64Eb90825)? +# On Core/Besu, factory.deploy(bytes,uint256) with CompliantFiatToken initcode consistently +# OOGs (~2.6M–8M+ gas) even with high limits; eth_call traces succeed — use standard CREATE here. +# +# Uses scoped Forge (scripts/forge/scope.sh create tokens) so solc does not pull the full repo. +# Compiles with --evm-version paris: Core 138 Besu may reject Cancun bytecode (invalid opcode 0x5f/PUSH0). +# +# Requires: PRIVATE_KEY; RPC via RPC_URL_138, CHAIN138_RPC, or RPC_URL (see below). +# Idempotent: skips if contract already has code at the address from env (if set). +# +# Usage: from repo root: +# bash smom-dbis-138/scripts/deployment/deploy-compliant-fiat-tether-iso-chain138.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)" +cd "$SMOM_ROOT" + +if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then + # shellcheck disable=SC1090 + PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" +elif [[ -f .env ]]; then + set -a && source .env && set +a +fi + +[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required"; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "jq required (for receipt status check)"; exit 1; } +RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}" +# Paris bytecode deploy ~2M gas; leave headroom for Besu. +GAS_LIMIT_DEPLOY="${GAS_LIMIT_DEPLOY:-3500000}" +EVM_VERSION_CHAIN138="${EVM_VERSION_CHAIN138:-paris}" +OWNER="$(cast wallet address "$PRIVATE_KEY")" + +# Ensure scoped artifacts match chain EVM (Paris for Chain 138 Core). +bash scripts/forge/scope.sh build tokens --evm-version "$EVM_VERSION_CHAIN138" --force + +declare -a SPECS=( + "cAUDT|Tether AUD (Compliant)|AUD|${CAUDT_ADDRESS_138:-}" + "cJPYT|Tether JPY (Compliant)|JPY|${CJPYT_ADDRESS_138:-}" + "cCHFT|Tether CHF (Compliant)|CHF|${CCHFT_ADDRESS_138:-}" + "cCADT|Tether CAD (Compliant)|CAD|${CCADT_ADDRESS_138:-}" +) + +DEPLOYED_SNIPPET="# After deploy (paste into smom-dbis-138/.env next to other C*ADDRESS_138):" +while IFS='|' read -r sym name cc existing; do + if [[ -n "$existing" && "$existing" != "0x0000000000000000000000000000000000000000" ]]; then + code=$(cast code "$existing" --rpc-url "$RPC" 2>/dev/null || echo "0x") + if [[ -n "$code" && "$code" != "0x" ]]; then + echo "Skip $sym (already deployed at $existing)" + continue + fi + fi + echo "Deploy $sym..." + out=$(bash scripts/forge/scope.sh create tokens contracts/tokens/CompliantFiatToken.sol:CompliantFiatToken \ + --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy \ + --evm-version "$EVM_VERSION_CHAIN138" \ + --gas-limit "$GAS_LIMIT_DEPLOY" \ + --broadcast \ + --constructor-args "$name" "$sym" 6 "$cc" "$OWNER" "$OWNER" 1000000000000 2>&1) || { + echo "$out" + exit 1 + } + echo "$out" + txh=$(printf '%s\n' "$out" | sed -n 's/.*Transaction hash: \(0x[0-9a-fA-F]\{64\}\).*/\1/p' | head -1) + if [[ -n "$txh" ]]; then + st=$(cast receipt "$txh" --json --rpc-url "$RPC" 2>/dev/null | jq -r '.status // "0x0"') + if [[ "$st" != "0x1" ]]; then + echo "error: tx $txh failed on-chain (status=$st). Raise GAS_LIMIT_DEPLOY or inspect: cast run $txh --rpc-url $RPC" >&2 + exit 1 + fi + fi + addr=$(printf '%s\n' "$out" | sed -n 's/.*Deployed to: \(0x[0-9a-fA-F]\{40\}\).*/\1/p' | head -1) + if [[ -z "$addr" ]]; then + addr=$(printf '%s\n' "$out" | sed -n 's/.*"contract":"\(0x[0-9a-fA-F]\{40\}\)".*/\1/p' | head -1) + fi + if [[ -n "$addr" ]]; then + ccode=$(cast code "$addr" --rpc-url "$RPC" 2>/dev/null || echo "0x") + if [[ -z "$ccode" || "$ccode" == "0x" ]]; then + echo "error: no code at $addr after successful receipt — aborting" >&2 + exit 1 + fi + fi + if [[ -n "$addr" ]]; then + var="" + case "$sym" in + cAUDT) var="CAUDT_ADDRESS_138" ;; + cJPYT) var="CJPYT_ADDRESS_138" ;; + cCHFT) var="CCHFT_ADDRESS_138" ;; + cCADT) var="CCADT_ADDRESS_138" ;; + esac + DEPLOYED_SNIPPET+=$'\n'"${var}=${addr}" + fi +done < <(printf '%s\n' "${SPECS[@]}") + +echo "" +echo "$DEPLOYED_SNIPPET" +echo "" +echo "Verify: cast code --rpc-url \"\$RPC_URL_138\"" +echo "Optional mint (random 26M–85M per token, deployer must hold minter role):" +echo " bash smom-dbis-138/scripts/mint-compliant-fiat-tether-iso-138.sh" diff --git a/scripts/deployment/deploy-pmm-all-l2s.sh b/scripts/deployment/deploy-pmm-all-l2s.sh index 230181e..828a961 100755 --- a/scripts/deployment/deploy-pmm-all-l2s.sh +++ b/scripts/deployment/deploy-pmm-all-l2s.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Deploy DODOPMMIntegration on each L2 (BSC, Polygon, Base, Optimism, Arbitrum, Avalanche, Cronos, Gnosis). +# Deploy DODOPMMIntegration on each L2/public chain (BSC, Polygon, Base, Optimism, Arbitrum, Avalanche, Cronos, Gnosis, Celo). # Uses .env for RPC and token addresses. Chains via tag: --chain bsc polygon ... (or DEPLOY_PMM_L2S_FILTER in .env). # Usage: ./scripts/deployment/deploy-pmm-all-l2s.sh [--chain bsc polygon base] set -euo pipefail @@ -37,6 +37,7 @@ CHAINS=( "AVALANCHE:43114:AVALANCHE_RPC_URL" "CRONOS:25:CRONOS_RPC_URL" "GNOSIS:100:GNOSIS_MAINNET_RPC" + "CELO:42220:CELO_MAINNET_RPC" ) for entry in "${CHAINS[@]}"; do @@ -59,14 +60,16 @@ for entry in "${CHAINS[@]}"; do cusdc_var="${name}_COMPLIANT_USDC_ADDRESS" cusdt_var_alt="COMPLIANT_USDT_${name}" cusdc_var_alt="COMPLIANT_USDC_${name}" + cwusdt_var="CWUSDT_${name}" + cwusdc_var="CWUSDC_${name}" # Per-chain cUSDT/cUSDC (optional): CUSDT_ADDRESS_ / CUSDC_ADDRESS_ or POLYGON_COMPLIANT_USDT_ADDRESS etc. cusdt_chain="CUSDT_ADDRESS_${chain_id}" cusdc_chain="CUSDC_ADDRESS_${chain_id}" dvm="$(first_set_env "$dvm_var" "DODO_VENDING_MACHINE_ADDRESS" || true)" usdt="$(first_set_env "$usdt_var" "$usdt_var_alt" "OFFICIAL_USDT_ADDRESS" || true)" usdc="$(first_set_env "$usdc_var" "$usdc_var_alt" "OFFICIAL_USDC_ADDRESS" || true)" - compliant_usdt="$(first_set_env "$cusdt_var" "$cusdt_var_alt" "$cusdt_chain" || true)" - compliant_usdc="$(first_set_env "$cusdc_var" "$cusdc_var_alt" "$cusdc_chain" || true)" + compliant_usdt="$(first_set_env "$cusdt_var" "$cusdt_var_alt" "$cusdt_chain" "$cwusdt_var" || true)" + compliant_usdc="$(first_set_env "$cusdc_var" "$cusdc_var_alt" "$cusdc_chain" "$cwusdc_var" || true)" compliant_usdt="${compliant_usdt:-$usdt}" compliant_usdc="${compliant_usdc:-$usdc}" diff --git a/scripts/deployment/seed-chain138-uni-v2-weth-quote-pairs.sh b/scripts/deployment/seed-chain138-uni-v2-weth-quote-pairs.sh new file mode 100755 index 0000000..7b7cb28 --- /dev/null +++ b/scripts/deployment/seed-chain138-uni-v2-weth-quote-pairs.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Seed Uniswap V2 pools on Chain 138: each canonical asset vs WETH (see script header). +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" +if [[ -f scripts/load-env.sh ]]; then + # shellcheck source=/dev/null + source scripts/load-env.sh +fi +export CHAIN138_RPC_URL="${CHAIN138_RPC_URL:-${RPC_URL_138:-http://192.168.11.211:8545}}" +exec npx hardhat run scripts/chain138/seed-uni-v2-weth-quote-pairs.js --network chain138 diff --git a/scripts/deployment/verify-wemix-bridges.sh b/scripts/deployment/verify-wemix-bridges.sh new file mode 100755 index 0000000..ef8e8b1 --- /dev/null +++ b/scripts/deployment/verify-wemix-bridges.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$PROJECT_ROOT" + +if [[ -f "$SCRIPT_DIR/../lib/deployment/dotenv.sh" ]]; then + # shellcheck disable=SC1090 + source "$SCRIPT_DIR/../lib/deployment/dotenv.sh" + load_deployment_env --repo-root "${PROJECT_ROOT}" +elif [[ -f "$PROJECT_ROOT/.env" ]]; then + set -a + # shellcheck disable=SC1090 + source "$PROJECT_ROOT/.env" + set +a +fi + +require_env() { + local name="$1" + if [[ -z "${!name:-}" ]]; then + echo "ERROR: $name is required" >&2 + exit 1 + fi +} + +require_env WEMIX_RPC +require_env CCIPWETH9_BRIDGE_WEMIX +require_env CCIPWETH10_BRIDGE_WEMIX +require_env CCIP_ROUTER_WEMIX +require_env WETH9_WEMIX +require_env WETH10_WEMIX +require_env LINK_TOKEN_WEMIX +require_env WEMIXSCAN_API_KEY + +flatten_b64() { + local source_file="$1" + forge flatten "$source_file" | python3 -c 'import sys,base64; print(base64.b64encode(sys.stdin.buffer.read()).decode())' +} + +constructor_args() { + local weth="$1" + cast abi-encode 'constructor(address,address,address)' \ + "$CCIP_ROUTER_WEMIX" \ + "$weth" \ + "$LINK_TOKEN_WEMIX" | sed 's/^0x//' +} + +submit_verify() { + local address="$1" + local contract_name="$2" + local source_file="$3" + local weth="$4" + + local payload + payload="$(mktemp)" + + python3 - "$payload" "$contract_name" "$(flatten_b64 "$source_file")" "$(constructor_args "$weth")" <<'PY' +import json +import sys +from pathlib import Path + +path, contract_name, contract_source, constructor_arguments = sys.argv[1:] +body = { + "contract_name": contract_name, + "compiler": "v0.8.20+commit.a1b79de6", + "runs_optimizer": 200, + "optimization_enabled": 1, + "contract_source": contract_source, + "constructor_arguments": constructor_arguments, + "libraries": [], +} +Path(path).write_text(json.dumps(body)) +PY + + echo "Submitting verification for $contract_name at $address..." + local response_file + response_file="$(mktemp)" + local http_code + http_code="$( + curl --max-time 90 \ + -sS \ + -o "$response_file" \ + -w '%{http_code}' \ + -H "api-key: ${WEMIXSCAN_API_KEY}" \ + -H 'Content-Type: application/json' \ + -X POST \ + --data @"$payload" \ + "https://explorerapi.wemix.com/v1/contracts/${address}/verify" + )" + + echo "HTTP ${http_code}" + cat "$response_file" + echo + + if [[ "$http_code" == "200" ]]; then + echo "Verification request accepted for $contract_name" + elif [[ "$http_code" == "504" ]]; then + echo "WARNING: Wemix explorer timed out while compiling $contract_name. Check the explorer code tab manually." >&2 + else + echo "WARNING: Verification not accepted for $contract_name" >&2 + fi +} + +check_status() { + local address="$1" + echo "Checking explorer code endpoint for $address..." + curl --max-time 30 \ + -sS \ + -H "api-key: ${WEMIXSCAN_API_KEY}" \ + "https://explorerapi.wemix.com/v1/contracts/${address}/code" || true + echo +} + +submit_verify "$CCIPWETH9_BRIDGE_WEMIX" "CCIPWETH9Bridge" "contracts/ccip/CCIPWETH9Bridge.sol" "$WETH9_WEMIX" +submit_verify "$CCIPWETH10_BRIDGE_WEMIX" "CCIPWETH10Bridge" "contracts/ccip/CCIPWETH10Bridge.sol" "$WETH10_WEMIX" + +check_status "$CCIPWETH9_BRIDGE_WEMIX" +check_status "$CCIPWETH10_BRIDGE_WEMIX" diff --git a/scripts/forge/scope.sh b/scripts/forge/scope.sh index b6d964d..5bf5126 100755 --- a/scripts/forge/scope.sh +++ b/scripts/forge/scope.sh @@ -44,12 +44,14 @@ Usage: bash scripts/forge/scope.sh build [scope] [forge build args...] bash scripts/forge/scope.sh test [scope] [forge test args...] bash scripts/forge/scope.sh script [scope] [forge script args...] + bash scripts/forge/scope.sh create [scope] [forge create args...] bash scripts/forge/scope.sh orphans [--json] Examples: bash scripts/forge/scope.sh build treasury bash scripts/forge/scope.sh test flash --match-path 'test/flash/*.t.sol' bash scripts/forge/scope.sh script bridge/trustless script/bridge/trustless/DeployTrustlessBridge.s.sol:DeployTrustlessBridge --rpc-url "$RPC_URL_138" + bash scripts/forge/scope.sh create tokens contracts/tokens/CompliantFiatToken.sol:CompliantFiatToken --rpc-url "$RPC_URL_138" --private-key "$PRIVATE_KEY" --legacy FORGE_SCOPE=vault bash scripts/forge/scope.sh test --match-path 'test/vault/*.t.sol' Notes: @@ -266,7 +268,7 @@ main() { orphans) exec python3 scripts/forge/report-contract-reachability.py "$@" ;; - build|test|script) + build|test|script|create) local scope="" if [[ $# -gt 0 && "$1" != --* ]]; then local maybe_scope @@ -313,6 +315,10 @@ main() { [[ $# -gt 0 ]] || die "script command requires a script target, e.g. script/treasury/DeployTreasuryExecutor138.s.sol:DeployTreasuryExecutor138" exec forge script "$@" ;; + create) + [[ $# -gt 0 ]] || die "create command requires a contract target, e.g. contracts/tokens/CompliantFiatToken.sol:CompliantFiatToken" + exec forge create "$@" + ;; esac ;; *) diff --git a/scripts/mint-compliant-fiat-tether-iso-138.sh b/scripts/mint-compliant-fiat-tether-iso-138.sh new file mode 100755 index 0000000..cb9a910 --- /dev/null +++ b/scripts/mint-compliant-fiat-tether-iso-138.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Mint CompliantFiatToken Tether-style ISO complements on Chain 138 (cAUDT, cJPYT, cCHFT, cCADT). +# Uses addresses from smom-dbis-138/.env: CAUDT_ADDRESS_138, CJPYT_ADDRESS_138, CCHFT_ADDRESS_138, CCADT_ADDRESS_138. +# +# Default: random whole-number amount per token between 26_000_000 and 85_000_000 (6 decimals). +# Override: ./scripts/mint-compliant-fiat-tether-iso-138.sh (same amount for all four) +# +# Requires: PRIVATE_KEY (minter/deployer), RPC. Skips any address unset or zero. +# Requires: cast (foundry). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PROXMOX_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$SMOM_ROOT" + +if [[ -f .env ]]; then + set -a && source .env && set +a +fi +if [[ -z "${PRIVATE_KEY:-}" && -f "${PROXMOX_ROOT}/scripts/lib/load-project-env.sh" ]]; then + # shellcheck disable=SC1090 + PROJECT_ROOT="$PROXMOX_ROOT" source "${PROXMOX_ROOT}/scripts/lib/load-project-env.sh" + cd "$SMOM_ROOT" +fi + +RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}" +GAS_LIMIT_MINT="${GAS_LIMIT_MINT:-400000}" + +[ -n "${PRIVATE_KEY:-}" ] || { echo "PRIVATE_KEY not set"; exit 1; } +DEPLOYER=$(cast wallet address "$PRIVATE_KEY" 2>/dev/null) || exit 1 + +if [[ -n "${1:-}" ]]; then + AMOUNT_HUMAN="$1" +elif command -v shuf >/dev/null 2>&1; then + AMOUNT_HUMAN=$(shuf -i 26000000-85000000 -n 1) +elif command -v python3 >/dev/null 2>&1; then + AMOUNT_HUMAN=$(python3 -c 'import random; print(random.randint(26_000_000, 85_000_000))') +else + echo "Install coreutils (shuf) or python3 for random mint amounts, or pass amount_human as \$1" >&2 + exit 1 +fi +BASE_UNITS=$((AMOUNT_HUMAN * 1000000)) + +echo "=== Mint Tether ISO complements (Chain 138) ===" +echo " Deployer: $DEPLOYER" +echo " Amount: $AMOUNT_HUMAN tokens each ($BASE_UNITS base units)" +echo " RPC: $RPC" +echo "" + +for pair in \ + "cAUDT:${CAUDT_ADDRESS_138:-}" \ + "cJPYT:${CJPYT_ADDRESS_138:-}" \ + "cCHFT:${CCHFT_ADDRESS_138:-}" \ + "cCADT:${CCADT_ADDRESS_138:-}"; do + sym="${pair%%:*}" + addr="${pair#*:}" + if [[ -z "$addr" || "$addr" == "0x0000000000000000000000000000000000000000" ]]; then + echo "Skip $sym (address not set in .env)" + continue + fi + echo -n "Mint $sym ($addr)... " + if cast send "$addr" "mint(address,uint256)" "$DEPLOYER" "$BASE_UNITS" \ + --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit "$GAS_LIMIT_MINT" 2>/dev/null; then + echo OK + else + echo FAIL + fi +done +echo "Done." diff --git a/scripts/verify-tezos-etherlink-support.js b/scripts/verify-tezos-etherlink-support.js index 13cd805..e519921 100644 --- a/scripts/verify-tezos-etherlink-support.js +++ b/scripts/verify-tezos-etherlink-support.js @@ -4,27 +4,59 @@ const LIFI_URL = 'https://li.quest/v1/chains'; const CCIP_URL = 'https://docs.chain.link/ccip/supported-networks'; +async function fetchText(url) { + const res = await fetch(url, { + headers: { + 'user-agent': 'proxmox-operator-support-check/1.0', + accept: 'text/html,application/json;q=0.9,*/*;q=0.8', + }, + }); + if (!res.ok) throw new Error(`${url} returned ${res.status}`); + return res.text(); +} + +async function fetchJson(url) { + const res = await fetch(url, { + headers: { + 'user-agent': 'proxmox-operator-support-check/1.0', + accept: 'application/json', + }, + }); + if (!res.ok) throw new Error(`${url} returned ${res.status}`); + return res.json(); +} + async function main() { console.log('Tezos/Etherlink support check\n'); try { - const lifiRes = await fetch(LIFI_URL); - const lifi = await lifiRes.json(); - const ids = (lifi.chains || []).map((c) => c.id || c.chainId).filter(Boolean); + const lifi = await fetchJson(LIFI_URL); + const chains = lifi.chains || []; + const ids = chains.map((c) => c.id || c.chainId).filter(Boolean); + const etherlink = chains.find((c) => (c.id || c.chainId) === 42793); console.log('LiFi chains include 138:', ids.includes(138)); console.log('LiFi chains include 42793:', ids.includes(42793)); console.log('LiFi chains include 651940:', ids.includes(651940)); + if (etherlink) { + console.log('LiFi Etherlink entry:', JSON.stringify({ + id: etherlink.id || etherlink.chainId, + key: etherlink.key, + name: etherlink.name, + coin: etherlink.coin, + mainnet: etherlink.mainnet, + })); + } } catch (e) { console.log('LiFi error:', e.message); } try { - const ccipRes = await fetch(CCIP_URL); - const html = await ccipRes.text(); - console.log('\nCCIP page mentions 42793:', html.includes('42793') || html.toLowerCase().includes('etherlink')); - console.log('CCIP page mentions 138:', html.includes('138')); + const html = await fetchText(CCIP_URL); + const hasEtherlinkMention = html.includes('42793') || html.toLowerCase().includes('etherlink'); + console.log('\nCCIP page mentions 42793/Etherlink:', hasEtherlinkMention); + console.log('CCIP page mention is not treated as support without selector/router confirmation.'); } catch (e) { console.log('CCIP error:', e.message); } - console.log('\nJumper: verify manually; see docs/07-ccip/TEZOS_JUMPER_SUPPORT_MATRIX.md'); + console.log('\nJumper: still verify manually; see docs/07-ccip/TEZOS_JUMPER_SUPPORT_MATRIX.md'); } main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/services/etherlink-relay/src/config.js b/services/etherlink-relay/src/config.js index 8b92e20..f094158 100644 --- a/services/etherlink-relay/src/config.js +++ b/services/etherlink-relay/src/config.js @@ -14,8 +14,22 @@ export const config = { sourceChain: { rpcUrl: process.env.RPC_URL_138 || process.env.RPC_URL || "http://127.0.0.1:8545" }, etherlinkRpcUrl: process.env.ETHERLINK_RPC_URL || "https://node.mainnet.etherlink.com", etherlinkRelayBridge: process.env.ETHERLINK_RELAY_BRIDGE || "", - relayPrivateKey: process.env.ETHERLINK_RELAY_PRIVATE_KEY || process.env.PRIVATE_KEY, + relayPrivateKey: resolvePrivateKey( + process.env.ETHERLINK_RELAY_PRIVATE_KEY, + process.env.PRIVATE_KEY, + process.env.DEPLOYER_PRIVATE_KEY, + ), pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || "5000", 10), maxConcurrent: parseInt(process.env.ETHERLINK_RELAY_MAX_CONCURRENT || "5", 10), queueDepthLimit: parseInt(process.env.ETHERLINK_RELAY_QUEUE_DEPTH || "100", 10), }; + +function resolvePrivateKey(...candidates) { + for (const candidate of candidates) { + if (!candidate) continue; + const value = candidate.trim(); + if (!value || value.includes('${')) continue; + if (/^0x[0-9a-fA-F]{64}$/.test(value)) return value; + } + return ""; +} diff --git a/services/non-evm-relay/lifecycle.ts b/services/non-evm-relay/lifecycle.ts new file mode 100644 index 0000000..16dcce9 --- /dev/null +++ b/services/non-evm-relay/lifecycle.ts @@ -0,0 +1,53 @@ +export const NON_EVM_RELAY_LIFECYCLE = [ + 'initiate', + 'observe_destination', + 'confirm_finalize', + 'reject_replay', + 'refund_recover' +] as const; + +export type NonEvmRelayLifecycleStep = (typeof NON_EVM_RELAY_LIFECYCLE)[number]; + +export type NonEvmExposureStatus = + | 'planned' + | 'operator_ready' + | 'public_ready' + | 'live' + | 'gated_by_chain138_prerequisites'; + +export interface NonEvmNetworkPolicy { + identifier: string; + relayMode: 'custom_relay' | 'custom_relay_scaffold' | 'parallel_program'; + destinationProgramModel: string; + signerFundingPolicy: string; + finalityPolicy: string; + publicExposureStatus: Exclude; +} + +export interface NonEvmRelayObservation { + requestId: string; + lifecycleStep: NonEvmRelayLifecycleStep; + destinationReference?: string; + fulfillmentId?: string; + finalityValue?: number; + replayRejected?: boolean; +} + +export function deriveExposureStatus( + policy: NonEvmNetworkPolicy, + checks: { + adapterPresent: boolean; + relaySurfacePresent: boolean; + chain138PrerequisitesReady: boolean; + } +): NonEvmExposureStatus { + if (!checks.adapterPresent || !checks.relaySurfacePresent) { + return 'planned'; + } + + if (!checks.chain138PrerequisitesReady) { + return 'gated_by_chain138_prerequisites'; + } + + return policy.publicExposureStatus; +} diff --git a/services/solana-relay/.env.example b/services/solana-relay/.env.example new file mode 100644 index 0000000..0f278c0 --- /dev/null +++ b/services/solana-relay/.env.example @@ -0,0 +1,11 @@ +SOLANA_RELAY_RUNTIME_CONFIG=../../config/solana-relay-runtime.json +SOLANA_OPERATOR_HOST=192.168.11.11 +SOLANA_CLUSTER=mainnet-beta +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_KEYPAIR_PATH=/root/.config/solana/id.json +SOLANA_OPERATOR_WALLET=26HRdYbob5LmVM638jfpDd6ELyHz6TqP25YHVd9dGT4A +SOLANA_DESTINATION_WALLET=26HRdYbob5LmVM638jfpDd6ELyHz6TqP25YHVd9dGT4A +SOLANA_ADAPTER_ADDRESS= +SOLANA_MIN_FINALITY_SLOTS=32 +CHAIN138_RPC_URL=http://192.168.11.211:8545 +CHAIN_REGISTRY_ADDRESS=0x6949137625CA923A4e9C80D5bc7DF673f9bbb84F diff --git a/services/solana-relay/package.json b/services/solana-relay/package.json new file mode 100644 index 0000000..8c315e6 --- /dev/null +++ b/services/solana-relay/package.json @@ -0,0 +1,13 @@ +{ + "name": "solana-relay", + "private": true, + "type": "module", + "scripts": { + "relay": "node src/relay-worker.mjs", + "relay:once": "node src/relay-worker.mjs --once" + }, + "dependencies": { + "dotenv": "^16.6.1", + "ethers": "^6.15.0" + } +} diff --git a/services/solana-relay/src/SolanaRelayService.ts b/services/solana-relay/src/SolanaRelayService.ts new file mode 100644 index 0000000..591df3d --- /dev/null +++ b/services/solana-relay/src/SolanaRelayService.ts @@ -0,0 +1,116 @@ +import { + NON_EVM_RELAY_LIFECYCLE, + type NonEvmNetworkPolicy, + type NonEvmRelayObservation +} from '../../non-evm-relay/lifecycle'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export const SOLANA_RELAY_POLICY: NonEvmNetworkPolicy = { + identifier: 'Solana', + relayMode: 'custom_relay', + destinationProgramModel: 'spl_or_bridge_wrapped_cw', + signerFundingPolicy: 'sol_operator_signer', + finalityPolicy: 'slot_finality>=32', + publicExposureStatus: 'live' +}; + +export interface SolanaRelayAssetRuntime { + chain138Symbol: string; + sourceToken: string; + solanaSymbol: string; + solanaMint: string; + decimals: number; + smokeTestAmountRaw: string; +} + +export interface SolanaRelayRuntimeConfig { + sourceChain: { + chainId: number; + chainRegistryAddress: string; + registryIdentifier: string; + adapterAddress: string | null; + }; + solana: { + cluster: string; + rpcUrl: string; + operatorWallet: string; + confirmationFinality: number; + recipientEncoding: string; + }; + relay: { + mode: string; + runtimeConfigEnv: string; + syntheticConfirmationPrefix: string; + publicExposureStatus?: string; + workerEntryPoint?: string; + }; + smokeTests: { + defaultAssets: string[]; + wethRawAmount: string; + gruRawAmount: string; + requireAdapterRegistration: boolean; + }; + assets: SolanaRelayAssetRuntime[]; +} + +export function resolveSolanaRelayRuntimeConfigPath(): string { + const explicit = process.env.SOLANA_RELAY_RUNTIME_CONFIG; + if (explicit && explicit.trim() !== '') { + return path.resolve(process.cwd(), explicit); + } + + return path.resolve(__dirname, '../../../../config/solana-relay-runtime.json'); +} + +export function loadSolanaRelayRuntimeConfig(): SolanaRelayRuntimeConfig { + const runtimePath = resolveSolanaRelayRuntimeConfigPath(); + const raw = fs.readFileSync(runtimePath, 'utf8'); + return JSON.parse(raw) as SolanaRelayRuntimeConfig; +} + +/** + * Shared lifecycle surface for the Solana relay worker. + * The actual host-side worker entry point lives in relay-worker.mjs and performs + * real Solana settlement plus Chain 138 finalization. + */ +export class SolanaRelayService { + readonly lifecycle = NON_EVM_RELAY_LIFECYCLE; + readonly runtime: SolanaRelayRuntimeConfig; + + constructor(runtime = loadSolanaRelayRuntimeConfig()) { + this.runtime = runtime; + } + + recordObservation(observation: NonEvmRelayObservation): NonEvmRelayObservation { + return { + ...observation, + lifecycleStep: observation.lifecycleStep + }; + } + + getAsset(symbol: string): SolanaRelayAssetRuntime | undefined { + return this.runtime.assets.find((asset) => asset.solanaSymbol === symbol || asset.chain138Symbol === symbol); + } + + buildSmokeObservation( + requestId: string, + symbol: string, + destinationReference?: string + ): NonEvmRelayObservation { + const asset = this.getAsset(symbol); + const destination = destinationReference || asset?.solanaMint || this.runtime.solana.operatorWallet; + + return { + requestId, + lifecycleStep: 'confirm_finalize', + destinationReference: destination, + fulfillmentId: `${this.runtime.relay.syntheticConfirmationPrefix}:${symbol}:${requestId}`, + finalityValue: this.runtime.solana.confirmationFinality, + replayRejected: false + }; + } +} diff --git a/services/solana-relay/src/relay-worker.mjs b/services/solana-relay/src/relay-worker.mjs new file mode 100644 index 0000000..76aed57 --- /dev/null +++ b/services/solana-relay/src/relay-worker.mjs @@ -0,0 +1,239 @@ +import fs from 'fs'; +import path from 'path'; +import process from 'process'; +import { execFileSync } from 'child_process'; +import { fileURLToPath } from 'url'; +import dotenv from 'dotenv'; +import { ethers } from 'ethers'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const serviceRoot = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(serviceRoot, '..', '..', '..'); +const stateDir = path.join(serviceRoot, 'state'); +const defaultStatePath = path.join(stateDir, 'relay-state.json'); +const defaultHealthPath = path.join(repoRoot, 'reports', 'status', 'solana-relay-worker-health.json'); + +dotenv.config({ path: path.join(repoRoot, '.env') }); +dotenv.config({ path: path.join(repoRoot, 'smom-dbis-138/.env') }); +dotenv.config({ path: path.join(process.env.HOME || '', '.secure-secrets', 'private-keys.env'), override: true }); + +function readJson(filePath, fallback) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch { + return fallback; + } +} + +function writeJson(filePath, value) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function loadRuntime() { + const runtimePath = process.env.SOLANA_RELAY_RUNTIME_CONFIG + ? path.resolve(process.cwd(), process.env.SOLANA_RELAY_RUNTIME_CONFIG) + : path.join(repoRoot, 'config', 'solana-relay-runtime.json'); + return { + path: runtimePath, + config: JSON.parse(fs.readFileSync(runtimePath, 'utf8')) + }; +} + +function normalizeAddress(value) { + return ethers.getAddress(String(value).toLowerCase()); +} + +function rawToUiAmount(rawValue, decimals) { + const raw = BigInt(rawValue); + const base = 10n ** BigInt(decimals); + const whole = raw / base; + const fraction = raw % base; + if (fraction === 0n) return whole.toString(); + const fractionString = fraction.toString().padStart(decimals, '0').replace(/0+$/, ''); + return `${whole.toString()}.${fractionString}`; +} + +function decodeRecipient(destination, recipientBytes) { + if (destination && destination.trim() !== '') return destination.trim(); + if (!recipientBytes || recipientBytes === '0x') return null; + return ethers.toUtf8String(recipientBytes).trim(); +} + +function runLocalCommand(command, args) { + return execFileSync(command, args, { + cwd: repoRoot, + encoding: 'utf8', + env: process.env + }).trim(); +} + +function ensureAta(mint, wallet, keypairPath) { + try { + runLocalCommand('spl-token', ['create-account', mint, '--owner', wallet, '--fee-payer', keypairPath]); + } catch { + // No-op when the associated token account already exists. + } +} + +function mintToDestination(mint, uiAmount, wallet, keypairPath) { + ensureAta(mint, wallet, keypairPath); + for (let attempt = 0; attempt < 3; attempt += 1) { + try { + const output = runLocalCommand('spl-token', [ + 'mint', + mint, + uiAmount, + '--mint-authority', + keypairPath, + '--recipient-owner', + wallet + ]); + const match = output.match(/Signature:\s+(\S+)/); + if (match) return match[1]; + } catch { + // Retry with a fresh blockhash on transient Solana RPC errors. + } + } + throw new Error(`Unable to parse Solana mint signature for ${mint}`); +} + +function confirmSolanaSignature(signature) { + try { + return runLocalCommand('solana', ['confirm', signature, '--output', 'json']); + } catch (error) { + throw new Error(`Solana confirmation failed for ${signature}: ${error.message}`); + } +} + +function buildFulfillmentId(requestId, solanaSymbol, solanaMint) { + return ethers.keccak256( + ethers.solidityPacked(['bytes32', 'string', 'string'], [requestId, solanaSymbol, solanaMint]) + ); +} + +async function main() { + const once = process.argv.includes('--once'); + const runtimeBundle = loadRuntime(); + const runtime = runtimeBundle.config; + const rpcUrl = process.env.RPC_URL_138 || process.env.CHAIN138_RPC_URL || runtime.sourceChain.rpcEnv; + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey || privateKey.includes('${')) { + throw new Error('PRIVATE_KEY is required'); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const signer = new ethers.Wallet(privateKey, provider); + const adapterAddress = normalizeAddress(runtime.sourceChain.adapterAddress); + const adapterAbi = [ + 'function confirmTransaction(bytes32 requestId,string txSignature,uint256 finalizedSlot,bytes32 fulfillmentId)', + 'function getBridgeStatus(bytes32 requestId) view returns (tuple(address sender,address token,uint256 amount,bytes destinationData,bytes32 requestId,uint8 status,uint256 createdAt,uint256 completedAt))', + 'event SolanaBridgeInitiated(bytes32 indexed requestId,address indexed sender,address indexed token,uint256 amount,string destination,bytes recipient)' + ]; + const adapter = new ethers.Contract(adapterAddress, adapterAbi, signer); + const interfaceDecoder = adapter.interface; + const assetBySourceToken = new Map( + (runtime.assets || []).map((asset) => [normalizeAddress(asset.sourceToken), asset]) + ); + const state = readJson(defaultStatePath, { lastBlock: 0, processed: {} }); + const latestBlock = await provider.getBlockNumber(); + const fromBlock = Math.max(0, Number(state.lastBlock || 0) + 1); + const health = { + generatedAt: new Date().toISOString(), + runtimeConfig: runtimeBundle.path, + adapterAddress, + signer: signer.address, + fromBlock, + latestBlock, + processedThisRun: [], + mode: once ? 'once' : 'poll' + }; + + if (fromBlock > latestBlock) { + state.lastBlock = latestBlock; + writeJson(defaultStatePath, state); + writeJson(defaultHealthPath, health); + return; + } + + const logs = await provider.getLogs({ + address: adapterAddress, + fromBlock, + toBlock: latestBlock, + topics: [ethers.id('SolanaBridgeInitiated(bytes32,address,address,uint256,string,bytes)')] + }); + + for (const log of logs) { + const parsed = interfaceDecoder.parseLog(log); + const requestId = parsed.args.requestId; + if (state.processed[requestId]) continue; + + const request = await adapter.getBridgeStatus(requestId); + if (Number(request.status) !== 1) continue; + + const sourceToken = normalizeAddress(parsed.args.token); + const asset = assetBySourceToken.get(sourceToken); + if (!asset) { + throw new Error(`No Solana runtime asset for source token ${sourceToken}`); + } + + const recipientWallet = decodeRecipient(parsed.args.destination, parsed.args.recipient); + if (!recipientWallet) { + throw new Error(`Missing Solana recipient for request ${requestId}`); + } + + const uiAmount = rawToUiAmount(request.amount.toString(), asset.decimals); + const solanaSignature = mintToDestination( + asset.solanaMint, + uiAmount, + recipientWallet, + runtime.solana.keypairPath + ); + confirmSolanaSignature(solanaSignature); + + const fulfillmentId = buildFulfillmentId(requestId, asset.solanaSymbol, asset.solanaMint); + const confirmTx = await adapter.confirmTransaction( + requestId, + solanaSignature, + BigInt(runtime.solana.confirmationFinality), + fulfillmentId, + { + type: 0, + gasPrice: BigInt(process.env.CHAIN138_TX_GAS_PRICE || '200000') + } + ); + const confirmReceipt = await confirmTx.wait(); + + state.processed[requestId] = { + blockNumber: log.blockNumber, + sourceToken, + solanaSymbol: asset.solanaSymbol, + solanaMint: asset.solanaMint, + recipientWallet, + requestAmountRaw: request.amount.toString(), + solanaSignature, + confirmTxHash: confirmReceipt.hash, + processedAt: new Date().toISOString() + }; + health.processedThisRun.push({ + requestId, + solanaSymbol: asset.solanaSymbol, + solanaSignature, + confirmTxHash: confirmReceipt.hash + }); + } + + state.lastBlock = latestBlock; + writeJson(defaultStatePath, state); + writeJson(defaultHealthPath, health); +} + +main().catch((error) => { + writeJson(defaultHealthPath, { + generatedAt: new Date().toISOString(), + status: 'error', + error: error.message + }); + console.error(error); + process.exit(1); +}); diff --git a/services/stellar-relay/src/StellarRelayService.ts b/services/stellar-relay/src/StellarRelayService.ts new file mode 100644 index 0000000..79954b4 --- /dev/null +++ b/services/stellar-relay/src/StellarRelayService.ts @@ -0,0 +1,22 @@ +import { + NON_EVM_RELAY_LIFECYCLE, + type NonEvmNetworkPolicy, + type NonEvmRelayObservation +} from '../../non-evm-relay/lifecycle'; + +export const STELLAR_RELAY_POLICY: NonEvmNetworkPolicy = { + identifier: 'Stellar', + relayMode: 'custom_relay_scaffold', + destinationProgramModel: 'stellar_issued_or_wrapped_cw', + signerFundingPolicy: 'xlm_operator_signer', + finalityPolicy: 'ledger_finality>=1', + publicExposureStatus: 'operator_ready' +}; + +export class StellarRelayService { + readonly lifecycle = NON_EVM_RELAY_LIFECYCLE; + + recordObservation(observation: NonEvmRelayObservation): NonEvmRelayObservation { + return observation; + } +} diff --git a/services/tezos-relay/src/config.js b/services/tezos-relay/src/config.js index 4906e45..b540438 100644 --- a/services/tezos-relay/src/config.js +++ b/services/tezos-relay/src/config.js @@ -15,8 +15,22 @@ export const config = { tezosAdapterAddress: process.env.TEZOS_ADAPTER_ADDRESS || '', tezosRpcUrl: process.env.TEZOS_RPC_URL || 'https://mainnet.smartpy.io', tezosMinterAddress: process.env.TEZOS_MINTER_ADDRESS || '', - oraclePrivateKey: process.env.TEZOS_RELAY_ORACLE_KEY || process.env.PRIVATE_KEY, + oraclePrivateKey: resolvePrivateKey( + process.env.TEZOS_RELAY_ORACLE_KEY, + process.env.PRIVATE_KEY, + process.env.DEPLOYER_PRIVATE_KEY, + ), pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || '5000', 10), maxConcurrent: parseInt(process.env.TEZOS_RELAY_MAX_CONCURRENT || '5', 10), mockTezosRelay: process.env.MOCK_TEZOS_RELAY === 'true', }; + +function resolvePrivateKey(...candidates) { + for (const candidate of candidates) { + if (!candidate) continue; + const value = candidate.trim(); + if (!value || value.includes('${')) continue; + if (/^0x[0-9a-fA-F]{64}$/.test(value)) return value; + } + return ''; +} diff --git a/services/token-aggregation/README.md b/services/token-aggregation/README.md index 8c253e9..33ea7ee 100644 --- a/services/token-aggregation/README.md +++ b/services/token-aggregation/README.md @@ -230,14 +230,29 @@ Configure DEX factory addresses in `src/config/dex-factories.ts` or via environm ```bash # ChainID 138 CHAIN_138_DODO_POOL_MANAGER=0x... -CHAIN_138_UNISWAP_V2_FACTORY=0x... -CHAIN_138_UNISWAP_V3_FACTORY=0x... +CHAIN_138_UNISWAP_V2_FACTORY=0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279 +CHAIN_138_UNISWAP_V2_ROUTER=0x3019A7fDc76ba7F64F18d78e66842760037ee638 +CHAIN_138_UNISWAP_V2_START_BLOCK=4041370 +CHAIN_138_SUSHISWAP_FACTORY=0x2871207ff0d56089D70c0134d33f1291B6Fce0BE +CHAIN_138_SUSHISWAP_ROUTER=0xB37b93D38559f53b62ab020A14919f2630a1aE34 +CHAIN_138_SUSHISWAP_START_BLOCK=4041495 +CHAIN_138_UNISWAP_V3_FACTORY=0x2f7219276e3ce367dB9ec74C1196a8ecEe67841C +CHAIN_138_UNISWAP_V3_ROUTER=0xde9cD8ee2811E6E64a41D5F68Be315d33995975E # ChainID 651940 CHAIN_651940_UNISWAP_V2_FACTORY=0x... +CHAIN_651940_UNISWAP_V2_ROUTER=0x... +CHAIN_651940_UNISWAP_V2_START_BLOCK=0 CHAIN_651940_UNISWAP_V3_FACTORY=0x... +CHAIN_651940_UNISWAP_V3_ROUTER=0x... +CHAIN_651940_UNISWAP_V3_START_BLOCK=0 +CHAIN_651940_HYDX_FACTORY=0x... +CHAIN_651940_HYDX_ROUTER=0x... +CHAIN_651940_HYDX_START_BLOCK=0 ``` +For ALL Mainnet non-DODO discovery, the repo now treats `HYDX` as the canonical custom venue surface when factory/router details are known. The broader `651940` non-DODO inventory is tracked in `config/allmainnet-non-dodo-protocol-surface.json`. + ## Monitoring The service includes: diff --git a/services/token-aggregation/docs/DEPLOYMENT.md b/services/token-aggregation/docs/DEPLOYMENT.md index 5709423..3116f25 100644 --- a/services/token-aggregation/docs/DEPLOYMENT.md +++ b/services/token-aggregation/docs/DEPLOYMENT.md @@ -175,15 +175,30 @@ For ChainID 138, configure DODO PoolManager address: ```bash CHAIN_138_DODO_POOL_MANAGER=0x... +CHAIN_138_UNISWAP_V2_FACTORY=0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279 +CHAIN_138_UNISWAP_V2_ROUTER=0x3019A7fDc76ba7F64F18d78e66842760037ee638 +CHAIN_138_UNISWAP_V2_START_BLOCK=4041370 +CHAIN_138_SUSHISWAP_FACTORY=0x2871207ff0d56089D70c0134d33f1291B6Fce0BE +CHAIN_138_SUSHISWAP_ROUTER=0xB37b93D38559f53b62ab020A14919f2630a1aE34 +CHAIN_138_SUSHISWAP_START_BLOCK=4041495 ``` For ChainID 651940, configure DEX factories as they are discovered: ```bash CHAIN_651940_UNISWAP_V2_FACTORY=0x... +CHAIN_651940_UNISWAP_V2_ROUTER=0x... +CHAIN_651940_UNISWAP_V2_START_BLOCK=0 CHAIN_651940_UNISWAP_V3_FACTORY=0x... +CHAIN_651940_UNISWAP_V3_ROUTER=0x... +CHAIN_651940_UNISWAP_V3_START_BLOCK=0 +CHAIN_651940_HYDX_FACTORY=0x... +CHAIN_651940_HYDX_ROUTER=0x... +CHAIN_651940_HYDX_START_BLOCK=0 ``` +The canonical ALL Mainnet non-DODO inventory is also tracked in the parent repo at `config/allmainnet-non-dodo-protocol-surface.json`. + ## Monitoring ### Health Checks diff --git a/services/token-aggregation/src/adapters/dexscreener-adapter.test.ts b/services/token-aggregation/src/adapters/dexscreener-adapter.test.ts new file mode 100644 index 0000000..70e8259 --- /dev/null +++ b/services/token-aggregation/src/adapters/dexscreener-adapter.test.ts @@ -0,0 +1,99 @@ +import { + aggregateDexScreenerPairsToMarketData, + normalizeDexScreenerTokenPairsPayload, + type DexScreenerPair, +} from './dexscreener-adapter'; + +describe('normalizeDexScreenerTokenPairsPayload', () => { + it('accepts a raw JSON array (current API)', () => { + const pairs: DexScreenerPair[] = [ + { + chainId: 'ethereum', + dexId: 'x', + url: '', + pairAddress: '0x', + baseToken: { address: '0xa', name: '', symbol: 'A' }, + quoteToken: { address: '0xb', name: '', symbol: 'B' }, + priceUsd: '1', + }, + ]; + expect(normalizeDexScreenerTokenPairsPayload(pairs)).toEqual(pairs); + }); + + it('accepts legacy { pairs: [...] } shape', () => { + const inner: DexScreenerPair[] = [ + { + chainId: 'ethereum', + dexId: 'x', + url: '', + pairAddress: '0x', + baseToken: { address: '0xa', name: '', symbol: 'A' }, + quoteToken: { address: '0xb', name: '', symbol: 'B' }, + priceUsd: '2', + }, + ]; + expect(normalizeDexScreenerTokenPairsPayload({ pairs: inner })).toEqual(inner); + }); +}); + +describe('aggregateDexScreenerPairsToMarketData', () => { + it('returns null for empty input', () => { + expect(aggregateDexScreenerPairsToMarketData(null)).toBeNull(); + expect(aggregateDexScreenerPairsToMarketData([])).toBeNull(); + }); + + it('picks price from the pair with highest USD liquidity', () => { + const pairs: DexScreenerPair[] = [ + { + chainId: 'ethereum', + dexId: 'uniswap', + url: '', + pairAddress: '0x1', + baseToken: { address: '0xt', name: '', symbol: 'T' }, + quoteToken: { address: '0xq', name: '', symbol: 'Q' }, + priceUsd: '100', + liquidity: { usd: 1000 }, + }, + { + chainId: 'ethereum', + dexId: 'sushi', + url: '', + pairAddress: '0x2', + baseToken: { address: '0xt', name: '', symbol: 'T' }, + quoteToken: { address: '0xq', name: '', symbol: 'Q' }, + priceUsd: '1', + liquidity: { usd: 1_000_000 }, + }, + ]; + const m = aggregateDexScreenerPairsToMarketData(pairs); + expect(m?.priceUsd).toBe(1); + expect(m?.liquidityUsd).toBe(1_001_000); + }); + + it('when liquidity is missing, prefers higher 24h volume', () => { + const pairs: DexScreenerPair[] = [ + { + chainId: 'ethereum', + dexId: 'a', + url: '', + pairAddress: '0x1', + baseToken: { address: '0xt', name: '', symbol: 'T' }, + quoteToken: { address: '0xq', name: '', symbol: 'Q' }, + priceUsd: '50', + volume: { h24: 100 }, + }, + { + chainId: 'ethereum', + dexId: 'b', + url: '', + pairAddress: '0x2', + baseToken: { address: '0xt', name: '', symbol: 'T' }, + quoteToken: { address: '0xq', name: '', symbol: 'Q' }, + priceUsd: '51', + volume: { h24: 1_000_000 }, + }, + ]; + const m = aggregateDexScreenerPairsToMarketData(pairs); + expect(m?.priceUsd).toBe(51); + }); +}); diff --git a/services/token-aggregation/src/adapters/dexscreener-adapter.ts b/services/token-aggregation/src/adapters/dexscreener-adapter.ts index bfaaf12..86d4fa4 100644 --- a/services/token-aggregation/src/adapters/dexscreener-adapter.ts +++ b/services/token-aggregation/src/adapters/dexscreener-adapter.ts @@ -1,8 +1,9 @@ import axios, { AxiosInstance } from 'axios'; import { ExternalApiAdapter, TokenMetadata, MarketData } from './base-adapter'; +import { preferGruV2OfficialDexPairs } from '../config/gru-v2-deployment-pools'; import { logger } from '../utils/logger'; -interface DexScreenerPair { +export interface DexScreenerPair { chainId: string; dexId: string; url: string; @@ -59,11 +60,23 @@ interface DexScreenerPair { } interface DexScreenerResponse { - schemaVersion: string; - pairs: DexScreenerPair[] | null; + schemaVersion?: string; + pairs?: DexScreenerPair[] | null; pair?: DexScreenerPair; } +/** Current API returns a JSON array of pairs; older clients used `{ pairs: [...] }`. */ +export function normalizeDexScreenerTokenPairsPayload(data: unknown): DexScreenerPair[] { + if (Array.isArray(data)) { + return data as DexScreenerPair[]; + } + if (data && typeof data === 'object' && data !== null && 'pairs' in data) { + const p = (data as DexScreenerResponse).pairs; + return Array.isArray(p) ? p : []; + } + return []; +} + // Chain ID to DexScreener chain identifier mapping // DexScreener uses chain identifiers like 'ethereum', 'bsc', etc. const CHAIN_TO_DEXSCREENER_ID: Record = { @@ -74,7 +87,11 @@ const CHAIN_TO_DEXSCREENER_ID: Record = { 42161: 'arbitrum', 10: 'optimism', 8453: 'base', - // Note: 138 and 651940 are likely not supported + 100: 'gnosis', + 42220: 'celo', + 25: 'cronos', + 1111: 'wemix', + // Chain 138 / ALL Mainnet: not listed on public DexScreener API }; // Reverse mapping for lookup @@ -83,6 +100,55 @@ Object.entries(CHAIN_TO_DEXSCREENER_ID).forEach(([chainId, dexId]) => { DEXSCREENER_ID_TO_CHAIN[dexId] = parseInt(chainId, 10); }); +/** Prefer the pair with the most USD liquidity, then highest 24h volume (avoids averaging thin pools). */ +function pairLiquidityScore(pair: DexScreenerPair): number { + const liq = pair.liquidity?.usd ?? 0; + if (liq > 0) return liq; + return pair.volume?.h24 ?? 0; +} + +/** + * Aggregates DexScreener token-pairs response into a single {@link MarketData} snapshot. + * Exported for unit tests. + */ +export function aggregateDexScreenerPairsToMarketData(pairs: DexScreenerPair[] | null | undefined): MarketData | null { + if (!pairs || pairs.length === 0) { + return null; + } + + let totalVolume24h = 0; + let totalLiquidity = 0; + for (const pair of pairs) { + if (pair.volume?.h24) totalVolume24h += pair.volume.h24; + if (pair.liquidity?.usd) totalLiquidity += pair.liquidity.usd; + } + + const priced = pairs.filter((p) => p.priceUsd && !Number.isNaN(parseFloat(p.priceUsd))); + if (priced.length === 0) { + return null; + } + + let best = priced[0]!; + let bestScore = pairLiquidityScore(best); + for (let i = 1; i < priced.length; i++) { + const p = priced[i]!; + const s = pairLiquidityScore(p); + if (s > bestScore) { + best = p; + bestScore = s; + } + } + + const priceUsd = parseFloat(best.priceUsd!); + + return { + priceUsd: Number.isFinite(priceUsd) ? priceUsd : undefined, + volume24h: totalVolume24h > 0 ? totalVolume24h : undefined, + liquidityUsd: totalLiquidity > 0 ? totalLiquidity : undefined, + lastUpdated: new Date(), + }; +} + export class DexScreenerAdapter implements ExternalApiAdapter { private api: AxiosInstance; private apiKey?: string; @@ -131,11 +197,10 @@ export class DexScreenerAdapter implements ExternalApiAdapter { return false; } - const response = await this.api.get( - `/token-pairs/v1/${dexId}/${testAddress}` - ); + const response = await this.api.get(`/token-pairs/v1/${dexId}/${testAddress}`); - const supported = response.status === 200 && (response.data.pairs?.length ?? 0) > 0; + const pairs = normalizeDexScreenerTokenPairsPayload(response.data); + const supported = response.status === 200 && pairs.length > 0; this.cache.set(cacheKey, { data: supported, expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hour cache @@ -181,39 +246,17 @@ export class DexScreenerAdapter implements ExternalApiAdapter { } try { - const response = await this.api.get( - `/token-pairs/v1/${dexId}/${address.toLowerCase()}` - ); + const response = await this.api.get(`/token-pairs/v1/${dexId}/${address.toLowerCase()}`); - if (!response.data.pairs || response.data.pairs.length === 0) { + const pairsRaw = normalizeDexScreenerTokenPairsPayload(response.data); + if (pairsRaw.length === 0) { + return null; + } + const pairs = preferGruV2OfficialDexPairs(chainId, address.toLowerCase(), pairsRaw); + const marketData = aggregateDexScreenerPairsToMarketData(pairs); + if (!marketData) { return null; } - - // Aggregate data from all pairs - let totalVolume24h = 0; - let totalLiquidity = 0; - let avgPrice = 0; - let priceCount = 0; - response.data.pairs.forEach((pair) => { - if (pair.priceUsd) { - avgPrice += parseFloat(pair.priceUsd); - priceCount++; - } - if (pair.volume?.h24) { - totalVolume24h += pair.volume.h24; - } - if (pair.liquidity?.usd) { - totalLiquidity += pair.liquidity.usd; - } - // txns h24 available on pair.txns?.h24 for future use - }); - - const marketData: MarketData = { - priceUsd: priceCount > 0 ? avgPrice / priceCount : undefined, - volume24h: totalVolume24h > 0 ? totalVolume24h : undefined, - liquidityUsd: totalLiquidity > 0 ? totalLiquidity : undefined, - lastUpdated: new Date(), - }; // Cache for 2 minutes (DexScreener updates frequently) this.cache.set(cacheKey, { @@ -242,11 +285,10 @@ export class DexScreenerAdapter implements ExternalApiAdapter { } try { - const response = await this.api.get( - `/token-pairs/v1/${dexId}/${tokenAddress.toLowerCase()}` - ); + const response = await this.api.get(`/token-pairs/v1/${dexId}/${tokenAddress.toLowerCase()}`); - return response.data.pairs || []; + const raw = normalizeDexScreenerTokenPairsPayload(response.data); + return preferGruV2OfficialDexPairs(chainId, tokenAddress.toLowerCase(), raw); } catch (error) { logger.error(`Error fetching DexScreener pairs for ${tokenAddress} on chain ${chainId}:`, error); return []; @@ -263,11 +305,11 @@ export class DexScreenerAdapter implements ExternalApiAdapter { } try { - const response = await this.api.get( + const response = await this.api.get<{ pair?: DexScreenerPair }>( `/latest/dex/pairs/${dexId}/${pairAddress.toLowerCase()}` ); - return response.data.pair || null; + return response.data.pair ?? null; } catch (error: unknown) { const err = error as { response?: { status?: number } }; if (err.response?.status === 404) { @@ -300,13 +342,13 @@ export class DexScreenerAdapter implements ExternalApiAdapter { for (const chunk of chunks) { try { - const response = await this.api.get( + const response = await this.api.get( `/tokens/v1/${dexId}/${chunk.map((addr) => addr.toLowerCase()).join(',')}` ); - if (response.data.pairs) { - // Group pairs by token address - response.data.pairs.forEach((pair) => { + const batchPairs = normalizeDexScreenerTokenPairsPayload(response.data); + if (batchPairs.length > 0) { + batchPairs.forEach((pair) => { const baseAddr = pair.baseToken.address.toLowerCase(); const quoteAddr = pair.quoteToken.address.toLowerCase(); diff --git a/services/token-aggregation/src/api/routes/report.test.ts b/services/token-aggregation/src/api/routes/report.test.ts index f519577..ef70e3c 100644 --- a/services/token-aggregation/src/api/routes/report.test.ts +++ b/services/token-aggregation/src/api/routes/report.test.ts @@ -6,6 +6,7 @@ import { createServer } from 'http'; import express from 'express'; import reportRoutes from './report'; +import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens'; jest.mock('../../database/repositories/token-repo', () => ({ TokenRepository: jest.fn().mockImplementation(() => ({ @@ -124,6 +125,30 @@ describe('Report API', () => { ]) ); }); + + it('fills canonical fallback usd pricing when market data is absent', async () => { + const weth = getCanonicalTokenBySymbol(138, 'WETH'); + expect(weth?.addresses[138]).toBeTruthy(); + const wethAddress = String(weth?.addresses[138]).toLowerCase(); + + const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=138`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + const tokens138 = body.tokens?.['138']; + expect(Array.isArray(tokens138)).toBe(true); + + const wethEntry = tokens138.find((token: Record) => token.address === wethAddress); + expect(wethEntry).toMatchObject({ + symbol: 'WETH', + decimals: 18, + market: expect.objectContaining({ + priceUsd: 2490, + volume24h: 0, + liquidityUsd: 0, + lastUpdated: '2026-04-15T00:00:00.000Z', + }), + }); + }); }); describe('GET /api/v1/report/gas-registry', () => { @@ -413,6 +438,68 @@ describe('Report API', () => { }); }); + describe('GET /api/v1/report/gru-v2-pmm-pools', () => { + it('returns resolved PMM pools from deployment-status when file is set', async () => { + const previousPath = process.env.DEPLOYMENT_STATUS_JSON_PATH; + const tempPath = `/tmp/token-aggregation-gru-v2-pmm-${Date.now()}.json`; + + process.env.DEPLOYMENT_STATUS_JSON_PATH = tempPath; + await import('fs/promises').then((fs) => + fs.writeFile( + tempPath, + JSON.stringify( + { + version: 'test-gru-pools', + updated: '2026-04-18', + homeChainId: 138, + chains: { + '1': { + name: 'Ethereum Mainnet', + cwTokens: { cWUSDT: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae' }, + anchorAddresses: { USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' }, + pmmPools: [ + { + base: 'cWUSDT', + quote: 'USDC', + poolAddress: '0x1111111111111111111111111111111111111111', + feeBps: 3, + role: 'public_routing', + publicRoutingEnabled: true, + }, + ], + }, + }, + }, + null, + 2 + ) + ) + ); + + try { + const res = await fetch(`${baseUrl}/api/v1/report/gru-v2-pmm-pools?chainId=1`); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + expect(body.source).toBe('deployment-status-file'); + expect(body.complete).toBe(true); + expect(body.version).toBe('test-gru-pools'); + expect(Array.isArray(body.pools)).toBe(true); + expect((body.pools as unknown[]).length).toBeGreaterThanOrEqual(1); + expect((body.pools as Array<{ poolAddress: string }>)[0]).toMatchObject({ + poolAddress: '0x1111111111111111111111111111111111111111', + section: 'pmmPools', + }); + } finally { + await import('fs/promises').then((fs) => fs.unlink(tempPath).catch(() => undefined)); + if (previousPath === undefined) { + delete process.env.DEPLOYMENT_STATUS_JSON_PATH; + } else { + process.env.DEPLOYMENT_STATUS_JSON_PATH = previousPath; + } + } + }); + }); + describe('GET /api/v1/report/gas-registry', () => { it('reads the live gas rollout registry from deployment-status json when available', async () => { const res = await fetch(`${baseUrl}/api/v1/report/gas-registry?chainId=10`); diff --git a/services/token-aggregation/src/api/routes/report.ts b/services/token-aggregation/src/api/routes/report.ts index 9e6b238..4f06c95 100644 --- a/services/token-aggregation/src/api/routes/report.ts +++ b/services/token-aggregation/src/api/routes/report.ts @@ -26,6 +26,8 @@ import { loadDeploymentStatusFile, type CwRegistryChain, } from '../../config/deployment-status'; +import { getGruV2DeploymentPoolRows } from '../../config/gru-v2-deployment-pools'; +import { getCanonicalPriceSnapshotGeneratedAt, getCanonicalPriceUsd } from '../../services/canonical-price-oracle'; const router: Router = Router(); const tokenRepo = new TokenRepository(); @@ -94,6 +96,8 @@ async function buildTokenReport(chainId: number) { }) ); + const fallbackPriceUsd = getCanonicalPriceUsd(chainId, address); + out.push({ chainId, address: address.toLowerCase(), @@ -110,7 +114,7 @@ async function buildTokenReport(chainId: number) { liquiditySourceSymbol: spec.liquiditySourceSymbol, market: marketData ? { - priceUsd: marketData.priceUsd, + priceUsd: marketData.priceUsd ?? fallbackPriceUsd, volume24h: marketData.volume24h, volume7d: marketData.volume7d, volume30d: marketData.volume30d, @@ -118,6 +122,15 @@ async function buildTokenReport(chainId: number) { liquidityUsd: marketData.liquidityUsd, lastUpdated: marketData.lastUpdated?.toISOString() ?? '', } + : fallbackPriceUsd !== undefined + ? { + priceUsd: fallbackPriceUsd, + volume24h: 0, + volume7d: 0, + volume30d: 0, + liquidityUsd: 0, + lastUpdated: `${getCanonicalPriceSnapshotGeneratedAt()}T00:00:00.000Z`, + } : undefined, pools: resolvedPools.map((p) => ({ poolAddress: p.poolAddress, @@ -543,6 +556,36 @@ router.get('/cw-registry', async (req: Request, res: Response) => { } }); +/** GET /report/gru-v2-pmm-pools — all GRU v2 PMM pools from deployment-status (stable, volatile, gas) with resolved token addresses. */ +router.get('/gru-v2-pmm-pools', async (req: Request, res: Response) => { + try { + const chainIdParam = req.query.chainId as string | undefined; + const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null; + const fileBacked = loadDeploymentStatusFile(); + let pools = getGruV2DeploymentPoolRows(); + + if (chainIdFilter && !Number.isNaN(chainIdFilter)) { + pools = pools.filter((p) => p.chainId === chainIdFilter); + } + + res.set('Cache-Control', 'public, max-age=0, must-revalidate'); + res.json({ + generatedAt: new Date().toISOString(), + source: fileBacked ? 'deployment-status-file' : 'none', + complete: !!fileBacked, + version: fileBacked?.data.version, + updated: fileBacked?.data.updated, + lastModified: fileBacked?.lastModified, + homeChainId: fileBacked?.data.homeChainId, + count: pools.length, + pools, + }); + } catch (error) { + logger.error('Error building report/gru-v2-pmm-pools:', error); + res.status(500).json({ error: 'Internal server error', pools: [] }); + } +}); + /** GET /report/gas-registry — live gas-family rollout registry from deployment-status.json plus GRU transport metadata. */ router.get('/gas-registry', async (req: Request, res: Response) => { try { diff --git a/services/token-aggregation/src/api/routes/tokens.test.ts b/services/token-aggregation/src/api/routes/tokens.test.ts new file mode 100644 index 0000000..1d25514 --- /dev/null +++ b/services/token-aggregation/src/api/routes/tokens.test.ts @@ -0,0 +1,219 @@ +import { createServer } from 'http'; +import express from 'express'; +import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens'; + +const mockGetTokens = jest.fn(); +const mockGetToken = jest.fn(); +const mockSearchTokens = jest.fn(); +const mockGetMarketData = jest.fn(); +const mockGetPoolsByToken = jest.fn(); +const mockGetPool = jest.fn(); +const mockGetLiveDodoPools = jest.fn(); +const mockResolveTokenDisplay = jest.fn(); +const mockResolvePoolTokenDisplays = jest.fn(); +const mockGetTokenByContract = jest.fn(); + +jest.mock('../../database/repositories/token-repo', () => ({ + TokenRepository: jest.fn().mockImplementation(() => ({ + getTokens: mockGetTokens, + getToken: mockGetToken, + searchTokens: mockSearchTokens, + })), +})); + +jest.mock('../../database/repositories/market-data-repo', () => ({ + MarketDataRepository: jest.fn().mockImplementation(() => ({ + getMarketData: mockGetMarketData, + })), +})); + +jest.mock('../../database/repositories/pool-repo', () => ({ + PoolRepository: jest.fn().mockImplementation(() => ({ + getPoolsByToken: mockGetPoolsByToken, + getPool: mockGetPool, + })), +})); + +jest.mock('../../indexer/ohlcv-generator', () => ({ + OHLCVGenerator: jest.fn().mockImplementation(() => ({ + getOHLCV: jest.fn().mockResolvedValue([]), + })), +})); + +const mockGetMarketDataAdapter = jest.fn(); + +jest.mock('../../adapters/coingecko-adapter', () => ({ + CoinGeckoAdapter: jest.fn().mockImplementation(() => ({ + getTokenByContract: mockGetTokenByContract, + getMarketData: mockGetMarketDataAdapter, + getTrending: jest.fn().mockResolvedValue([]), + })), +})); + +jest.mock('../../adapters/cmc-adapter', () => ({ + CoinMarketCapAdapter: jest.fn().mockImplementation(() => ({ + getTokenByContract: mockGetTokenByContract, + getMarketData: mockGetMarketDataAdapter, + })), +})); + +jest.mock('../../adapters/dexscreener-adapter', () => ({ + DexScreenerAdapter: jest.fn().mockImplementation(() => ({ + getTokenByContract: mockGetTokenByContract, + getMarketData: mockGetMarketDataAdapter, + })), +})); + +jest.mock('../../services/live-dodo-fallback', () => ({ + getLiveDodoPools: (...args: unknown[]) => mockGetLiveDodoPools(...args), +})); + +jest.mock('../../services/token-display', () => ({ + resolveTokenDisplay: (...args: unknown[]) => mockResolveTokenDisplay(...args), + resolvePoolTokenDisplays: (...args: unknown[]) => mockResolvePoolTokenDisplays(...args), +})); + +jest.mock('../middleware/cache'); + +const tokensRoutes = require('./tokens').default as typeof import('./tokens').default; + +function createApp() { + const app = express(); + app.use('/api/v1', tokensRoutes); + return app; +} + +async function startServer(app: express.Application): Promise<{ server: ReturnType; baseUrl: string }> { + const server = createServer(app); + await new Promise((resolve) => server.listen(0, () => resolve())); + const port = (server.address() as { port: number }).port; + return { server, baseUrl: `http://127.0.0.1:${port}` }; +} + +describe('Tokens API', () => { + let server: ReturnType; + let baseUrl: string; + + beforeAll(async () => { + const app = createApp(); + const started = await startServer(app); + server = started.server; + baseUrl = started.baseUrl; + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockGetMarketDataAdapter.mockResolvedValue(null); + mockGetTokens.mockResolvedValue([]); + mockGetToken.mockResolvedValue(null); + mockSearchTokens.mockResolvedValue([]); + mockGetMarketData.mockResolvedValue(null); + mockGetPoolsByToken.mockResolvedValue([]); + mockGetPool.mockResolvedValue(null); + mockGetLiveDodoPools.mockResolvedValue([]); + mockResolveTokenDisplay.mockResolvedValue({ + address: '', + name: 'Unknown Token', + symbol: 'UNKNOWN', + decimals: 18, + source: 'fallback', + }); + mockResolvePoolTokenDisplays.mockResolvedValue({ + token0: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' }, + token1: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' }, + }); + mockGetTokenByContract.mockResolvedValue(null); + }); + + afterAll(async () => { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + }); + + it('lists canonical 138 tokens with stable and ETH-family fallback pricing when db market data is missing', async () => { + const usdt = getCanonicalTokenBySymbol(138, 'USDT'); + const weth = getCanonicalTokenBySymbol(138, 'WETH'); + const weth10 = getCanonicalTokenBySymbol(138, 'WETH10'); + + expect(usdt?.addresses[138]).toBeTruthy(); + expect(weth?.addresses[138]).toBeTruthy(); + expect(weth10?.addresses[138]).toBeTruthy(); + + const res = await fetch(`${baseUrl}/api/v1/tokens?chainId=138&limit=400`); + expect(res.status).toBe(200); + + const body = (await res.json()) as Record; + expect(body.source).toBe('canonical'); + + const findByAddress = (address?: string) => + body.tokens.find((token: Record) => token.address === address?.toLowerCase()); + + expect(findByAddress(usdt?.addresses[138])).toMatchObject({ + symbol: 'USDT', + decimals: 6, + market: expect.objectContaining({ + priceUsd: 1, + volume24h: 0, + liquidityUsd: 0, + }), + }); + + expect(findByAddress(weth?.addresses[138])).toMatchObject({ + symbol: 'WETH', + decimals: 18, + market: expect.objectContaining({ + priceUsd: 2490, + }), + }); + + expect(findByAddress(weth10?.addresses[138])).toMatchObject({ + symbol: 'WETH10', + decimals: 18, + market: expect.objectContaining({ + priceUsd: 2490, + }), + }); + }); + + it('fills missing priceUsd on token detail responses while preserving repository market fields', async () => { + const weth10 = getCanonicalTokenBySymbol(138, 'WETH10'); + expect(weth10?.addresses[138]).toBeTruthy(); + const weth10Address = String(weth10?.addresses[138]).toLowerCase(); + + mockGetMarketData.mockResolvedValue({ + chainId: 138, + tokenAddress: weth10Address, + priceUsd: undefined, + volume24h: 1234, + volume7d: 5678, + volume30d: 9012, + liquidityUsd: 3456, + holdersCount: 78, + transfers24h: 9, + lastUpdated: new Date('2026-04-16T00:00:00.000Z'), + }); + + const res = await fetch(`${baseUrl}/api/v1/tokens/${weth10Address}?chainId=138`); + expect(res.status).toBe(200); + + const body = (await res.json()) as Record; + expect(body.token).toMatchObject({ + symbol: 'WETH10', + decimals: 18, + market: expect.objectContaining({ + priceUsd: 2490, + volume24h: 1234, + liquidityUsd: 3456, + }), + hasDodoPool: false, + }); + expect(body.token.canonicalLiquidity).toBeUndefined(); + }); +}); diff --git a/services/token-aggregation/src/api/routes/tokens.ts b/services/token-aggregation/src/api/routes/tokens.ts index c3f76a7..6fac348 100644 --- a/services/token-aggregation/src/api/routes/tokens.ts +++ b/services/token-aggregation/src/api/routes/tokens.ts @@ -16,6 +16,11 @@ import { resolveCanonicalQuoteAddress, } from '../../config/canonical-tokens'; import { getLiveDodoPools } from '../../services/live-dodo-fallback'; +import { + buildExplorerLinks, + mergeMarketWithValuation, + resolveUsdValuation, +} from '../../services/valuation-precedence'; const router: Router = Router(); const tokenRepo = new TokenRepository(); @@ -26,6 +31,26 @@ const coingeckoAdapter = new CoinGeckoAdapter(); const cmcAdapter = new CoinMarketCapAdapter(); const dexscreenerAdapter = new DexScreenerAdapter(); +function buildMarketPricingExplorer( + chainId: number, + displayAddress: string, + lookupAddress: string, + marketData: Awaited>, + external: { coingecko?: Awaited>; cmc?: Awaited>; dexscreener?: Awaited> } | null +) { + const pricing = resolveUsdValuation({ + chainId, + normalizedAddress: lookupAddress.toLowerCase(), + indexer: marketData, + coingecko: external?.coingecko ?? undefined, + cmc: external?.cmc ?? undefined, + dexscreener: external?.dexscreener ?? undefined, + }); + const market = mergeMarketWithValuation(chainId, displayAddress.toLowerCase(), marketData, pricing); + const explorer = buildExplorerLinks(chainId, displayAddress); + return { market, pricing, explorer }; +} + function tokenFromCanonical(chainId: number, address: string): Token | null { const spec = getCanonicalTokenByAddress(chainId, address.toLowerCase()); if (!spec) { @@ -182,10 +207,20 @@ router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Resp const { tokens, source } = await getTokensWithFallback(chainId, limit, offset); const tokensWithMarketData = await Promise.all( tokens.map(async (token) => { - const marketData = await marketDataRepo.getMarketData(chainId, token.address); + const resolution = resolveCanonicalQuoteAddress(chainId, token.address); + const marketData = await marketDataRepo.getMarketData(chainId, resolution.lookupAddress); + const { market, pricing, explorer } = buildMarketPricingExplorer( + chainId, + token.address, + resolution.lookupAddress, + marketData, + null + ); const out: Record = { ...token, - market: marketData || undefined, + market: market || undefined, + pricing, + explorer, }; if (includeDodoPool) { const pools = await getPoolsByTokenWithFallback(chainId, token.address); @@ -228,13 +263,32 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request, return res.status(404).json({ error: 'Token not found' }); } - const [marketData, pools, coingeckoData, cmcData, dexscreenerData] = await Promise.all([ + const [ + marketDataRaw, + pools, + coingeckoData, + cmcData, + dexscreenerData, + coingeckoMarket, + cmcMarket, + dexscreenerMarket, + ] = await Promise.all([ marketDataRepo.getMarketData(chainId, resolution.lookupAddress), getPoolsByTokenWithFallback(chainId, normalizedAddress), coingeckoAdapter.getTokenByContract(chainId, resolution.lookupAddress), cmcAdapter.getTokenByContract(chainId, resolution.lookupAddress), dexscreenerAdapter.getTokenByContract(chainId, resolution.lookupAddress), + coingeckoAdapter.getMarketData(chainId, resolution.lookupAddress), + cmcAdapter.getMarketData(chainId, resolution.lookupAddress), + dexscreenerAdapter.getMarketData(chainId, resolution.lookupAddress), ]); + const { market: marketData, pricing, explorer } = buildMarketPricingExplorer( + chainId, + normalizedAddress, + resolution.lookupAddress, + marketDataRaw, + { coingecko: coingeckoMarket, cmc: cmcMarket, dexscreener: dexscreenerMarket } + ); res.json({ token: { @@ -243,6 +297,8 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request, totalSupply: token.totalSupply, }, market: marketData || undefined, + pricing, + explorer, external: { coingecko: coingeckoData || undefined, cmc: cmcData || undefined, diff --git a/services/token-aggregation/src/config/canonical-tokens.test.ts b/services/token-aggregation/src/config/canonical-tokens.test.ts index 8b13402..60f8c59 100644 --- a/services/token-aggregation/src/config/canonical-tokens.test.ts +++ b/services/token-aggregation/src/config/canonical-tokens.test.ts @@ -115,6 +115,26 @@ describe('canonical cW token catalog', () => { expect(cwethL2?.addresses[10]).toBe('0xce7200000000000000000000000000000000000a'); expect(getCanonicalTokenByAddress(10, '0xce7200000000000000000000000000000000000a')?.symbol).toBe('cWETHL2'); expect(getTokenRegistryFamily(cwethL2!)).toBe('gas_native'); + + const weth = getCanonicalTokenBySymbol(138, 'WETH'); + expect(weth).toMatchObject({ + symbol: 'WETH', + type: 'w', + currencyCode: 'ETH', + decimals: 18, + }); + expect(weth?.addresses[138]).toBe('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); + expect(getCanonicalTokenByAddress(138, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')?.symbol).toBe('WETH'); + + const weth10 = getCanonicalTokenBySymbol(138, 'WETH10'); + expect(weth10).toMatchObject({ + symbol: 'WETH10', + type: 'w', + currencyCode: 'ETH', + decimals: 18, + }); + expect(weth10?.addresses[138]).toBe('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f'); + expect(getCanonicalTokenByAddress(138, '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')?.symbol).toBe('WETH10'); }); it('surfaces cAUSDT on Chain 138 from env and keeps cWAUSDT fallback mirrors on active public chains', () => { diff --git a/services/token-aggregation/src/config/canonical-tokens.ts b/services/token-aggregation/src/config/canonical-tokens.ts index cf73245..61796d1 100644 --- a/services/token-aggregation/src/config/canonical-tokens.ts +++ b/services/token-aggregation/src/config/canonical-tokens.ts @@ -247,6 +247,8 @@ const FALLBACK_ADDRESSES: Record>> = { cCADC: { [CHAIN_138]: '0x54dBd40cF05e15906A2C21f600937e96787f5679' }, cXAUC: { [CHAIN_138]: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b' }, cXAUT: { [CHAIN_138]: '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E' }, + WETH: { [CHAIN_138]: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' }, + WETH10: { [CHAIN_138]: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f' }, // ISO-4217W on Cronos (25) — from DeployISO4217WSystem USDW: { [CHAIN_25]: '0x948690147D2e50ffe50C5d38C14125aD6a9FA036' }, EURW: { [CHAIN_25]: '0x58a8D8F78F1B65c06dAd7542eC46b299629A60dd' }, @@ -450,6 +452,26 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [ { 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: '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', + name: 'Wrapped Ether (WETH9)', + type: 'w', + decimals: 18, + currencyCode: 'ETH', + registryFamily: 'gas_native', + description: 'Legacy WETH9 surface used on Chain 138 for canonical ETH swap routing and CCIP WETH9 bridge lanes.', + addresses: { [CHAIN_138]: addr('WETH', CHAIN_138) || '' }, + }, + { + symbol: 'WETH10', + name: 'Wrapped Ether 10', + type: 'w', + decimals: 18, + currencyCode: 'ETH', + registryFamily: 'gas_native', + 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: 'cWBTC', name: 'Bitcoin (Compliant Wrapped Monetary Unit)', @@ -728,6 +750,8 @@ const LOGO_BY_SYMBOL: Record = { cWUSDC: USDC_LOGO, cWUSDT: USDT_LOGO, cWUSDW: USDC_LOGO, + WETH: ETH_LOGO, + WETH10: ETH_LOGO, cEURC: `${GRU_LOGO_BASE}/cEURC.svg`, cEURT: `${GRU_LOGO_BASE}/cEURT.svg`, cGBPC: `${GRU_LOGO_BASE}/cGBPC.svg`, diff --git a/services/token-aggregation/src/config/dex-factories.ts b/services/token-aggregation/src/config/dex-factories.ts index 8d41a82..55bb618 100644 --- a/services/token-aggregation/src/config/dex-factories.ts +++ b/services/token-aggregation/src/config/dex-factories.ts @@ -1,4 +1,4 @@ -export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom'; +export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'sushiswap' | 'dodo' | 'custom'; export interface UniswapV2Config { factory: string; @@ -30,6 +30,7 @@ export interface CustomDexConfig { export interface DexFactoryConfig { uniswap_v2?: UniswapV2Config[]; uniswap_v3?: UniswapV3Config[]; + sushiswap?: UniswapV2Config[]; dodo?: DodoConfig[]; custom?: CustomDexConfig[]; } @@ -38,6 +39,35 @@ export interface DexFactoryConfig { const CANONICAL_CHAIN138_DODO_PMM_INTEGRATION = '0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895'; +function getUniswapV2Config(chainId: number): UniswapV2Config[] | undefined { + const factory = process.env[`CHAIN_${chainId}_UNISWAP_V2_FACTORY`]; + if (!factory) return undefined; + + return [ + { + factory, + router: process.env[`CHAIN_${chainId}_UNISWAP_V2_ROUTER`] || '', + startBlock: parseInt(process.env[`CHAIN_${chainId}_UNISWAP_V2_START_BLOCK`] || '0', 10), + }, + ]; +} + +function getDodoConfig(chainId: number): DodoConfig[] | undefined { + const poolManager = process.env[`CHAIN_${chainId}_DODO_POOL_MANAGER`] || ''; + const dodoPmmIntegration = process.env[`CHAIN_${chainId}_DODO_PMM_INTEGRATION`] || ''; + + if (!poolManager && !dodoPmmIntegration) return undefined; + + return [ + { + poolManager, + dodoPmmIntegration, + dodoVendingMachine: process.env[`CHAIN_${chainId}_DODO_VENDING_MACHINE`] || '', + startBlock: parseInt(process.env[`CHAIN_${chainId}_DODO_START_BLOCK`] || '0', 10), + }, + ]; +} + export const DEX_FACTORIES: Record = { 138: { // DODO PMM Integration - index from DODOPMMIntegration or PoolManager @@ -51,12 +81,13 @@ export const DEX_FACTORIES: Record = { }, ], // UniswapV2 - if deployed - uniswap_v2: process.env.CHAIN_138_UNISWAP_V2_FACTORY + uniswap_v2: getUniswapV2Config(138), + sushiswap: process.env.CHAIN_138_SUSHISWAP_FACTORY ? [ { - factory: process.env.CHAIN_138_UNISWAP_V2_FACTORY, - router: process.env.CHAIN_138_UNISWAP_V2_ROUTER || '', - startBlock: parseInt(process.env.CHAIN_138_UNISWAP_V2_START_BLOCK || '0', 10), + factory: process.env.CHAIN_138_SUSHISWAP_FACTORY, + router: process.env.CHAIN_138_SUSHISWAP_ROUTER || '', + startBlock: parseInt(process.env.CHAIN_138_SUSHISWAP_START_BLOCK || '0', 10), }, ] : undefined, @@ -74,15 +105,7 @@ export const DEX_FACTORIES: Record = { 651940: { // ALL Mainnet - DEX factories to be discovered/configured // These can be set via environment variables or discovered on-chain - uniswap_v2: process.env.CHAIN_651940_UNISWAP_V2_FACTORY - ? [ - { - factory: process.env.CHAIN_651940_UNISWAP_V2_FACTORY, - router: process.env.CHAIN_651940_UNISWAP_V2_ROUTER || '', - startBlock: parseInt(process.env.CHAIN_651940_UNISWAP_V2_START_BLOCK || '0', 10), - }, - ] - : undefined, + uniswap_v2: getUniswapV2Config(651940), uniswap_v3: process.env.CHAIN_651940_UNISWAP_V3_FACTORY ? [ { @@ -101,72 +124,61 @@ export const DEX_FACTORIES: Record = { }, ] : undefined, + custom: process.env.CHAIN_651940_HYDX_FACTORY + ? [ + { + factory: process.env.CHAIN_651940_HYDX_FACTORY, + router: process.env.CHAIN_651940_HYDX_ROUTER || '', + startBlock: parseInt(process.env.CHAIN_651940_HYDX_START_BLOCK || '0', 10), + pairCreatedEvent: process.env.CHAIN_651940_HYDX_PAIR_CREATED_EVENT || '', + }, + ] + : undefined, }, // cW* edge chains (1, 10, 56, 100, 137): set CHAIN_*_DODO_PMM_INTEGRATION or CHAIN_*_DODO_POOL_MANAGER to index DODO/pools 1: { - dodo: - process.env.CHAIN_1_DODO_PMM_INTEGRATION || process.env.CHAIN_1_DODO_POOL_MANAGER - ? [ - { - poolManager: process.env.CHAIN_1_DODO_POOL_MANAGER || '', - dodoPmmIntegration: process.env.CHAIN_1_DODO_PMM_INTEGRATION || '', - dodoVendingMachine: process.env.CHAIN_1_DODO_VENDING_MACHINE || '', - startBlock: parseInt(process.env.CHAIN_1_DODO_START_BLOCK || '0', 10), - }, - ] - : undefined, + uniswap_v2: getUniswapV2Config(1), + dodo: getDodoConfig(1), }, 10: { - dodo: - process.env.CHAIN_10_DODO_PMM_INTEGRATION || process.env.CHAIN_10_DODO_POOL_MANAGER - ? [ - { - poolManager: process.env.CHAIN_10_DODO_POOL_MANAGER || '', - dodoPmmIntegration: process.env.CHAIN_10_DODO_PMM_INTEGRATION || '', - dodoVendingMachine: process.env.CHAIN_10_DODO_VENDING_MACHINE || '', - startBlock: parseInt(process.env.CHAIN_10_DODO_START_BLOCK || '0', 10), - }, - ] - : undefined, + uniswap_v2: getUniswapV2Config(10), + dodo: getDodoConfig(10), + }, + 25: { + uniswap_v2: getUniswapV2Config(25), + dodo: getDodoConfig(25), }, 56: { - dodo: - process.env.CHAIN_56_DODO_PMM_INTEGRATION || process.env.CHAIN_56_DODO_POOL_MANAGER - ? [ - { - poolManager: process.env.CHAIN_56_DODO_POOL_MANAGER || '', - dodoPmmIntegration: process.env.CHAIN_56_DODO_PMM_INTEGRATION || '', - dodoVendingMachine: process.env.CHAIN_56_DODO_VENDING_MACHINE || '', - startBlock: parseInt(process.env.CHAIN_56_DODO_START_BLOCK || '0', 10), - }, - ] - : undefined, + uniswap_v2: getUniswapV2Config(56), + dodo: getDodoConfig(56), }, 100: { - dodo: - process.env.CHAIN_100_DODO_PMM_INTEGRATION || process.env.CHAIN_100_DODO_POOL_MANAGER - ? [ - { - poolManager: process.env.CHAIN_100_DODO_POOL_MANAGER || '', - dodoPmmIntegration: process.env.CHAIN_100_DODO_PMM_INTEGRATION || '', - dodoVendingMachine: process.env.CHAIN_100_DODO_VENDING_MACHINE || '', - startBlock: parseInt(process.env.CHAIN_100_DODO_START_BLOCK || '0', 10), - }, - ] - : undefined, + uniswap_v2: getUniswapV2Config(100), + dodo: getDodoConfig(100), }, 137: { - dodo: - process.env.CHAIN_137_DODO_PMM_INTEGRATION || process.env.CHAIN_137_DODO_POOL_MANAGER - ? [ - { - poolManager: process.env.CHAIN_137_DODO_POOL_MANAGER || '', - dodoPmmIntegration: process.env.CHAIN_137_DODO_PMM_INTEGRATION || '', - dodoVendingMachine: process.env.CHAIN_137_DODO_VENDING_MACHINE || '', - startBlock: parseInt(process.env.CHAIN_137_DODO_START_BLOCK || '0', 10), - }, - ] - : undefined, + uniswap_v2: getUniswapV2Config(137), + dodo: getDodoConfig(137), + }, + 8453: { + uniswap_v2: getUniswapV2Config(8453), + dodo: getDodoConfig(8453), + }, + 42161: { + uniswap_v2: getUniswapV2Config(42161), + dodo: getDodoConfig(42161), + }, + 42220: { + uniswap_v2: getUniswapV2Config(42220), + dodo: getDodoConfig(42220), + }, + 43114: { + uniswap_v2: getUniswapV2Config(43114), + dodo: getDodoConfig(43114), + }, + 1111: { + uniswap_v2: getUniswapV2Config(1111), + dodo: getDodoConfig(1111), }, }; @@ -189,6 +201,8 @@ export function hasDexType(chainId: number, dexType: DexType): boolean { return !!config.uniswap_v2 && config.uniswap_v2.length > 0; case 'uniswap_v3': return !!config.uniswap_v3 && config.uniswap_v3.length > 0; + case 'sushiswap': + return !!config.sushiswap && config.sushiswap.length > 0; case 'dodo': return !!config.dodo && config.dodo.length > 0; case 'custom': @@ -208,6 +222,7 @@ export function getConfiguredDexTypes(chainId: number): DexType[] { const types: DexType[] = []; if (hasDexType(chainId, 'uniswap_v2')) types.push('uniswap_v2'); if (hasDexType(chainId, 'uniswap_v3')) types.push('uniswap_v3'); + if (hasDexType(chainId, 'sushiswap')) types.push('sushiswap'); if (hasDexType(chainId, 'dodo')) types.push('dodo'); if (hasDexType(chainId, 'custom')) types.push('custom'); diff --git a/services/token-aggregation/src/config/gru-v2-deployment-pools.test.ts b/services/token-aggregation/src/config/gru-v2-deployment-pools.test.ts new file mode 100644 index 0000000..842f707 --- /dev/null +++ b/services/token-aggregation/src/config/gru-v2-deployment-pools.test.ts @@ -0,0 +1,83 @@ +import type { DeploymentStatusFile } from './deployment-status'; +import { + buildGruV2PoolRegistryFromDeploymentData, + preferGruV2OfficialDexPairs, + resolveDeploymentTokenAddress, +} from './gru-v2-deployment-pools'; + +describe('gru-v2-deployment-pools', () => { + it('resolveDeploymentTokenAddress checks cwTokens, gasMirrors, anchors, gasQuoteAddresses', () => { + const chain = { + cwTokens: { cWUSDT: '0xcc' }, + anchorAddresses: { USDC: '0xaa' }, + gasQuoteAddresses: { WETH: '0xee' }, + }; + expect(resolveDeploymentTokenAddress(chain, 'cWUSDT')).toBe('0xcc'); + expect(resolveDeploymentTokenAddress(chain, 'USDC')).toBe('0xaa'); + expect(resolveDeploymentTokenAddress(chain, 'WETH')).toBe('0xee'); + expect(resolveDeploymentTokenAddress(chain, 'MISSING')).toBeNull(); + }); + + it('buildGruV2PoolRegistryFromDeploymentData merges pmmPools, pmmPoolsVolatile, and gasPmmPools', () => { + const data = { + chains: { + '1': { + name: 'Ethereum Mainnet', + cwTokens: { cWUSDT: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae' }, + anchorAddresses: { + USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + TRUU: '0xdae0fafd65385e7775cf75b1398735155ef6acd2', + }, + pmmPools: [ + { + base: 'cWUSDT', + quote: 'USDC', + poolAddress: '0x1111111111111111111111111111111111111111', + feeBps: 3, + role: 'public_routing', + publicRoutingEnabled: true, + }, + ], + pmmPoolsVolatile: [ + { + base: 'cWUSDT', + quote: 'TRUU', + poolAddress: '0x2222222222222222222222222222222222222222', + feeBps: 30, + role: 'truu_routing', + }, + ], + gasMirrors: { cWETH: '0xf6dc5587e18f27adff60e303fdd98f35b50fa8a5' }, + gasQuoteAddresses: { + WETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + }, + gasPmmPools: [ + { + familyKey: 'eth_mainnet', + base: 'cWETH', + quote: 'USDC', + poolAddress: '0x3333333333333333333333333333333333333333', + feeBps: 30, + role: 'public_routing', + venue: 'dodo_pmm', + }, + ], + }, + }, + } as unknown as DeploymentStatusFile; + + const { rows, byChainTokenPools } = buildGruV2PoolRegistryFromDeploymentData(data); + expect(rows).toHaveLength(3); + expect(rows.map((r) => r.section).sort()).toEqual(['gasPmmPools', 'pmmPools', 'pmmPoolsVolatile']); + + const usdt = '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae'; + expect(byChainTokenPools.get(`1:${usdt}`)?.has('0x1111111111111111111111111111111111111111')).toBe(true); + expect(byChainTokenPools.get(`1:${usdt}`)?.has('0x2222222222222222222222222222222222222222')).toBe(true); + }); + + it('preferGruV2OfficialDexPairs leaves pairs unchanged when no deployment pools index the token', () => { + const pairs = [{ pairAddress: '0xbb', priceUsd: '1' }]; + expect(preferGruV2OfficialDexPairs(99999, '0x0000000000000000000000000000000000000001', pairs)).toEqual(pairs); + }); +}); diff --git a/services/token-aggregation/src/config/gru-v2-deployment-pools.ts b/services/token-aggregation/src/config/gru-v2-deployment-pools.ts new file mode 100644 index 0000000..95ed486 --- /dev/null +++ b/services/token-aggregation/src/config/gru-v2-deployment-pools.ts @@ -0,0 +1,197 @@ +import type { DeploymentStatusFile } from './deployment-status'; +import { loadDeploymentStatusFile, resolveDeploymentStatusPath } from './deployment-status'; + +export type GruV2PmmSection = 'pmmPools' | 'pmmPoolsVolatile' | 'gasPmmPools'; + +export interface GruV2DeploymentPoolRow { + chainId: number; + chainName: string; + section: GruV2PmmSection; + baseSymbol: string; + quoteSymbol: string; + baseAddress: string; + quoteAddress: string; + poolAddress: string; + feeBps?: number; + role?: string; + publicRoutingEnabled?: boolean; + familyKey?: string; + venue?: string; +} + +interface ChainTokenMaps { + cwTokens?: Record; + gasMirrors?: Record; + anchorAddresses?: Record; + gasQuoteAddresses?: Record; +} + +/** Resolve a pool leg symbol to an address using deployment-status chain maps (cW*, anchors, gas quotes). */ +export function resolveDeploymentTokenAddress(chain: ChainTokenMaps, symbol: string): string | null { + const candidates = [ + chain.cwTokens?.[symbol], + chain.gasMirrors?.[symbol], + chain.anchorAddresses?.[symbol], + chain.gasQuoteAddresses?.[symbol], + ]; + for (const a of candidates) { + if (typeof a === 'string' && a.startsWith('0x')) { + return a.toLowerCase(); + } + } + return null; +} + +function pushTokenPoolIndex( + byChainTokenPools: Map>, + chainId: number, + tokenAddress: string, + poolAddress: string +): void { + const key = `${chainId}:${tokenAddress.toLowerCase()}`; + let set = byChainTokenPools.get(key); + if (!set) { + set = new Set(); + byChainTokenPools.set(key, set); + } + set.add(poolAddress.toLowerCase()); +} + +/** + * Build GRU v2 PMM pool rows and a (chainId:token) → official pool addresses index from deployment-status data. + * Includes stable mesh (`pmmPools`), volatile (`pmmPoolsVolatile`), and gas (`gasPmmPools`) sections. + */ +export function buildGruV2PoolRegistryFromDeploymentData(data: DeploymentStatusFile): { + rows: GruV2DeploymentPoolRow[]; + byChainTokenPools: Map>; +} { + const rows: GruV2DeploymentPoolRow[] = []; + const byChainTokenPools = new Map>(); + + const sections: GruV2PmmSection[] = ['pmmPools', 'pmmPoolsVolatile', 'gasPmmPools']; + + for (const [cid, rawChain] of Object.entries(data.chains ?? {})) { + const chainId = Number(cid); + if (Number.isNaN(chainId)) continue; + const chain = rawChain as ChainTokenMaps & { + name?: string; + pmmPools?: unknown; + pmmPoolsVolatile?: unknown; + gasPmmPools?: unknown; + }; + const chainName = typeof chain.name === 'string' ? chain.name : `Chain ${cid}`; + + for (const section of sections) { + const arr = chain[section]; + if (!Array.isArray(arr)) continue; + + for (const raw of arr) { + if (!raw || typeof raw !== 'object') continue; + const pool = raw as Record; + const poolAddress = typeof pool.poolAddress === 'string' ? pool.poolAddress.trim() : ''; + const baseSymbol = typeof pool.base === 'string' ? pool.base : ''; + const quoteSymbol = typeof pool.quote === 'string' ? pool.quote : ''; + if (!poolAddress.startsWith('0x') || !baseSymbol || !quoteSymbol) continue; + + const baseAddress = resolveDeploymentTokenAddress(chain, baseSymbol); + const quoteAddress = resolveDeploymentTokenAddress(chain, quoteSymbol); + if (!baseAddress || !quoteAddress) continue; + + const row: GruV2DeploymentPoolRow = { + chainId, + chainName, + section, + baseSymbol, + quoteSymbol, + baseAddress, + quoteAddress, + poolAddress: poolAddress.toLowerCase(), + feeBps: typeof pool.feeBps === 'number' ? pool.feeBps : undefined, + role: typeof pool.role === 'string' ? pool.role : undefined, + publicRoutingEnabled: + typeof pool.publicRoutingEnabled === 'boolean' ? pool.publicRoutingEnabled : undefined, + familyKey: typeof pool.familyKey === 'string' ? pool.familyKey : undefined, + venue: typeof pool.venue === 'string' ? pool.venue : undefined, + }; + rows.push(row); + + pushTokenPoolIndex(byChainTokenPools, chainId, baseAddress, poolAddress); + pushTokenPoolIndex(byChainTokenPools, chainId, quoteAddress, poolAddress); + } + } + } + + rows.sort((a, b) => { + const c = a.chainId - b.chainId; + if (c !== 0) return c; + return a.poolAddress.localeCompare(b.poolAddress); + }); + + return { rows, byChainTokenPools }; +} + +let cachedSnapshot: { + sourcePath: string; + lastModified: string; + rows: GruV2DeploymentPoolRow[]; + byChainTokenPools: Map>; +} | null = null; + +function ensureRegistry(): void { + const loaded = loadDeploymentStatusFile(); + const sourcePath = resolveDeploymentStatusPath() ?? ''; + const lm = loaded?.lastModified ?? ''; + if (cachedSnapshot && cachedSnapshot.lastModified === lm && cachedSnapshot.sourcePath === sourcePath) { + return; + } + if (!loaded) { + cachedSnapshot = { sourcePath, lastModified: lm, rows: [], byChainTokenPools: new Map() }; + return; + } + const built = buildGruV2PoolRegistryFromDeploymentData(loaded.data); + cachedSnapshot = { + sourcePath, + lastModified: lm, + rows: built.rows, + byChainTokenPools: built.byChainTokenPools, + }; +} + +/** All resolved GRU v2 PMM pools from `deployment-status.json` (when available). */ +export function getGruV2DeploymentPoolRows(): GruV2DeploymentPoolRow[] { + ensureRegistry(); + return cachedSnapshot?.rows ?? []; +} + +/** + * Official pool contract addresses for this token on this chain (from deployment-status). + * Returns `null` when the registry is empty or this token has no GRU pools — callers should use all DexScreener pairs. + */ +export function getOfficialGruV2PoolAddressesForToken(chainId: number, normalizedTokenAddress: string): Set | null { + ensureRegistry(); + if (!cachedSnapshot || cachedSnapshot.rows.length === 0) { + return null; + } + const key = `${chainId}:${normalizedTokenAddress.toLowerCase()}`; + const set = cachedSnapshot.byChainTokenPools.get(key); + if (!set || set.size === 0) { + return null; + } + return set; +} + +/** + * When DexScreener returns multiple pairs, prefer rows whose `pairAddress` is an official GRU v2 pool; if none match, keep full list. + */ +export function preferGruV2OfficialDexPairs( + chainId: number, + tokenAddress: string, + pairs: T[] +): T[] { + const official = getOfficialGruV2PoolAddressesForToken(chainId, tokenAddress.toLowerCase()); + if (!official || official.size === 0) { + return pairs; + } + const preferred = pairs.filter((p) => official.has(p.pairAddress.toLowerCase())); + return preferred.length > 0 ? preferred : pairs; +} diff --git a/services/token-aggregation/src/config/provider-capabilities.ts b/services/token-aggregation/src/config/provider-capabilities.ts index 764f9ab..5b58be6 100644 --- a/services/token-aggregation/src/config/provider-capabilities.ts +++ b/services/token-aggregation/src/config/provider-capabilities.ts @@ -94,6 +94,10 @@ function encodeOneInchRoute(router: string): string { return abiCoder.encode(['address', 'address', 'bytes'], [router, router, '0x']); } +function encodeRouterV2Route(factory: string, router: string): string { + return abiCoder.encode(['address', 'address'], [factory, router]); +} + function chain138DodoCapabilities(): ProviderCapabilityRecord { const assets = getChain138RoutingAssets(); const dodoProvider = @@ -384,6 +388,140 @@ function chain138UniswapCapabilities(): ProviderCapabilityRecord { }; } +function chain138UniswapV2Capabilities(): ProviderCapabilityRecord { + const assets = getChain138RoutingAssets(); + const factory = normalizeAddress(process.env.CHAIN_138_UNISWAP_V2_FACTORY); + const router = normalizeAddress(process.env.CHAIN_138_UNISWAP_V2_ROUTER); + const wethUsdtPair = normalizeAddress(process.env.CHAIN138_UNISWAP_V2_NATIVE_WETH_USDT_PAIR); + const wethUsdcPair = normalizeAddress(process.env.CHAIN138_UNISWAP_V2_NATIVE_WETH_USDC_PAIR); + const cusdtCusdcPair = normalizeAddress(process.env.CHAIN138_UNISWAP_V2_NATIVE_CUSDT_CUSDC_PAIR); + const status = factory && router ? 'live' : 'planned'; + + const pairs = [ + ...bidirectionalPair({ + chainId: CHAIN_138, + provider: 'uniswap_v2', + tokenASymbol: 'WETH', + tokenAAddress: assets.WETH.address, + tokenBSymbol: 'USDT', + tokenBAddress: assets.USDT.address, + status, + target: router, + providerData: status === 'live' ? { factory, router, pair: wethUsdtPair } : undefined, + providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined, + notes: ['Canonical Chain 138 native Uniswap v2 WETH/USDT venue.'], + reason: status === 'planned' ? 'Configure CHAIN_138_UNISWAP_V2_FACTORY and CHAIN_138_UNISWAP_V2_ROUTER after Chain 138 native venue deployment.' : undefined, + }), + ...bidirectionalPair({ + chainId: CHAIN_138, + provider: 'uniswap_v2', + tokenASymbol: 'WETH', + tokenAAddress: assets.WETH.address, + tokenBSymbol: 'USDC', + tokenBAddress: assets.USDC.address, + status, + target: router, + providerData: status === 'live' ? { factory, router, pair: wethUsdcPair } : undefined, + providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined, + notes: ['Canonical Chain 138 native Uniswap v2 WETH/USDC venue.'], + reason: status === 'planned' ? 'Configure CHAIN_138_UNISWAP_V2_FACTORY and CHAIN_138_UNISWAP_V2_ROUTER after Chain 138 native venue deployment.' : undefined, + }), + ...bidirectionalPair({ + chainId: CHAIN_138, + provider: 'uniswap_v2', + tokenASymbol: 'cUSDT', + tokenAAddress: assets.cUSDT.address, + tokenBSymbol: 'cUSDC', + tokenBAddress: assets.cUSDC.address, + status, + target: router, + providerData: status === 'live' ? { factory, router, pair: cusdtCusdcPair } : undefined, + providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined, + notes: ['Canonical Chain 138 native Uniswap v2 GRU stable venue.'], + reason: status === 'planned' ? 'Configure CHAIN_138_UNISWAP_V2_FACTORY and CHAIN_138_UNISWAP_V2_ROUTER after Chain 138 native venue deployment.' : undefined, + }), + ]; + + return { + chainId: CHAIN_138, + provider: 'uniswap_v2', + executionMode: 'onchain', + live: status === 'live', + quoteLive: status === 'live', + executionLive: status === 'live', + supportedLegTypes: ['swap'], + pairs, + notes: ['Canonical Chain 138 native Uniswap v2 router/factory path.'], + }; +} + +function chain138SushiswapCapabilities(): ProviderCapabilityRecord { + const assets = getChain138RoutingAssets(); + const factory = normalizeAddress(process.env.CHAIN_138_SUSHISWAP_FACTORY); + const router = normalizeAddress(process.env.CHAIN_138_SUSHISWAP_ROUTER); + const wethUsdtPair = normalizeAddress(process.env.CHAIN138_SUSHISWAP_NATIVE_WETH_USDT_PAIR); + const wethUsdcPair = normalizeAddress(process.env.CHAIN138_SUSHISWAP_NATIVE_WETH_USDC_PAIR); + const cusdtCusdcPair = normalizeAddress(process.env.CHAIN138_SUSHISWAP_NATIVE_CUSDT_CUSDC_PAIR); + const status = factory && router ? 'live' : 'planned'; + + const pairs = [ + ...bidirectionalPair({ + chainId: CHAIN_138, + provider: 'sushiswap', + tokenASymbol: 'WETH', + tokenAAddress: assets.WETH.address, + tokenBSymbol: 'USDT', + tokenBAddress: assets.USDT.address, + status, + target: router, + providerData: status === 'live' ? { factory, router, pair: wethUsdtPair } : undefined, + providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined, + notes: ['Canonical Chain 138 native SushiSwap-compatible WETH/USDT venue.'], + reason: status === 'planned' ? 'Configure CHAIN_138_SUSHISWAP_FACTORY and CHAIN_138_SUSHISWAP_ROUTER after Chain 138 Sushi deployment.' : undefined, + }), + ...bidirectionalPair({ + chainId: CHAIN_138, + provider: 'sushiswap', + tokenASymbol: 'WETH', + tokenAAddress: assets.WETH.address, + tokenBSymbol: 'USDC', + tokenBAddress: assets.USDC.address, + status, + target: router, + providerData: status === 'live' ? { factory, router, pair: wethUsdcPair } : undefined, + providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined, + notes: ['Canonical Chain 138 native SushiSwap-compatible WETH/USDC venue.'], + reason: status === 'planned' ? 'Configure CHAIN_138_SUSHISWAP_FACTORY and CHAIN_138_SUSHISWAP_ROUTER after Chain 138 Sushi deployment.' : undefined, + }), + ...bidirectionalPair({ + chainId: CHAIN_138, + provider: 'sushiswap', + tokenASymbol: 'cUSDT', + tokenAAddress: assets.cUSDT.address, + tokenBSymbol: 'cUSDC', + tokenBAddress: assets.cUSDC.address, + status, + target: router, + providerData: status === 'live' ? { factory, router, pair: cusdtCusdcPair } : undefined, + providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined, + notes: ['Canonical Chain 138 native SushiSwap-compatible GRU stable venue.'], + reason: status === 'planned' ? 'Configure CHAIN_138_SUSHISWAP_FACTORY and CHAIN_138_SUSHISWAP_ROUTER after Chain 138 Sushi deployment.' : undefined, + }), + ]; + + return { + chainId: CHAIN_138, + provider: 'sushiswap', + executionMode: 'onchain', + live: status === 'live', + quoteLive: status === 'live', + executionLive: status === 'live', + supportedLegTypes: ['swap'], + pairs, + notes: ['Canonical Chain 138 native SushiSwap-compatible router/factory path.'], + }; +} + function chain138BalancerCapabilities(): ProviderCapabilityRecord { const assets = getChain138RoutingAssets(); const vault = normalizeAddress(process.env.BALANCER_VAULT || CHAIN138_PILOT_BALANCER_VAULT); @@ -538,6 +676,8 @@ export function getProviderCapabilities(chainId: number): ProviderCapabilityReco chain138DodoCapabilities(), chain138DodoV3Capabilities(), chain138UniswapCapabilities(), + chain138UniswapV2Capabilities(), + chain138SushiswapCapabilities(), chain138BalancerCapabilities(), chain138CurveCapabilities(), chain138OneInchCapabilities(), diff --git a/services/token-aggregation/src/config/routing-policies.ts b/services/token-aggregation/src/config/routing-policies.ts index e2e5833..db9660f 100644 --- a/services/token-aggregation/src/config/routing-policies.ts +++ b/services/token-aggregation/src/config/routing-policies.ts @@ -16,7 +16,7 @@ export function resolveRoutingPolicy( const baseStandard: RoutingPolicy = { profile: 'standard', - allowedProviders: ['dodo', 'dodo_v3', 'uniswap_v3', 'balancer', 'curve', 'one_inch'], + allowedProviders: ['dodo', 'dodo_v3', 'uniswap_v3', 'uniswap_v2', 'sushiswap', 'balancer', 'curve', 'one_inch'], defaultIntermediateAddresses: defaultIntermediates, allowBridge: constraints.allowBridge !== false, allowedBridgeLabels: ['GRUTransport', 'CCIPStableBridge', 'CCIPWETH9Bridge', 'UniversalCCIPBridge', 'AlltraAdapter'], diff --git a/services/token-aggregation/src/database/repositories/admin-repo.ts b/services/token-aggregation/src/database/repositories/admin-repo.ts index 17ef5b4..067977c 100644 --- a/services/token-aggregation/src/database/repositories/admin-repo.ts +++ b/services/token-aggregation/src/database/repositories/admin-repo.ts @@ -41,7 +41,7 @@ export interface ApiEndpoint { export interface DexFactoryConfig { id?: number; chainId: number; - dexType: 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom'; + dexType: 'uniswap_v2' | 'uniswap_v3' | 'sushiswap' | 'dodo' | 'custom'; factoryAddress: string; routerAddress?: string; poolManagerAddress?: string; diff --git a/services/token-aggregation/src/database/repositories/pool-repo.ts b/services/token-aggregation/src/database/repositories/pool-repo.ts index 9c2d8b1..4c5a28b 100644 --- a/services/token-aggregation/src/database/repositories/pool-repo.ts +++ b/services/token-aggregation/src/database/repositories/pool-repo.ts @@ -1,7 +1,7 @@ import { Pool } from 'pg'; import { getDatabasePool } from '../client'; -export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom'; +export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'sushiswap' | 'dodo' | 'custom'; export interface LiquidityPool { id?: number; diff --git a/services/token-aggregation/src/indexer/chain-indexer.ts b/services/token-aggregation/src/indexer/chain-indexer.ts index a46a310..dbcad4e 100644 --- a/services/token-aggregation/src/indexer/chain-indexer.ts +++ b/services/token-aggregation/src/indexer/chain-indexer.ts @@ -9,6 +9,8 @@ import { CoinGeckoAdapter } from '../adapters/coingecko-adapter'; import { CoinMarketCapAdapter } from '../adapters/cmc-adapter'; import { DexScreenerAdapter } from '../adapters/dexscreener-adapter'; import { logger } from '../utils/logger'; +import { getCanonicalPriceUsd } from '../services/canonical-price-oracle'; +import { pickExternalMarketDataForIndexer } from '../services/valuation-precedence'; export class ChainIndexer { private chainId: number; @@ -153,8 +155,12 @@ export class ChainIndexer { this.adapters.dexscreener.getMarketData(this.chainId, tokenAddress), ]); - // Merge external data (prefer CoinGecko, fallback to others) - const externalData = coingeckoData || dexscreenerData || cmcData; + const externalData = pickExternalMarketDataForIndexer(this.chainId, tokenAddress.toLowerCase(), { + coingecko: coingeckoData, + cmc: cmcData, + dexscreener: dexscreenerData, + }); + const canonicalPriceUsd = getCanonicalPriceUsd(this.chainId, tokenAddress); // Get pools for liquidity calculation const tokenPools = pools.filter( @@ -166,7 +172,7 @@ export class ChainIndexer { await this.marketDataRepo.upsertMarketData({ chainId: this.chainId, tokenAddress, - priceUsd: externalData?.priceUsd, + priceUsd: externalData?.priceUsd ?? canonicalPriceUsd, priceChange24h: externalData?.priceChange24h, volume24h: volumeMetrics.volume24h || externalData?.volume24h || 0, volume7d: volumeMetrics.volume7d, diff --git a/services/token-aggregation/src/indexer/pool-indexer.ts b/services/token-aggregation/src/indexer/pool-indexer.ts index e3847be..3da2b3e 100644 --- a/services/token-aggregation/src/indexer/pool-indexer.ts +++ b/services/token-aggregation/src/indexer/pool-indexer.ts @@ -83,6 +83,7 @@ export class PoolIndexer { const hasDexConfig = !!dexConfig && (dexConfig.uniswap_v2?.length || + dexConfig.sushiswap?.length || dexConfig.uniswap_v3?.length || dexConfig.dodo?.length || dexConfig.custom?.length); @@ -100,7 +101,14 @@ export class PoolIndexer { // Index UniswapV2 pools if (dexConfig.uniswap_v2) { for (const config of dexConfig.uniswap_v2) { - const pools = await this.indexUniswapV2Pools(config); + const pools = await this.indexUniswapV2Pools(config, 'uniswap_v2'); + allPools.push(...pools); + } + } + + if (dexConfig.sushiswap) { + for (const config of dexConfig.sushiswap) { + const pools = await this.indexUniswapV2Pools(config, 'sushiswap'); allPools.push(...pools); } } @@ -208,7 +216,10 @@ export class PoolIndexer { /** * Index UniswapV2 pools from PairCreated events */ - private async indexUniswapV2Pools(config: UniswapV2Config): Promise { + private async indexUniswapV2Pools( + config: UniswapV2Config, + dexType: 'uniswap_v2' | 'sushiswap' + ): Promise { const pools: LiquidityPool[] = []; const factory = new ethers.Contract(config.factory, UNISWAP_V2_FACTORY_ABI, this.provider); @@ -237,7 +248,7 @@ export class PoolIndexer { poolAddress: pairAddress.toLowerCase(), token0Address: token0.toLowerCase(), token1Address: token1.toLowerCase(), - dexType: 'uniswap_v2', + dexType, factoryAddress: config.factory.toLowerCase(), routerAddress: config.router?.toLowerCase(), reserve0: reserve0.toString(), @@ -255,7 +266,7 @@ export class PoolIndexer { } } } catch (error) { - logger.error(`Error indexing UniswapV2 pools:`, error); + logger.error(`Error indexing ${dexType} pools:`, error); } return pools; @@ -390,7 +401,7 @@ export class PoolIndexer { } try { - if (dexType === 'uniswap_v2') { + if (dexType === 'uniswap_v2' || dexType === 'sushiswap') { const pair = new ethers.Contract(poolAddress, UNISWAP_V2_PAIR_ABI, this.provider); const [reserve0, reserve1] = await pair.getReserves(); diff --git a/services/token-aggregation/src/services/aggregator-route-matrix-generator.ts b/services/token-aggregation/src/services/aggregator-route-matrix-generator.ts index 8643d38..266548c 100644 --- a/services/token-aggregation/src/services/aggregator-route-matrix-generator.ts +++ b/services/token-aggregation/src/services/aggregator-route-matrix-generator.ts @@ -23,6 +23,10 @@ function providerProtocol(provider: PlannerProvider): string { return 'dodo_v3'; case 'uniswap_v3': return 'uniswap_v3'; + case 'uniswap_v2': + return 'uniswap_v2'; + case 'sushiswap': + return 'sushiswap'; case 'balancer': return 'balancer'; case 'curve': @@ -42,6 +46,10 @@ function providerLabel(provider: PlannerProvider): string { return 'DODO V3 / D3MM'; case 'uniswap_v3': return 'Uniswap V3'; + case 'uniswap_v2': + return 'Uniswap V2'; + case 'sushiswap': + return 'SushiSwap'; case 'balancer': return 'Balancer'; case 'curve': diff --git a/services/token-aggregation/src/services/best-execution-planner.test.ts b/services/token-aggregation/src/services/best-execution-planner.test.ts index fee59ac..37a8fb5 100644 --- a/services/token-aggregation/src/services/best-execution-planner.test.ts +++ b/services/token-aggregation/src/services/best-execution-planner.test.ts @@ -183,7 +183,7 @@ describe('BestExecutionPlanner', () => { expect(response.legs[0].provider).toBe('dodo_v3'); expect(response.estimatedAmountOut).toBe('211660490'); expect(response.routePlan).toBeDefined(); - expect(response.routePlan?.legs[0]?.provider).toBe(6); + expect(response.routePlan?.legs[0]?.provider).toBe(8); expect(response.riskFlags).toContain('pilot-venue'); expect(response.riskFlags).not.toContain('manual-execution-only'); }); diff --git a/services/token-aggregation/src/services/best-execution-planner.ts b/services/token-aggregation/src/services/best-execution-planner.ts index 4a6c424..d52a3d5 100644 --- a/services/token-aggregation/src/services/best-execution-planner.ts +++ b/services/token-aggregation/src/services/best-execution-planner.ts @@ -29,20 +29,24 @@ import { const abiCoder = AbiCoder.defaultAbiCoder(); const ROUTER_V2_RECIPIENT_PLACEHOLDER = ZeroAddress; const DEFAULT_INTENT_BRIDGE_COORDINATOR_V2 = normalizeAddress('0x7D0022B7e8360172fd9C0bB6778113b7Ea3674E7'); -const PROVIDER_PRIORITY: PlannerProvider[] = ['dodo', 'dodo_v3', 'uniswap_v3', 'balancer', 'curve', 'one_inch', 'partner']; +const PROVIDER_PRIORITY: PlannerProvider[] = ['dodo', 'dodo_v3', 'uniswap_v3', 'uniswap_v2', 'sushiswap', 'balancer', 'curve', 'one_inch', 'partner']; const PROVIDER_ENUM: Partial> = { dodo: 0, uniswap_v3: 1, - balancer: 2, - curve: 3, - one_inch: 4, - partner: 5, - dodo_v3: 6, + uniswap_v2: 2, + sushiswap: 3, + balancer: 4, + curve: 5, + one_inch: 6, + partner: 7, + dodo_v3: 8, }; const PROVIDER_GAS_USD: Record = { dodo: 0.22, dodo_v3: 0.3, uniswap_v3: 0.28, + uniswap_v2: 0.24, + sushiswap: 0.25, balancer: 0.34, curve: 0.29, one_inch: 0.48, diff --git a/services/token-aggregation/src/services/canonical-price-oracle.test.ts b/services/token-aggregation/src/services/canonical-price-oracle.test.ts new file mode 100644 index 0000000..6ddfbca --- /dev/null +++ b/services/token-aggregation/src/services/canonical-price-oracle.test.ts @@ -0,0 +1,99 @@ +import { getCanonicalTokenBySymbol } from '../config/canonical-tokens'; +import { getCanonicalPriceUsd, resolveCanonicalPriceUsd, resolveCanonicalPriceUsdForSpec } from './canonical-price-oracle'; + +describe('canonical-price-oracle', () => { + it('pegs Chain 138 USD-family mirrors to one dollar', () => { + expect(getCanonicalPriceUsd(138, '0x71D6687F38b93CCad569Fa6352c876eea967201b')).toBe(1); + expect(getCanonicalPriceUsd(138, '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22')).toBe(1); + expect(getCanonicalPriceUsd(138, '0xf22258f57794CC8E06237084b353Ab30fFfa640b')).toBe(1); + }); + + it('anchors GRU v2 and lending wrappers to the matching c* ISO-4217 asset family', () => { + const cUsdcV2 = getCanonicalTokenBySymbol(138, 'cUSDC_V2'); + expect(cUsdcV2?.addresses[138]).toBeTruthy(); + + expect(resolveCanonicalPriceUsd(138, String(cUsdcV2?.addresses[138]))).toMatchObject({ + priceUsd: 1, + referenceSymbol: 'USD', + }); + + expect( + resolveCanonicalPriceUsdForSpec({ + symbol: 'acUSDC', + name: 'Deposit cUSDC', + type: 'asset', + decimals: 6, + addresses: { 138: '0xac00000000000000000000000000000000000138' }, + }) + ).toMatchObject({ + priceUsd: 1, + referenceSymbol: 'USD', + }); + + expect( + resolveCanonicalPriceUsdForSpec({ + symbol: 'vdcEURC', + name: 'Debt cEURC (variable)', + type: 'debt', + decimals: 6, + addresses: { 138: '0xbdc0000000000000000000000000000000000138' }, + }) + ).toMatchObject({ + priceUsd: 1.1780, + referenceSymbol: 'EUR', + }); + + expect( + resolveCanonicalPriceUsdForSpec({ + symbol: 'sdcCADC', + name: 'Debt cCADC (stable)', + type: 'debt', + decimals: 6, + addresses: { 138: '0xcdc0000000000000000000000000000000000138' }, + }) + ).toMatchObject({ + priceUsd: 0.7255928549430243, + referenceSymbol: 'CAD', + }); + }); + + it('prices fiat GRU W tokens from the same ISO-4217 oracle references as the c* canonicals', () => { + const usdw = getCanonicalTokenBySymbol(25, 'USDW'); + const eurw = getCanonicalTokenBySymbol(25, 'EURW'); + + expect(usdw?.addresses[25]).toBeTruthy(); + expect(eurw?.addresses[25]).toBeTruthy(); + + expect(resolveCanonicalPriceUsd(25, String(usdw?.addresses[25]))).toMatchObject({ + priceUsd: 1, + referenceSymbol: 'USD', + }); + expect(resolveCanonicalPriceUsd(25, String(eurw?.addresses[25]))).toMatchObject({ + priceUsd: 1.1780, + referenceSymbol: 'EUR', + }); + }); + + it('resolves WETH9 and WETH10 to the ETH peg with 18-decimal canonical metadata', () => { + expect(resolveCanonicalPriceUsd(138, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')).toMatchObject({ + priceUsd: 2490, + referenceSymbol: 'ETH', + }); + expect(resolveCanonicalPriceUsd(138, '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')).toMatchObject({ + priceUsd: 2490, + referenceSymbol: 'ETH', + }); + }); + + it('provides repo-local fallback pegs for commodity and monetary-unit canonicals', () => { + expect(resolveCanonicalPriceUsd(138, '0x290E52a8819A4fbD0714E517225429aA2B70EC6b')).toMatchObject({ + referenceSymbol: 'XAU', + source: 'repo-fallback', + }); + expect(resolveCanonicalPriceUsd(138, '0xcb7c000000000000000000000000000000000138')).toMatchObject({ + priceUsd: 90000, + referenceSymbol: 'BTC', + source: 'repo-fallback', + }); + }); +}); diff --git a/services/token-aggregation/src/services/canonical-price-oracle.ts b/services/token-aggregation/src/services/canonical-price-oracle.ts new file mode 100644 index 0000000..242d0a4 --- /dev/null +++ b/services/token-aggregation/src/services/canonical-price-oracle.ts @@ -0,0 +1,193 @@ +import { type CanonicalTokenSpec, getCanonicalTokenByAddress, getCanonicalTokenBySymbol } from '../config/canonical-tokens'; + +export interface CanonicalPriceResolution { + priceUsd?: number; + referenceSymbol?: string; + source: 'env' | 'repo-fallback' | 'unresolved'; +} + +const FX_SNAPSHOT_GENERATED_AT = '2026-04-15'; + +// Repo-local inferred FX snapshot from scripts/lib/extraction_gap_closure.py. +const REPO_FALLBACK_PRICE_USD: Record = { + USD: 1, + EUR: 1.1780, + GBP: 1.3550353712543854, + AUD: 0.7136366390016357, + CAD: 0.7255928549430243, + CHF: 1.2776572668112798, + JPY: 0.006285683794888213, + XAU: 5163.3401260328355, + ETH: 2490, + BTC: 90000, + BNB: 610, + POL: 0.78, + AVAX: 48, + CELO: 0.72, + CRO: 0.14, + XDAI: 1, +}; + +function normalizeAddress(value: string): string { + return value.trim().toLowerCase(); +} + +function readEnvPrice(keys: string[]): number | undefined { + for (const key of keys) { + const raw = process.env[key]; + if (!raw || raw.trim() === '') continue; + const parsed = Number(raw); + if (Number.isFinite(parsed) && parsed > 0) { + return parsed; + } + } + return undefined; +} + +function resolveReferenceSymbol(spec: CanonicalTokenSpec): string | undefined { + const symbol = spec.symbol.toUpperCase(); + const currencyCode = String(spec.currencyCode || '').trim().toUpperCase(); + + if ( + symbol === 'WETH' || + symbol === 'WETH9' || + symbol === 'WETH10' || + symbol === 'CETH' || + symbol === 'CETHL2' || + symbol === 'CWETH' || + symbol === 'CWETHL2' + ) { + return 'ETH'; + } + + if (symbol === 'CBTC' || symbol === 'CWBTC') { + return 'BTC'; + } + + if (symbol === 'CXAUC' || symbol === 'CXAUT' || symbol === 'CAXAUC' || symbol === 'CAXAUT' || symbol === 'CWAXAUC' || symbol === 'CWAXAUT' || symbol === 'LIXAU') { + return 'XAU'; + } + + if (currencyCode) { + return currencyCode; + } + + return undefined; +} + +function resolvePegSourceSpec(spec: CanonicalTokenSpec, seenSymbols: Set = new Set()): CanonicalTokenSpec { + const symbol = spec.symbol.trim(); + const symbolUpper = symbol.toUpperCase(); + if (seenSymbols.has(symbolUpper)) { + return spec; + } + + const nextSeen = new Set(seenSymbols); + nextSeen.add(symbolUpper); + + const familySymbol = String(spec.familySymbol || '').trim(); + if (familySymbol) { + const familyMatch = getCanonicalTokenByAddressFromSymbolFamily(spec, familySymbol); + if (familyMatch) { + return resolvePegSourceSpec(familyMatch, nextSeen); + } + } + + const lendingWrapperMatch = /^(ac|vdc|sdc)(.+)$/i.exec(symbol); + if (lendingWrapperMatch) { + const underlyingSymbol = `c${lendingWrapperMatch[2]}`; + const underlyingMatch = getCanonicalTokenByAddressFromSymbolFamily(spec, underlyingSymbol); + if (underlyingMatch) { + return resolvePegSourceSpec(underlyingMatch, nextSeen); + } + } + + return spec; +} + +function getCanonicalTokenByAddressFromSymbolFamily(spec: CanonicalTokenSpec, symbol: string): CanonicalTokenSpec | undefined { + for (const [chainIdText, address] of Object.entries(spec.addresses)) { + if (!address || String(address).trim() === '') continue; + const chainId = Number(chainIdText); + if (!Number.isFinite(chainId)) continue; + const familyMatch = getCanonicalTokenBySymbol(chainId, symbol); + if (familyMatch) { + return familyMatch; + } + } + return undefined; +} + +function resolveEnvPriceKeys(referenceSymbol: string): string[] { + const symbol = referenceSymbol.toUpperCase(); + if (symbol === 'XAU') { + return [ + 'CHAIN138_CANONICAL_PRICE_USD_XAU', + 'CANONICAL_PRICE_USD_XAU', + 'XAU_SPOT_USD', + 'GOLD_USD_PRICE', + ]; + } + + if (symbol === 'ETH') { + return [ + 'CHAIN138_CANONICAL_PRICE_USD_ETH', + 'CANONICAL_PRICE_USD_ETH', + 'ETH_PRICE_USD', + 'CHAIN138_D3_PILOT_WETH_USD', + ]; + } + + return [ + `CHAIN138_CANONICAL_PRICE_USD_${symbol}`, + `CANONICAL_PRICE_USD_${symbol}`, + `${symbol}_PRICE_USD`, + ]; +} + +export function resolveCanonicalPriceUsdForSpec(spec: CanonicalTokenSpec): CanonicalPriceResolution { + const pegSourceSpec = resolvePegSourceSpec(spec); + const referenceSymbol = resolveReferenceSymbol(pegSourceSpec); + if (!referenceSymbol) { + return { source: 'unresolved' }; + } + + const envPrice = readEnvPrice(resolveEnvPriceKeys(referenceSymbol)); + if (envPrice !== undefined) { + return { + priceUsd: envPrice, + referenceSymbol, + source: 'env', + }; + } + + const fallback = REPO_FALLBACK_PRICE_USD[referenceSymbol]; + if (fallback !== undefined) { + return { + priceUsd: fallback, + referenceSymbol, + source: 'repo-fallback', + }; + } + + return { + referenceSymbol, + source: 'unresolved', + }; +} + +export function resolveCanonicalPriceUsd(chainId: number, address: string): CanonicalPriceResolution { + const spec = getCanonicalTokenByAddress(chainId, normalizeAddress(address)); + if (!spec) { + return { source: 'unresolved' }; + } + return resolveCanonicalPriceUsdForSpec(spec); +} + +export function getCanonicalPriceUsd(chainId: number, address: string): number | undefined { + return resolveCanonicalPriceUsd(chainId, address).priceUsd; +} + +export function getCanonicalPriceSnapshotGeneratedAt(): string { + return FX_SNAPSHOT_GENERATED_AT; +} diff --git a/services/token-aggregation/src/services/chain138-dodo-liquidity.test.ts b/services/token-aggregation/src/services/chain138-dodo-liquidity.test.ts index 8d72292..961a7bd 100644 --- a/services/token-aggregation/src/services/chain138-dodo-liquidity.test.ts +++ b/services/token-aggregation/src/services/chain138-dodo-liquidity.test.ts @@ -28,7 +28,20 @@ describe('estimateChain138DodoLiquidityUsd', () => { expect(result.totalLiquidityUsd).toBe(210_830); }); - it('keeps non-USD pairs at zero without a usable USD side', () => { + it('keeps WETH9 on the ETH peg even when live oracle price is unavailable', () => { + const result = estimateChain138DodoLiquidityUsd({ + token0Address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + token1Address: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1', + reserve0: 10n * 10n ** 18n, + reserve1: 24_900n * 10n ** 6n, + }); + + expect(result.reserve0Usd).toBe(24_900); + expect(result.reserve1Usd).toBe(24_900); + expect(result.totalLiquidityUsd).toBe(49_800); + }); + + it('values non-USD canonical pairs from their repo-local peg references', () => { const result = estimateChain138DodoLiquidityUsd({ token0Address: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f', token1Address: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b', @@ -36,11 +49,22 @@ describe('estimateChain138DodoLiquidityUsd', () => { reserve1: 5n * 10n ** 6n, }); - expect(result).toEqual({ - reserve0Usd: 0, - reserve1Usd: 0, - totalLiquidityUsd: 0, + expect(result.reserve0Usd).toBe(24_900); + expect(result.reserve1Usd).toBeCloseTo(25_816.700630164178, 6); + expect(result.totalLiquidityUsd).toBeCloseTo(50_716.70063016418, 6); + }); + + it('values XAU/stable DODO pools from the canonical gold peg', () => { + const result = estimateChain138DodoLiquidityUsd({ + token0Address: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b', + token1Address: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1', + reserve0: 5n * 10n ** 6n, + reserve1: 25_816n * 10n ** 6n, }); + + expect(result.reserve0Usd).toBeCloseTo(25_816.700630164178, 6); + expect(result.reserve1Usd).toBe(25_816); + expect(result.totalLiquidityUsd).toBeCloseTo(51_632.70063016418, 6); }); it('values cBTC/stable DODO pools using satoshi precision and the BTC fallback price', () => { diff --git a/services/token-aggregation/src/services/chain138-dodo-liquidity.ts b/services/token-aggregation/src/services/chain138-dodo-liquidity.ts index 407adb8..5b97858 100644 --- a/services/token-aggregation/src/services/chain138-dodo-liquidity.ts +++ b/services/token-aggregation/src/services/chain138-dodo-liquidity.ts @@ -1,9 +1,8 @@ import { formatUnits } from 'ethers'; import { getCanonicalTokenByAddress } from '../config/canonical-tokens'; +import { getCanonicalPriceUsd } from './canonical-price-oracle'; const CHAIN_138 = 138; -const DEFAULT_WETH_USD_PRICE = 2100; -const DEFAULT_BTC_USD_PRICE = 90000; export interface Chain138DodoLiquidityUsd { reserve0Usd: number; @@ -20,21 +19,6 @@ function decimalsForAddress(address: string): number { return Number(spec?.decimals ?? 18); } -function isUsdAddress(address: string): boolean { - const spec = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address)); - return spec?.currencyCode === 'USD'; -} - -function isWethLikeAddress(address: string): boolean { - const symbol = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address))?.symbol?.toUpperCase(); - return symbol === 'WETH' || symbol === 'WETH10'; -} - -function isBtcLikeAddress(address: string): boolean { - const symbol = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address))?.symbol?.toUpperCase(); - return symbol === 'CBTC'; -} - function parseAmount(value: bigint, decimals: number): number { if (value <= 0n) return 0; const parsed = Number(formatUnits(value, decimals)); @@ -60,54 +44,32 @@ export function estimateChain138DodoLiquidityUsd(args: { const reserve1Amount = parseAmount(args.reserve1, decimalsForAddress(token1Address)); const price = parsePrice(args.price); - const token0IsUsd = isUsdAddress(token0Address); - const token1IsUsd = isUsdAddress(token1Address); - if (reserve0Amount <= 0 || reserve1Amount <= 0) { return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 }; } - if (token0IsUsd && token1IsUsd) { - return { - reserve0Usd: reserve0Amount, - reserve1Usd: reserve1Amount, - totalLiquidityUsd: reserve0Amount + reserve1Amount, - }; + let token0PriceUsd = getCanonicalPriceUsd(CHAIN_138, token0Address) ?? 0; + let token1PriceUsd = getCanonicalPriceUsd(CHAIN_138, token1Address) ?? 0; + + if (price > 0) { + if (token1PriceUsd === 1 && token0PriceUsd !== 1) { + token0PriceUsd = price; + } + if (token0PriceUsd === 1 && token1PriceUsd !== 1) { + token1PriceUsd = price; + } } - if (token1IsUsd) { - const reserve0Usd = - price > 0 - ? reserve0Amount * price - : isWethLikeAddress(token0Address) - ? reserve0Amount * DEFAULT_WETH_USD_PRICE - : isBtcLikeAddress(token0Address) - ? reserve0Amount * DEFAULT_BTC_USD_PRICE - : 0; - - return { - reserve0Usd, - reserve1Usd: reserve1Amount, - totalLiquidityUsd: reserve0Usd > 0 ? reserve0Usd + reserve1Amount : 0, - }; + if (token0PriceUsd <= 0 || token1PriceUsd <= 0) { + return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 }; } - if (token0IsUsd) { - const reserve1Usd = - price > 0 - ? reserve1Amount / price - : isWethLikeAddress(token1Address) - ? reserve1Amount * DEFAULT_WETH_USD_PRICE - : isBtcLikeAddress(token1Address) - ? reserve1Amount * DEFAULT_BTC_USD_PRICE - : 0; + const reserve0Usd = reserve0Amount * token0PriceUsd; + const reserve1Usd = reserve1Amount * token1PriceUsd; - return { - reserve0Usd: reserve0Amount, - reserve1Usd, - totalLiquidityUsd: reserve1Usd > 0 ? reserve0Amount + reserve1Usd : 0, - }; - } - - return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 }; + return { + reserve0Usd, + reserve1Usd, + totalLiquidityUsd: reserve0Usd + reserve1Usd, + }; } diff --git a/services/token-aggregation/src/services/planner-v2-types.ts b/services/token-aggregation/src/services/planner-v2-types.ts index 693dddc..d39364d 100644 --- a/services/token-aggregation/src/services/planner-v2-types.ts +++ b/services/token-aggregation/src/services/planner-v2-types.ts @@ -1,4 +1,13 @@ -export type PlannerProvider = 'dodo' | 'dodo_v3' | 'uniswap_v3' | 'balancer' | 'curve' | 'one_inch' | 'partner'; +export type PlannerProvider = + | 'dodo' + | 'dodo_v3' + | 'uniswap_v3' + | 'uniswap_v2' + | 'sushiswap' + | 'balancer' + | 'curve' + | 'one_inch' + | 'partner'; export type PlannerLegKind = 'swap' | 'bridge'; export type PlannerDecision = 'direct-pool' | 'multi-hop' | 'swap-bridge-swap' | 'bridge-only' | 'unresolved'; export type ComplianceProfile = 'standard' | 'institutional'; diff --git a/services/token-aggregation/src/services/route-graph-builder.ts b/services/token-aggregation/src/services/route-graph-builder.ts index e10894e..ec07d3b 100644 --- a/services/token-aggregation/src/services/route-graph-builder.ts +++ b/services/token-aggregation/src/services/route-graph-builder.ts @@ -30,6 +30,10 @@ function providerFromDexType(dexType: string): PlannerProvider | null { return 'dodo_v3'; case 'uniswap_v3': return 'uniswap_v3'; + case 'uniswap_v2': + return 'uniswap_v2'; + case 'sushiswap': + return 'sushiswap'; default: return null; } diff --git a/services/token-aggregation/src/services/valuation-precedence.test.ts b/services/token-aggregation/src/services/valuation-precedence.test.ts new file mode 100644 index 0000000..f528e53 --- /dev/null +++ b/services/token-aggregation/src/services/valuation-precedence.test.ts @@ -0,0 +1,195 @@ +import { + isGruV2CwMeshEdgeToken, + pickExternalMarketDataForIndexer, + resolveUsdValuation, + mergeMarketWithValuation, +} from './valuation-precedence'; + +/** Bridged `cW*` on Ethereum; on Chain 138 the native form is `cUSDC` (`c*`), not `cW*`. */ +const ETH_MAINNET_CWUSDC = '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a'; + +describe('valuation-precedence', () => { + const prevIndexerAge = process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS; + const prevEdgeCwDexFirst = process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST; + + afterEach(() => { + if (prevIndexerAge === undefined) { + delete process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS; + } else { + process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = prevIndexerAge; + } + if (prevEdgeCwDexFirst === undefined) { + delete process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST; + } else { + process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST = prevEdgeCwDexFirst; + } + }); + + it('prefers fresh indexer price over canonical', () => { + process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '3600'; + const lu = new Date(Date.now() - 60_000); + const v = resolveUsdValuation({ + chainId: 1, + normalizedAddress: '0x0000000000000000000000000000000000000001', + indexer: { + chainId: 1, + tokenAddress: '0x0000000000000000000000000000000000000001', + priceUsd: 2.5, + volume24h: 0, + volume7d: 0, + volume30d: 0, + liquidityUsd: 0, + holdersCount: 0, + transfers24h: 0, + lastUpdated: lu, + }, + coingecko: { priceUsd: 9.99, lastUpdated: new Date() }, + }); + expect(v.sourceLayer).toBe('indexer_market'); + expect(v.priceUsd).toBe(2.5); + expect(v.stale).toBe(false); + }); + + it('skips stale indexer and uses external when indexer is too old', () => { + process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '60'; + const lu = new Date(Date.now() - 120_000); + const v = resolveUsdValuation({ + chainId: 1, + normalizedAddress: '0x0000000000000000000000000000000000000001', + indexer: { + chainId: 1, + tokenAddress: '0x0000000000000000000000000000000000000001', + priceUsd: 2.5, + volume24h: 0, + volume7d: 0, + volume30d: 0, + liquidityUsd: 0, + holdersCount: 0, + transfers24h: 0, + lastUpdated: lu, + }, + coingecko: { priceUsd: 9.99, lastUpdated: new Date() }, + }); + expect(v.sourceLayer).toBe('external_coingecko'); + expect(v.priceUsd).toBe(9.99); + expect(v.stale).toBe(false); + }); + + it('falls back to stale indexer when nothing else is available', () => { + process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '60'; + const lu = new Date(Date.now() - 120_000); + const v = resolveUsdValuation({ + chainId: 1, + normalizedAddress: '0x0000000000000000000000000000000000000001', + indexer: { + chainId: 1, + tokenAddress: '0x0000000000000000000000000000000000000001', + priceUsd: 2.5, + volume24h: 0, + volume7d: 0, + volume30d: 0, + liquidityUsd: 0, + holdersCount: 0, + transfers24h: 0, + lastUpdated: lu, + }, + }); + expect(v.sourceLayer).toBe('indexer_market'); + expect(v.priceUsd).toBe(2.5); + expect(v.stale).toBe(true); + }); + + it('detects GRU v2 cW* on edge chains via canonical registry', () => { + expect(isGruV2CwMeshEdgeToken(1, ETH_MAINNET_CWUSDC)).toBe(true); + expect(isGruV2CwMeshEdgeToken(138, ETH_MAINNET_CWUSDC)).toBe(false); + expect(isGruV2CwMeshEdgeToken(1, '0x0000000000000000000000000000000000000001')).toBe(false); + }); + + it('for edge GRU v2 cW*, prefers DexScreener over CoinGecko when indexer is stale', () => { + process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '60'; + const lu = new Date(Date.now() - 120_000); + const v = resolveUsdValuation({ + chainId: 1, + normalizedAddress: ETH_MAINNET_CWUSDC, + indexer: { + chainId: 1, + tokenAddress: ETH_MAINNET_CWUSDC, + priceUsd: 2.5, + volume24h: 0, + volume7d: 0, + volume30d: 0, + liquidityUsd: 0, + holdersCount: 0, + transfers24h: 0, + lastUpdated: lu, + }, + coingecko: { priceUsd: 9.99, lastUpdated: new Date() }, + dexscreener: { priceUsd: 1.0, lastUpdated: new Date() }, + }); + expect(v.gruV2CwEdgeDexPriority).toBe(true); + expect(v.sourceLayer).toBe('external_dexscreener'); + expect(v.priceUsd).toBe(1.0); + expect(v.precedenceRank).toBe(2); + }); + + it('pickExternalMarketDataForIndexer prefers DexScreener for edge cW*', () => { + const dex = { priceUsd: 1, lastUpdated: new Date() }; + const cg = { priceUsd: 2, lastUpdated: new Date() }; + const picked = pickExternalMarketDataForIndexer(1, ETH_MAINNET_CWUSDC, { + coingecko: cg, + cmc: null, + dexscreener: dex, + }); + expect(picked?.priceUsd).toBe(1); + }); + + it('pickExternalMarketDataForIndexer prefers CoinGecko when edge cW* flag is off', () => { + process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST = '0'; + const dex = { priceUsd: 1, lastUpdated: new Date() }; + const cg = { priceUsd: 2, lastUpdated: new Date() }; + const picked = pickExternalMarketDataForIndexer(1, ETH_MAINNET_CWUSDC, { + coingecko: cg, + cmc: null, + dexscreener: dex, + }); + expect(picked?.priceUsd).toBe(2); + }); + + it('TOKEN_AGG_EDGE_CW_DEX_FIRST=0 restores CoinGecko-before-DexScreener for edge cW*', () => { + process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST = '0'; + process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '60'; + const lu = new Date(Date.now() - 120_000); + const v = resolveUsdValuation({ + chainId: 1, + normalizedAddress: ETH_MAINNET_CWUSDC, + indexer: { + chainId: 1, + tokenAddress: ETH_MAINNET_CWUSDC, + priceUsd: 2.5, + volume24h: 0, + volume7d: 0, + volume30d: 0, + liquidityUsd: 0, + holdersCount: 0, + transfers24h: 0, + lastUpdated: lu, + }, + coingecko: { priceUsd: 9.99, lastUpdated: new Date() }, + dexscreener: { priceUsd: 1.0, lastUpdated: new Date() }, + }); + expect(v.gruV2CwEdgeDexPriority).toBe(false); + expect(v.sourceLayer).toBe('external_coingecko'); + expect(v.priceUsd).toBe(9.99); + }); + + it('mergeMarketWithValuation applies priced layer onto indexer row', () => { + const pricing = resolveUsdValuation({ + chainId: 138, + normalizedAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase(), + indexer: null, + coingecko: undefined, + }); + const m = mergeMarketWithValuation(138, '0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', null, pricing); + expect(m?.priceUsd).toBe(pricing.priceUsd); + }); +}); diff --git a/services/token-aggregation/src/services/valuation-precedence.ts b/services/token-aggregation/src/services/valuation-precedence.ts new file mode 100644 index 0000000..99b7842 --- /dev/null +++ b/services/token-aggregation/src/services/valuation-precedence.ts @@ -0,0 +1,276 @@ +import type { MarketData } from '../adapters/base-adapter'; +import type { TokenMarketData } from '../database/repositories/token-repo'; +import { getChainConfig } from '../config/chains'; +import { getCanonicalTokenByAddress } from '../config/canonical-tokens'; +import { resolveCanonicalPriceUsd } from './canonical-price-oracle'; + +const CHAIN_138 = 138; + +/** + * When true (default), bridged GRU v2 **`cW*`** tokens (non–Chain-138) prefer DexScreener before CoinGecko/CMC. + * Native Chain 138 assets use **`c*`** naming (e.g. cUSDT); those are not bridged `cW*` and this flag does not apply on 138. + */ +function readEdgeCwDexFirst(): boolean { + const raw = process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST; + if (raw === undefined || raw === '') return true; + return raw === '1' || raw.toLowerCase() === 'true' || raw.toLowerCase() === 'yes'; +} + +/** + * True for **bridged** GRU v2 transport on a public network: symbols **`cW*`** (e.g. cWUSDT, cWEURC, cWXAUC, cWETH). + * On **Chain 138**, native GRU v2 uses **`c*`** (cUSDT, cUSDC, …), not `cW*` — this helper always returns false on 138 + * so valuation for home-chain assets follows the default layer order. + */ +export function isGruV2CwMeshEdgeToken(chainId: number, normalizedAddress: string): boolean { + if (chainId === CHAIN_138) return false; + const spec = getCanonicalTokenByAddress(chainId, normalizedAddress.toLowerCase()); + if (!spec) return false; + return /^cW/i.test(spec.symbol.trim()); +} + +export type PriceSourceLayer = + | 'indexer_market' + | 'external_coingecko' + | 'external_coinmarketcap' + | 'external_dexscreener' + | 'canonical_env' + | 'canonical_repo_fallback' + | 'none'; + +export interface ExplorerLinks { + chainId: number; + explorerBaseUrl?: string; + /** Primary explorer URL for this contract (Blockscout/Etherscan style). */ + addressUrl?: string; + /** Alias for UIs that expect a “token page”; often same as address on explorers. */ + tokenUrl?: string; +} + +export interface TokenPricing { + /** Omitted when no layer produced a price. */ + priceUsd?: number; + sourceLayer: PriceSourceLayer; + /** 1 = highest precedence (indexer). */ + precedenceRank: number; + /** True when the chosen layer is older than configured max age (indexer only today). */ + stale: boolean; + maxAgeSeconds: number; + /** ISO 8601 — effective “as of” for the displayed price. */ + asOf: string; + ageSeconds?: number; + indexerLastUpdated?: string; + referenceSymbol?: string; + canonicalResolutionSource?: 'env' | 'repo-fallback' | 'unresolved'; + /** Bridged `cW*` on an edge chain used DexScreener-priority ordering (native `c*` on 138 never sets this). */ + gruV2CwEdgeDexPriority?: boolean; +} + +export interface ValuationInput { + chainId: number; + normalizedAddress: string; + indexer: TokenMarketData | null; + coingecko?: MarketData | null; + cmc?: MarketData | null; + dexscreener?: MarketData | null; +} + +function readIndexerMaxAgeSeconds(): number { + const raw = process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS; + const n = raw ? parseInt(raw, 10) : NaN; + return Number.isFinite(n) && n > 0 ? n : 900; +} + +function ageMsOf(date: Date): number { + return Date.now() - date.getTime(); +} + +function iso(d: Date): string { + return d.toISOString(); +} + +/** + * Layered USD valuation: + * - Default: indexer (staleness-aware) → CoinGecko → CMC → DexScreener → canonical env/repo. + * - **Bridged GRU v2 `cW*` on edge networks** (not Chain 138; native **`c*`** lives on 138 only): indexer → + * **DexScreener → CoinGecko → CMC** → canonical, so that chain’s DEX/aggregated prices lead before generic repo FX. + * If the indexer price exists but is stale, we fall through to fresher layers before accepting the stale indexer. + */ +export function resolveUsdValuation(input: ValuationInput): TokenPricing { + const maxAgeSeconds = readIndexerMaxAgeSeconds(); + const { chainId, normalizedAddress, indexer } = input; + + const edgeCwDexPriority = readEdgeCwDexFirst() && isGruV2CwMeshEdgeToken(chainId, normalizedAddress); + + const canonical = resolveCanonicalPriceUsd(chainId, normalizedAddress); + const referenceSymbol = canonical.referenceSymbol; + const canonicalResolutionSource = canonical.source === 'unresolved' ? 'unresolved' : canonical.source; + + type Candidate = { + layer: PriceSourceLayer; + rank: number; + priceUsd: number; + stale: boolean; + asOf: Date; + }; + + const out: Candidate[] = []; + + if (indexer?.priceUsd !== undefined && indexer.priceUsd !== null) { + const lu = indexer.lastUpdated instanceof Date ? indexer.lastUpdated : new Date(indexer.lastUpdated); + const stale = ageMsOf(lu) > maxAgeSeconds * 1000; + out.push({ + layer: 'indexer_market', + rank: 1, + priceUsd: indexer.priceUsd, + stale, + asOf: lu, + }); + } + + const pushExternal = (layer: PriceSourceLayer, rank: number, md: MarketData | null | undefined): void => { + if (!md || md.priceUsd === undefined || md.priceUsd === null) return; + const asOf = md.lastUpdated instanceof Date ? md.lastUpdated : new Date(); + out.push({ + layer, + rank, + priceUsd: md.priceUsd, + stale: false, + asOf, + }); + }; + + if (edgeCwDexPriority) { + pushExternal('external_dexscreener', 2, input.dexscreener); + pushExternal('external_coingecko', 3, input.coingecko); + pushExternal('external_coinmarketcap', 4, input.cmc); + } else { + pushExternal('external_coingecko', 2, input.coingecko); + pushExternal('external_coinmarketcap', 3, input.cmc); + pushExternal('external_dexscreener', 4, input.dexscreener); + } + + if (canonical.priceUsd !== undefined && canonical.source === 'env') { + out.push({ + layer: 'canonical_env', + rank: 5, + priceUsd: canonical.priceUsd, + stale: false, + asOf: new Date(), + }); + } + + if (canonical.priceUsd !== undefined && canonical.source === 'repo-fallback') { + out.push({ + layer: 'canonical_repo_fallback', + rank: 6, + priceUsd: canonical.priceUsd, + stale: false, + asOf: new Date(), + }); + } + + const byRank = [...out].sort((a, b) => a.rank - b.rank); + const fresh = byRank.find((c) => !c.stale); + const winner = fresh || byRank[0]; + + if (!winner) { + return { + sourceLayer: 'none', + precedenceRank: 99, + stale: true, + maxAgeSeconds, + asOf: iso(new Date()), + referenceSymbol, + canonicalResolutionSource, + gruV2CwEdgeDexPriority: edgeCwDexPriority, + }; + } + + const ageSeconds = Math.max(0, Math.floor(ageMsOf(winner.asOf) / 1000)); + + return { + priceUsd: winner.priceUsd, + sourceLayer: winner.layer, + precedenceRank: winner.rank, + stale: winner.stale, + maxAgeSeconds, + asOf: iso(winner.asOf), + ageSeconds, + indexerLastUpdated: indexer?.lastUpdated + ? indexer.lastUpdated instanceof Date + ? iso(indexer.lastUpdated) + : String(indexer.lastUpdated) + : undefined, + referenceSymbol, + canonicalResolutionSource, + gruV2CwEdgeDexPriority: edgeCwDexPriority, + }; +} + +/** + * Chooses one external feed for persisting indexer `market_data` rows — same **coingecko / dexscreener / cmc** order as + * {@link resolveUsdValuation} (bridged **`cW*`** on edge chains prefer DexScreener when enabled). + */ +export function pickExternalMarketDataForIndexer( + chainId: number, + normalizedAddress: string, + sources: { coingecko: MarketData | null; cmc: MarketData | null; dexscreener: MarketData | null } +): MarketData | null { + const edgeCw = readEdgeCwDexFirst() && isGruV2CwMeshEdgeToken(chainId, normalizedAddress); + if (edgeCw) { + return sources.dexscreener ?? sources.coingecko ?? sources.cmc ?? null; + } + return sources.coingecko ?? sources.dexscreener ?? sources.cmc ?? null; +} + +/** + * Merge DB/indexer market row with layered valuation (priceUsd + optional fields). + */ +export function mergeMarketWithValuation( + chainId: number, + normalizedAddress: string, + marketData: TokenMarketData | null, + pricing: TokenPricing +): TokenMarketData | null { + const addr = normalizedAddress.toLowerCase(); + if (pricing.sourceLayer === 'none' && !marketData) { + return null; + } + if (!marketData) { + if (pricing.priceUsd === undefined) { + return null; + } + return { + chainId, + tokenAddress: addr, + priceUsd: pricing.priceUsd, + volume24h: 0, + volume7d: 0, + volume30d: 0, + liquidityUsd: 0, + holdersCount: 0, + transfers24h: 0, + lastUpdated: new Date(pricing.asOf), + }; + } + return { + ...marketData, + priceUsd: pricing.priceUsd !== undefined ? pricing.priceUsd : marketData.priceUsd, + }; +} + +export function buildExplorerLinks(chainId: number, address: string): ExplorerLinks { + const cfg = getChainConfig(chainId); + const base = cfg?.explorerUrl?.replace(/\/$/, '') || ''; + const lower = address.toLowerCase(); + if (!base) { + return { chainId }; + } + const url = `${base}/address/${lower}`; + return { + chainId, + explorerBaseUrl: base, + addressUrl: url, + tokenUrl: url, + }; +} diff --git a/services/tron-relay/src/TronRelayService.ts b/services/tron-relay/src/TronRelayService.ts new file mode 100644 index 0000000..9a6fadc --- /dev/null +++ b/services/tron-relay/src/TronRelayService.ts @@ -0,0 +1,22 @@ +import { + NON_EVM_RELAY_LIFECYCLE, + type NonEvmNetworkPolicy, + type NonEvmRelayObservation +} from '../../non-evm-relay/lifecycle'; + +export const TRON_RELAY_POLICY: NonEvmNetworkPolicy = { + identifier: 'Tron', + relayMode: 'custom_relay_scaffold', + destinationProgramModel: 'trc20_or_bridge_wrapped_cw', + signerFundingPolicy: 'trx_operator_signer', + finalityPolicy: 'block_finality>=20', + publicExposureStatus: 'operator_ready' +}; + +export class TronRelayService { + readonly lifecycle = NON_EVM_RELAY_LIFECYCLE; + + recordObservation(observation: NonEvmRelayObservation): NonEvmRelayObservation { + return observation; + } +} diff --git a/test/wrapped-lp-public/WrappedLPProgram.t.sol b/test/wrapped-lp-public/WrappedLPProgram.t.sol new file mode 100644 index 0000000..2293209 --- /dev/null +++ b/test/wrapped-lp-public/WrappedLPProgram.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import {Chain138LPLocker} from "../../contracts/wrapped-lp-public/Chain138LPLocker.sol"; +import {WLPReceiptToken} from "../../contracts/wrapped-lp-public/WLPReceiptToken.sol"; +import {PublicChainMintController} from "../../contracts/wrapped-lp-public/PublicChainMintController.sol"; +import {WLPRedemptionGateway} from "../../contracts/wrapped-lp-public/WLPRedemptionGateway.sol"; +import {WrappedLPNAVVault} from "../../contracts/wrapped-lp-public/WrappedLPNAVVault.sol"; +import {WLPNAVOracle} from "../../contracts/wrapped-lp-public/WLPNAVOracle.sol"; + +contract MockERC20 is ERC20 { + constructor(string memory n, string memory s) ERC20(n, s) {} + + function mint(address to, uint256 a) external { + _mint(to, a); + } +} + +contract WrappedLPProgramTest is Test { + MockERC20 lp; + Chain138LPLocker locker; + WLPReceiptToken wlp; + PublicChainMintController mintCtl; + WLPRedemptionGateway gateway; + + address admin = address(this); + address relayer = address(0xBEEF); + address user = address(0xA11CE); + + function setUp() public { + lp = new MockERC20("DODO LP", "DLP"); + locker = new Chain138LPLocker(address(lp), admin); + wlp = new WLPReceiptToken("Wrapped LP", "wLP", 18, admin); + mintCtl = new PublicChainMintController(address(wlp), address(locker), admin); + gateway = new WLPRedemptionGateway(address(wlp), admin); + + wlp.grantRole(wlp.MINTER_ROLE(), address(mintCtl)); + wlp.grantRole(wlp.BURNER_ROLE(), address(gateway)); + mintCtl.grantRole(mintCtl.RELAYER_ROLE(), relayer); + locker.grantRole(locker.BRIDGE_RELEASE_ROLE(), relayer); + } + + function test_lock_mint_release_invariant() public { + uint256 amt = 1000e18; + lp.mint(user, amt); + vm.startPrank(user); + lp.approve(address(locker), amt); + bytes32 lockRef = locker.deposit(amt); + vm.stopPrank(); + + assertEq(locker.totalEscrowed(), amt); + + vm.prank(relayer); + mintCtl.mintForLock(lockRef, user, amt); + assertEq(wlp.balanceOf(user), amt); + + vm.expectRevert(); + vm.prank(relayer); + mintCtl.mintForLock(lockRef, user, amt); + + vm.prank(user); + gateway.requestRedeem(amt); + assertEq(wlp.balanceOf(user), 0); + + vm.prank(relayer); + locker.releaseAmount(user, amt); + assertEq(locker.totalEscrowed(), 0); + assertEq(lp.balanceOf(user), amt); + } + + function test_nav_vault_cap() public { + MockERC20 usdc = new MockERC20("USDC", "USDC"); + WrappedLPNAVVault vault = new WrappedLPNAVVault( + usdc, + "Wrapped LP NAV Vault", + "wLPV", + admin + ); + vault.setDepositCap(1_000_000e18); + usdc.mint(user, 2_000_000e18); + vm.startPrank(user); + usdc.approve(address(vault), type(uint256).max); + vm.expectRevert(); + vault.deposit(1_500_000e18, user); + vm.stopPrank(); + } + + function test_nav_oracle_stale() public { + WLPNAVOracle oracle = new WLPNAVOracle(admin, 3600); + oracle.grantRole(oracle.KEEPER_ROLE(), relayer); + vm.prank(relayer); + oracle.submitAnswer(1e8); + assertFalse(oracle.isStale()); + vm.warp(block.timestamp + 4000); + assertTrue(oracle.isStale()); + } + + /// @notice Fuzz: distinct deposits yield distinct lockRefs and independent mints. + function testFuzz_lockRef_unique(uint128 amtA, uint128 amtB) public { + vm.assume(amtA > 0 && amtB > 0); + vm.assume(amtA != amtB); + + uint256 a = uint256(amtA) * 1e10; + uint256 b = uint256(amtB) * 1e10; + + lp.mint(user, a + b); + vm.startPrank(user); + lp.approve(address(locker), a + b); + bytes32 rA = locker.deposit(a); + bytes32 rB = locker.deposit(b); + vm.stopPrank(); + + assertTrue(rA != rB); + + vm.startPrank(relayer); + mintCtl.mintForLock(rA, user, a); + mintCtl.mintForLock(rB, user, b); + vm.stopPrank(); + + assertEq(wlp.balanceOf(user), a + b); + } +}