feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
This commit is contained in:
@@ -57,6 +57,9 @@ temp/
|
||||
# Node modules (if any)
|
||||
node_modules/
|
||||
|
||||
# TypeScript service emit (build in-tree)
|
||||
services/btc-intake/dist/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -139,11 +139,12 @@ forge script script/deploy/chains/DeployAllAdapters.s.sol:DeployAllAdapters \
|
||||
### **3. Configure Services**
|
||||
|
||||
```bash
|
||||
# Firefly (VMID 6202)
|
||||
ssh [email protected]
|
||||
# FireFly is currently validated only on VMID 6200.
|
||||
# 6201 is retired / standby, and 6202 / 6203 are not deployed.
|
||||
ssh [email protected]
|
||||
ff init alltra-bridge --multiparty
|
||||
|
||||
# Cacti (VMID 5201)
|
||||
# Cacti primary (VMID 5200)
|
||||
# Configure Cacti API server with Besu connector
|
||||
```
|
||||
|
||||
|
||||
@@ -78,10 +78,10 @@
|
||||
|
||||
| Framework | Type | Adapter | Status | Infrastructure |
|
||||
|-----------|------|---------|--------|----------------|
|
||||
| Firefly | Orchestration | FireflyAdapter | ✅ Created | ✅ VMIDs 6202, 6203 |
|
||||
| Cacti | Interoperability | CactiAdapter | 🔨 Plan | ✅ VMID 5201 |
|
||||
| Fabric | Permissioned | FabricAdapter | 🔨 Plan | 🔨 Deploy network |
|
||||
| Indy | Identity | IndyVerifier | 🔨 Plan | 🔨 Deploy network |
|
||||
| FireFly | Orchestration | FireflyAdapter | ✅ Created | `6200` live primary; `6201` retired / standby; `6202` and `6203` are not deployed |
|
||||
| Cacti | Interoperability | CactiAdapter | 🔨 Plan | `5200` live primary; `5201` / `5202` live Alltra/HYBX public Cacti surfaces with local `:4000` APIs |
|
||||
| Fabric | Permissioned | FabricAdapter | 🔨 Plan | `6000` live sample network; `6001` / `6002` are placeholders |
|
||||
| Indy | Identity | IndyVerifier | 🔨 Plan | `6400` live validator pool; `6401` / `6402` are placeholders |
|
||||
|
||||
---
|
||||
|
||||
@@ -172,11 +172,12 @@ chainRegistry.registerNonEVMChain(
|
||||
);
|
||||
```
|
||||
|
||||
### **4. Configure Firefly**
|
||||
### **4. Configure FireFly**
|
||||
|
||||
```bash
|
||||
# SSH to Firefly node (VMID 6202)
|
||||
ssh [email protected]
|
||||
# FireFly automation is validated only on the primary node today (VMID 6200)
|
||||
# Secondary FireFly nodes are not deployed; 6201 is retired / standby metadata.
|
||||
ssh [email protected]
|
||||
|
||||
# Initialize Firefly namespace
|
||||
ff init alltra-bridge --multiparty
|
||||
|
||||
@@ -192,11 +192,12 @@ chainRegistry.registerEVMChain(50, xdcAdapter, "https://explorer.xdc.network", 1
|
||||
chainRegistry.registerNonEVMChain("XRPL-Mainnet", ChainType.XRPL, xrplAdapter, "https://xrpscan.com", 1, 4, true, "");
|
||||
```
|
||||
|
||||
### **4. Configure Firefly**
|
||||
### **4. Configure FireFly**
|
||||
|
||||
```bash
|
||||
# On VMID 6202
|
||||
ssh [email protected]
|
||||
# FireFly is currently validated only on VMID 6200.
|
||||
# VMID 6201 is retired / standby, and 6202 / 6203 are not deployed.
|
||||
ssh [email protected]
|
||||
ff init alltra-bridge --multiparty
|
||||
ff accounts create --key /path/to/besu/key.json
|
||||
```
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[profile.default]
|
||||
src = "../contracts/bridge/atomic"
|
||||
test = "../test/bridge/atomic"
|
||||
out = "out"
|
||||
libs = ["../lib"]
|
||||
solc = "0.8.20"
|
||||
optimizer = true
|
||||
optimizer_runs = 200
|
||||
via_ir = true
|
||||
evm_version = "london"
|
||||
remappings = [
|
||||
"@openzeppelin/contracts/=../lib/openzeppelin-contracts/contracts/",
|
||||
"@openzeppelin/contracts-upgradeable/=../lib/openzeppelin-contracts-upgradeable/contracts/",
|
||||
"forge-std/=../lib/forge-std/src/",
|
||||
"ds-test/=../lib/forge-std/lib/ds-test/src/",
|
||||
"@emoney=../contracts/emoney/",
|
||||
"@emoney-scripts=../script/emoney/",
|
||||
"erc4626-tests/=../lib/openzeppelin-contracts/lib/erc4626-tests/",
|
||||
"openzeppelin-contracts-upgradeable/=../lib/openzeppelin-contracts-upgradeable/",
|
||||
"openzeppelin-contracts/=../lib/openzeppelin-contracts/"
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "Reference inventory moved out of smom-dbis-138/.env during dotenv cleanup. These are deployed/inventory addresses, not the minimal runtime env surface.",
|
||||
"updated": "2026-03-27",
|
||||
"updated": "2026-04-03",
|
||||
"chain138Inventory": {
|
||||
"COMPLIANCE_REGISTRY": "0xbc54fe2b6fda157c59d59826bcfdbcc654ec9ea1",
|
||||
"COMPLIANCE_REGISTRY_ADDRESS": "0xbc54fe2b6fda157c59d59826bcfdbcc654ec9ea1",
|
||||
@@ -11,13 +11,48 @@
|
||||
"TOKEN_IMPLEMENTATION": "0x0059e237973179146237aB49f1322E8197c22b21",
|
||||
"TOKEN_REGISTRY_ADDRESS": "0x91Efe92229dbf7C5B38D422621300956B55870Fa",
|
||||
"FEE_COLLECTOR_ADDRESS": "0xF78246eB94c6CB14018E507E60661314E5f4C53f",
|
||||
"COMPLIANT_USDT_V2": "0x9FBfab33882Efe0038DAa608185718b772EE5660",
|
||||
"COMPLIANT_USDC_V2": "0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d",
|
||||
"CUSDT_V2_ADDRESS_138": "0x9FBfab33882Efe0038DAa608185718b772EE5660",
|
||||
"CUSDC_V2_ADDRESS_138": "0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d",
|
||||
"UNIVERSAL_ASSET_REGISTRY": "0xAEE4b7fBe82E1F8295951584CBc772b8BBD68575",
|
||||
"GOVERNANCE_CONTROLLER": "0xA6891D5229f2181a34D4FF1B515c3Aa37dd90E0e",
|
||||
"BRIDGE_ORCHESTRATOR": "0x89aB428c437f23bAB9781ff8Db8D3848e27EeD6c",
|
||||
"ENHANCED_SWAP_ROUTER_V2_ADDRESS": "0xF1c93F54A5C2fc0d7766Ccb0Ad8f157DFB4C99Ce",
|
||||
"INTENT_BRIDGE_COORDINATOR_V2_ADDRESS": "0x7D0022B7e8360172fd9C0bB6778113b7Ea3674E7",
|
||||
"DODO_ROUTE_EXECUTOR_ADAPTER": "0x88495B3dccEA93b0633390fDE71992683121Fa62",
|
||||
"DODO_V3_ROUTE_EXECUTOR_ADAPTER": "0x9Cb97adD29c52e3B81989BcA2E33D46074B530eF",
|
||||
"UNISWAP_V3_ROUTE_EXECUTOR_ADAPTER": "0x960D6db4E78705f82995690548556fb2266308EA",
|
||||
"BALANCER_ROUTE_EXECUTOR_ADAPTER": "0x4E1B71B69188Ab45021c797039b4887a4924157A",
|
||||
"CURVE_ROUTE_EXECUTOR_ADAPTER": "0x5f0E07071c41ACcD2A1b1032D3bd49b323b9ADE6",
|
||||
"ONEINCH_ROUTE_EXECUTOR_ADAPTER": "0x8168083d29b3293F215392A49D16e7FeF4a02600",
|
||||
"UNISWAP_V3_ROUTER": "0xde9cD8ee2811E6E64a41D5F68Be315d33995975E",
|
||||
"UNISWAP_QUOTER_ADDRESS": "0x6abbB1CEb2468e748a03A00CD6aA9BFE893AFa1f",
|
||||
"CHAIN_138_UNISWAP_V3_FACTORY": "0x2f7219276e3ce367dB9ec74C1196a8ecEe67841C",
|
||||
"CHAIN_138_UNISWAP_V3_ROUTER": "0xde9cD8ee2811E6E64a41D5F68Be315d33995975E",
|
||||
"UNISWAP_V3_WETH_USDT_POOL": "0xa893add35aEfe6A6d858EB01828bE4592f12C9F5",
|
||||
"UNISWAP_V3_WETH_USDC_POOL": "0xEC745bfb6b3cd32f102d594E5F432d8d85B19391",
|
||||
"CHAIN138_UNISWAP_V3_NATIVE_FACTORY": "0x2f7219276e3ce367dB9ec74C1196a8ecEe67841C",
|
||||
"CHAIN138_UNISWAP_V3_NATIVE_NFT_DESCRIPTOR_LIBRARY": "0x6F5fdE32DD2aC66B27e296EC9D6F4E79A3dE2947",
|
||||
"CHAIN138_UNISWAP_V3_NATIVE_TOKEN_DESCRIPTOR": "0xca66DCAC4633555033F6fDDBE4234B6913c7ff51",
|
||||
"CHAIN138_UNISWAP_V3_NATIVE_POSITION_MANAGER": "0x31b68BE5af4Df565Ce261dfe53D529005D947B48",
|
||||
"CHAIN138_UNISWAP_V3_NATIVE_SWAP_ROUTER": "0xde9cD8ee2811E6E64a41D5F68Be315d33995975E",
|
||||
"CHAIN138_UNISWAP_V3_NATIVE_QUOTER_V2": "0x6abbB1CEb2468e748a03A00CD6aA9BFE893AFa1f",
|
||||
"CHAIN138_UNISWAP_V3_NATIVE_WETH_USDT_POOL": "0xa893add35aEfe6A6d858EB01828bE4592f12C9F5",
|
||||
"CHAIN138_UNISWAP_V3_NATIVE_WETH_USDC_POOL": "0xEC745bfb6b3cd32f102d594E5F432d8d85B19391",
|
||||
"BALANCER_VAULT": "0x96423d7C1727698D8a25EbFB88131e9422d1a3C3",
|
||||
"BALANCER_WETH_USDT_POOL_ID": "0x877cd220759e8c94b82f55450c85d382ae06856c426b56d93092a420facbc324",
|
||||
"BALANCER_WETH_USDC_POOL_ID": "0xd8dfb18a6baf9b29d8c2dbd74639db87ac558af120df5261dab8e2a5de69013b",
|
||||
"CURVE_3POOL": "0xE440Ec15805BE4C7BabCD17A63B8C8A08a492e0f",
|
||||
"ONEINCH_ROUTER": "0x500B84b1Bc6F59C1898a5Fe538eA20A758757A4F",
|
||||
"CROSS_CHAIN_FLASH_BRIDGE_ADAPTER": "0xBe9e0B2d4cF6A3b2994d6f2f0904D2B165eB8ffC",
|
||||
"CROSS_CHAIN_FLASH_REPAY_RECEIVER": "0xD084b68cB4B1ef2cBA09CF99FB1B6552fd9b4859",
|
||||
"CROSS_CHAIN_FLASH_VAULT_CREDIT_RECEIVER": "0x89F7a1fcbBe104BeE96Da4b4b6b7d3AF85f7E661",
|
||||
"TRANSACTION_MIRROR_ADDRESS": "0x7131F887DBEEb2e44c1Ed267D2A68b5b83285afc",
|
||||
"PAYMENT_CHANNEL_MANAGER": "0x302aF72966aFd21C599051277a48DAa7f01a5f54",
|
||||
"GENERIC_STATE_CHANNEL_MANAGER": "0xe5e3bB424c8a0259FDE23F0A58F7e36f73B90aBd",
|
||||
"ADDRESS_MAPPER": "0xe48E3f248698610e18Db865457fcd935Bb3da856",
|
||||
"ADDRESS_MAPPER": "0x439Fcb2d2ab2f890DCcAE50461Fa7d978F9Ffe1A",
|
||||
"ADDRESS_MAPPER_LEGACY_DUPLICATE": "0xe48E3f248698610e18Db865457fcd935Bb3da856",
|
||||
"MIRROR_MANAGER": "0x6eD905A30c552a6e003061A38FD52A5A427beE56",
|
||||
"ORACLE_AGGREGATOR_ADDRESS": "0x99b3511a2d315a497c8112c1fdd8d508d4b1e506",
|
||||
"ORACLE_PROXY_ADDRESS": "0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6",
|
||||
@@ -35,9 +70,14 @@
|
||||
"UNIVERSAL_CCIP_BRIDGE_DETERMINISTIC": "0x532DE218b94993446Be30eC894442f911499f6a3",
|
||||
"MIRROR_REGISTRY": "0x6427F9739e6B6c3dDb4E94fEfeBcdF35549549d8",
|
||||
"ALLTRA_ADAPTER": "0x66FEBA2fC9a0B47F26DD4284DAd24F970436B8Dc",
|
||||
"POOL_CUSDTCUSDC": "0xff8d3b8fDF7B112759F076B69f4271D4209C0849",
|
||||
"POOL_CUSDTUSDT": "0x6fc60DEDc92a2047062294488539992710b99D71",
|
||||
"POOL_CUSDCUSDC": "0x0309178ae30302D83c76d6Dd402a684eF3160eec",
|
||||
"DODO_DVM_FACTORY": "0xc93870594C7f83A0aE076c2e30b494Efc526b68E",
|
||||
"DODO_VENDING_MACHINE_ADDRESS": "0xb6D9EF3575bc48De3f011C310DC24d87bEC6087C",
|
||||
"DODO_PMM_INTEGRATION": "0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895",
|
||||
"DODO_PMM_PROVIDER": "0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e",
|
||||
"CAUSDT_ADDRESS_138": "0x5fdDF65733e3d590463F68f93Cf16E8c04081271",
|
||||
"POOL_CUSDTCUSDC": "0x9e89bAe009adf128782E19e8341996c596ac40dC",
|
||||
"POOL_CUSDTUSDT": "0x866Cb44b59303d8dc5f4F9E3E7A8e8b0bf238d66",
|
||||
"POOL_CUSDCUSDC": "0xc39B7D0F40838cbFb54649d327f49a6DAC964062",
|
||||
"CHAIN_REGISTRY_ADDRESS_138": "0x6949137625CA923A4e9C80D5bc7DF673f9bbb84F",
|
||||
"TRUTH_ADAPTER_ADDRESS_138": "0x7880Ef14887a0567807AcF785eC92553D014930f",
|
||||
"XRPL_ADAPTER_CHAIN138": "0x351f207F2DE66bF166ec730a0133613A10691439",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Besu — VMID 2103 besu-rpc-core-thirdweb (Thirdweb admin / core lane)
|
||||
# LAN: 192.168.11.217:8545 / :8546 — NPM: rpc.tw-core.d-bis.org / wss.tw-core.d-bis.org
|
||||
# Same RPC API surface as config-rpc-core.toml (ADMIN, TXPOOL, QBFT, DEBUG, TRACE);
|
||||
# CORS is open for browser / Thirdweb tooling behind NPM TLS.
|
||||
data-path="/data/besu"
|
||||
genesis-file="/genesis/genesis.json"
|
||||
|
||||
network-id=138
|
||||
p2p-host="0.0.0.0"
|
||||
p2p-port=30303
|
||||
|
||||
sync-mode="FULL"
|
||||
data-storage-format="FOREST"
|
||||
|
||||
rpc-http-enabled=true
|
||||
rpc-http-host="0.0.0.0"
|
||||
rpc-http-port=8545
|
||||
rpc-http-api=["ETH","NET","WEB3","TXPOOL","QBFT","ADMIN","DEBUG","TRACE"]
|
||||
rpc-http-cors-origins=["*"]
|
||||
rpc-ws-enabled=true
|
||||
rpc-ws-host="0.0.0.0"
|
||||
rpc-ws-port=8546
|
||||
rpc-ws-api=["ETH","NET","WEB3","TXPOOL","QBFT","ADMIN"]
|
||||
rpc-ws-origins=["*"]
|
||||
|
||||
metrics-enabled=true
|
||||
metrics-port=9545
|
||||
metrics-host="0.0.0.0"
|
||||
metrics-push-enabled=false
|
||||
|
||||
logging="WARN"
|
||||
|
||||
permissions-nodes-config-file-enabled=true
|
||||
permissions-nodes-config-file="/var/lib/besu/permissions/permissions-nodes.toml"
|
||||
permissions-accounts-config-file-enabled=true
|
||||
permissions-accounts-config-file="/permissions/permissions-accounts.toml"
|
||||
|
||||
tx-pool-max-size=8192
|
||||
tx-pool-limit-by-account-percentage=0.5
|
||||
tx-pool-price-bump=10
|
||||
|
||||
bootnodes=[]
|
||||
|
||||
static-nodes-file="/var/lib/besu/static-nodes.json"
|
||||
|
||||
discovery-enabled=false
|
||||
|
||||
max-peers=32
|
||||
|
||||
rpc-http-timeout=120
|
||||
@@ -1,22 +1,29 @@
|
||||
{
|
||||
"description": "Canonical runtime-used Chain 138 env surface for token-aggregation and PMM-aware services.",
|
||||
"updated": "2026-03-27",
|
||||
"updated": "2026-04-03",
|
||||
"runtimeUsed": {
|
||||
"DODO_PMM_INTEGRATION": "0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d",
|
||||
"DODO_PMM_INTEGRATION_ADDRESS": "0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d",
|
||||
"DODO_PMM_INTEGRATION": "0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895",
|
||||
"DODO_PMM_INTEGRATION_ADDRESS": "0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895",
|
||||
"OFFICIAL_USDT_ADDRESS": "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1",
|
||||
"OFFICIAL_USDC_ADDRESS": "0x71D6687F38b93CCad569Fa6352c876eea967201b",
|
||||
"CCIPWETH9_BRIDGE_CHAIN138": "0xcacfd227A040002e49e2e01626363071324f820a",
|
||||
"CCIPWETH10_BRIDGE_CHAIN138": "0xe0E93247376aa097dB308B92e6Ba36bA015535D0",
|
||||
"CHAIN_138_DODO_PMM_INTEGRATION": "0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d",
|
||||
"CHAIN_138_DODO_VENDING_MACHINE": "0xB16c3D48A111714B1795E58341FeFDd643Ab01ab",
|
||||
"CHAIN_138_DODO_PMM_INTEGRATION": "0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895",
|
||||
"CHAIN_138_DODO_VENDING_MACHINE": "0xb6D9EF3575bc48De3f011C310DC24d87bEC6087C",
|
||||
"UNIVERSAL_CCIP_BRIDGE": "0xCd42e8eD79Dc50599535d1de48d3dAFa0BE156F8",
|
||||
"UNISWAP_V3_ROUTER": "0xde9cD8ee2811E6E64a41D5F68Be315d33995975E",
|
||||
"UNISWAP_QUOTER_ADDRESS": "0x6abbB1CEb2468e748a03A00CD6aA9BFE893AFa1f",
|
||||
"CHAIN_138_UNISWAP_V3_FACTORY": "0x2f7219276e3ce367dB9ec74C1196a8ecEe67841C",
|
||||
"CHAIN_138_UNISWAP_V3_ROUTER": "0xde9cD8ee2811E6E64a41D5F68Be315d33995975E",
|
||||
"UNISWAP_V3_WETH_USDT_POOL": "0xa893add35aEfe6A6d858EB01828bE4592f12C9F5",
|
||||
"UNISWAP_V3_WETH_USDC_POOL": "0xEC745bfb6b3cd32f102d594E5F432d8d85B19391",
|
||||
"RPC_URL_138": "http://192.168.11.211:8545",
|
||||
"CHAIN138_RPC_URL": "http://192.168.11.211:8545",
|
||||
"RPC_URL_138_PUBLIC": "https://rpc.public-0138.defi-oracle.io",
|
||||
"CHAIN138_RPC_URL_PUBLIC": "https://rpc.public-0138.defi-oracle.io",
|
||||
"CUSDT_ADDRESS_138": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22",
|
||||
"CUSDC_ADDRESS_138": "0xf22258f57794CC8E06237084b353Ab30fFfa640b",
|
||||
"CAUSDT_ADDRESS_138": "0x5fdDF65733e3d590463F68f93Cf16E8c04081271",
|
||||
"CEURC_ADDRESS_138": "0x8085961F9cF02b4d800A3c6d386D31da4B34266a",
|
||||
"CEURT_ADDRESS_138": "0xdf4b71c61E5912712C1Bdd451416B9aC26949d72",
|
||||
"CGBPC_ADDRESS_138": "0x003960f16D9d34F2e98d62723B6721Fb92074aD2",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "../registry/UniversalAssetRegistry.sol";
|
||||
import "./UniversalCCIPBridge.sol";
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
import "../ccip/IRouterClient.sol";
|
||||
import "./integration/ICWReserveVerifier.sol";
|
||||
|
||||
/**
|
||||
* @title CWMultiTokenBridgeL1
|
||||
* @notice Chain 138 side escrow bridge for non-prefunded c* -> cW* routing.
|
||||
* @dev Locks canonical c* tokens on send and releases them on return messages.
|
||||
*/
|
||||
contract CWMultiTokenBridgeL1 {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
IRouterClient public sendRouter;
|
||||
address public receiveRouter;
|
||||
address public feeToken;
|
||||
address public admin;
|
||||
address public reserveVerifier;
|
||||
|
||||
struct DestinationConfig {
|
||||
address receiverBridge;
|
||||
bool enabled;
|
||||
}
|
||||
|
||||
mapping(address => mapping(uint64 => DestinationConfig)) public destinations;
|
||||
mapping(bytes32 => bool) public processed;
|
||||
mapping(address => bool) public supportedCanonicalToken;
|
||||
mapping(address => bool) public paused;
|
||||
mapping(address => uint256) public lockedBalance;
|
||||
mapping(address => uint256) public totalOutstanding;
|
||||
mapping(address => mapping(uint64 => uint256)) public outstandingMinted;
|
||||
mapping(address => mapping(uint64 => uint256)) public maxOutstanding;
|
||||
|
||||
event Locked(address indexed canonicalToken, address indexed user, uint256 amount);
|
||||
event Released(address indexed canonicalToken, address indexed recipient, uint256 amount);
|
||||
event MessageSent(
|
||||
bytes32 indexed messageId,
|
||||
address indexed canonicalToken,
|
||||
uint64 indexed destinationChainSelector,
|
||||
address recipient,
|
||||
uint256 amount
|
||||
);
|
||||
event DestinationConfigured(address indexed canonicalToken, uint64 indexed chainSelector, address receiverBridge, bool enabled);
|
||||
event SupportedCanonicalTokenConfigured(address indexed canonicalToken, bool enabled);
|
||||
event TokenPauseUpdated(address indexed canonicalToken, bool paused);
|
||||
event MaxOutstandingUpdated(address indexed canonicalToken, uint64 indexed chainSelector, uint256 maxOutstandingAmount);
|
||||
event OutstandingUpdated(
|
||||
address indexed canonicalToken,
|
||||
uint64 indexed chainSelector,
|
||||
uint256 chainOutstanding,
|
||||
uint256 totalOutstandingAmount,
|
||||
uint256 lockedBalanceAmount
|
||||
);
|
||||
event SendRouterUpdated(address indexed newRouter);
|
||||
event ReceiveRouterUpdated(address indexed newRouter);
|
||||
event FeeTokenUpdated(address indexed newFeeToken);
|
||||
event AdminChanged(address indexed newAdmin);
|
||||
event ReserveVerifierUpdated(address indexed newVerifier);
|
||||
|
||||
modifier onlyAdmin() {
|
||||
require(msg.sender == admin, "CWMultiTokenBridgeL1: only admin");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyReceiveRouter() {
|
||||
require(msg.sender == receiveRouter, "CWMultiTokenBridgeL1: only receive router");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _sendRouter, address _receiveRouter, address _feeToken) {
|
||||
require(_sendRouter != address(0), "CWMultiTokenBridgeL1: zero send router");
|
||||
require(_receiveRouter != address(0), "CWMultiTokenBridgeL1: zero receive router");
|
||||
sendRouter = IRouterClient(_sendRouter);
|
||||
receiveRouter = _receiveRouter;
|
||||
feeToken = _feeToken;
|
||||
admin = msg.sender;
|
||||
}
|
||||
|
||||
function configureDestination(
|
||||
address canonicalToken,
|
||||
uint64 chainSelector,
|
||||
address receiverBridge,
|
||||
bool enabled
|
||||
) external onlyAdmin {
|
||||
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
|
||||
require(receiverBridge != address(0), "CWMultiTokenBridgeL1: zero bridge");
|
||||
destinations[canonicalToken][chainSelector] = DestinationConfig({
|
||||
receiverBridge: receiverBridge,
|
||||
enabled: enabled
|
||||
});
|
||||
emit DestinationConfigured(canonicalToken, chainSelector, receiverBridge, enabled);
|
||||
}
|
||||
|
||||
function configureSupportedCanonicalToken(address canonicalToken, bool enabled) external onlyAdmin {
|
||||
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
|
||||
if (!enabled) {
|
||||
require(lockedBalance[canonicalToken] == 0, "CWMultiTokenBridgeL1: token still locked");
|
||||
require(totalOutstanding[canonicalToken] == 0, "CWMultiTokenBridgeL1: token still outstanding");
|
||||
}
|
||||
supportedCanonicalToken[canonicalToken] = enabled;
|
||||
emit SupportedCanonicalTokenConfigured(canonicalToken, enabled);
|
||||
}
|
||||
|
||||
function setTokenPaused(address canonicalToken, bool isPaused) external onlyAdmin {
|
||||
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
|
||||
paused[canonicalToken] = isPaused;
|
||||
emit TokenPauseUpdated(canonicalToken, isPaused);
|
||||
}
|
||||
|
||||
function setMaxOutstanding(address canonicalToken, uint64 chainSelector, uint256 amount) external onlyAdmin {
|
||||
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
|
||||
maxOutstanding[canonicalToken][chainSelector] = amount;
|
||||
emit MaxOutstandingUpdated(canonicalToken, chainSelector, amount);
|
||||
}
|
||||
|
||||
function setSendRouter(address newRouter) external onlyAdmin {
|
||||
require(newRouter != address(0), "CWMultiTokenBridgeL1: zero router");
|
||||
sendRouter = IRouterClient(newRouter);
|
||||
emit SendRouterUpdated(newRouter);
|
||||
}
|
||||
|
||||
function setReceiveRouter(address newRouter) external onlyAdmin {
|
||||
require(newRouter != address(0), "CWMultiTokenBridgeL1: zero router");
|
||||
receiveRouter = newRouter;
|
||||
emit ReceiveRouterUpdated(newRouter);
|
||||
}
|
||||
|
||||
function setFeeToken(address newFeeToken) external onlyAdmin {
|
||||
feeToken = newFeeToken;
|
||||
emit FeeTokenUpdated(newFeeToken);
|
||||
}
|
||||
|
||||
function changeAdmin(address newAdmin) external onlyAdmin {
|
||||
require(newAdmin != address(0), "CWMultiTokenBridgeL1: zero admin");
|
||||
admin = newAdmin;
|
||||
emit AdminChanged(newAdmin);
|
||||
}
|
||||
|
||||
function setReserveVerifier(address newVerifier) external onlyAdmin {
|
||||
reserveVerifier = newVerifier;
|
||||
emit ReserveVerifierUpdated(newVerifier);
|
||||
}
|
||||
|
||||
function calculateFee(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector,
|
||||
address recipient,
|
||||
uint256 amount
|
||||
) external view returns (uint256) {
|
||||
DestinationConfig memory dest = destinations[canonicalToken][destinationChainSelector];
|
||||
require(dest.enabled, "CWMultiTokenBridgeL1: destination disabled");
|
||||
return sendRouter.getFee(destinationChainSelector, _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount));
|
||||
}
|
||||
|
||||
function availableToMint(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector
|
||||
) public view returns (uint256) {
|
||||
return _availableToMint(
|
||||
canonicalToken,
|
||||
destinationChainSelector,
|
||||
lockedBalance[canonicalToken],
|
||||
totalOutstanding[canonicalToken],
|
||||
outstandingMinted[canonicalToken][destinationChainSelector]
|
||||
);
|
||||
}
|
||||
|
||||
function lockAndSend(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector,
|
||||
address recipient,
|
||||
uint256 amount
|
||||
) external payable returns (bytes32 messageId) {
|
||||
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
|
||||
require(recipient != address(0), "CWMultiTokenBridgeL1: zero recipient");
|
||||
require(amount > 0, "CWMultiTokenBridgeL1: zero amount");
|
||||
require(supportedCanonicalToken[canonicalToken], "CWMultiTokenBridgeL1: unsupported token");
|
||||
require(!paused[canonicalToken], "CWMultiTokenBridgeL1: token paused");
|
||||
|
||||
DestinationConfig memory dest = destinations[canonicalToken][destinationChainSelector];
|
||||
require(dest.enabled, "CWMultiTokenBridgeL1: destination disabled");
|
||||
|
||||
IERC20(canonicalToken).safeTransferFrom(msg.sender, address(this), amount);
|
||||
uint256 nextLockedBalance = lockedBalance[canonicalToken] + amount;
|
||||
uint256 currentTotalOutstanding = totalOutstanding[canonicalToken];
|
||||
uint256 currentChainOutstanding = outstandingMinted[canonicalToken][destinationChainSelector];
|
||||
require(
|
||||
amount <= _availableToMint(
|
||||
canonicalToken,
|
||||
destinationChainSelector,
|
||||
nextLockedBalance,
|
||||
currentTotalOutstanding,
|
||||
currentChainOutstanding
|
||||
),
|
||||
"CWMultiTokenBridgeL1: exceeds escrow capacity"
|
||||
);
|
||||
|
||||
lockedBalance[canonicalToken] = nextLockedBalance;
|
||||
totalOutstanding[canonicalToken] = currentTotalOutstanding + amount;
|
||||
outstandingMinted[canonicalToken][destinationChainSelector] = currentChainOutstanding + amount;
|
||||
|
||||
if (reserveVerifier != address(0) && !ICWReserveVerifier(reserveVerifier).verifyLock(canonicalToken, destinationChainSelector, amount)) {
|
||||
revert("CWMultiTokenBridgeL1: reserve verification failed");
|
||||
}
|
||||
|
||||
emit Locked(canonicalToken, msg.sender, amount);
|
||||
emit OutstandingUpdated(
|
||||
canonicalToken,
|
||||
destinationChainSelector,
|
||||
outstandingMinted[canonicalToken][destinationChainSelector],
|
||||
totalOutstanding[canonicalToken],
|
||||
lockedBalance[canonicalToken]
|
||||
);
|
||||
|
||||
IRouterClient.EVM2AnyMessage memory message =
|
||||
_buildMessage(dest.receiverBridge, canonicalToken, recipient, amount);
|
||||
uint256 fee = sendRouter.getFee(destinationChainSelector, message);
|
||||
|
||||
_collectAndApproveFee(fee);
|
||||
|
||||
(messageId, ) = feeToken == address(0)
|
||||
? sendRouter.ccipSend{value: fee}(destinationChainSelector, message)
|
||||
: sendRouter.ccipSend(destinationChainSelector, message);
|
||||
|
||||
emit MessageSent(messageId, canonicalToken, destinationChainSelector, recipient, amount);
|
||||
_refundNativeExcess(fee);
|
||||
return messageId;
|
||||
}
|
||||
|
||||
function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyReceiveRouter {
|
||||
require(!processed[message.messageId], "CWMultiTokenBridgeL1: replayed");
|
||||
|
||||
(address canonicalToken, address recipient, uint256 amount) =
|
||||
abi.decode(message.data, (address, address, uint256));
|
||||
require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token");
|
||||
require(recipient != address(0), "CWMultiTokenBridgeL1: zero recipient");
|
||||
require(amount > 0, "CWMultiTokenBridgeL1: zero amount");
|
||||
require(supportedCanonicalToken[canonicalToken], "CWMultiTokenBridgeL1: unsupported token");
|
||||
require(!paused[canonicalToken], "CWMultiTokenBridgeL1: token paused");
|
||||
|
||||
DestinationConfig memory peer = destinations[canonicalToken][message.sourceChainSelector];
|
||||
require(peer.enabled, "CWMultiTokenBridgeL1: peer disabled");
|
||||
require(_decodeSender(message.sender) == peer.receiverBridge, "CWMultiTokenBridgeL1: peer mismatch");
|
||||
require(outstandingMinted[canonicalToken][message.sourceChainSelector] >= amount, "CWMultiTokenBridgeL1: outstanding underflow");
|
||||
require(totalOutstanding[canonicalToken] >= amount, "CWMultiTokenBridgeL1: total outstanding underflow");
|
||||
require(lockedBalance[canonicalToken] >= amount, "CWMultiTokenBridgeL1: locked underflow");
|
||||
|
||||
processed[message.messageId] = true;
|
||||
outstandingMinted[canonicalToken][message.sourceChainSelector] -= amount;
|
||||
totalOutstanding[canonicalToken] -= amount;
|
||||
lockedBalance[canonicalToken] -= amount;
|
||||
|
||||
IERC20(canonicalToken).safeTransfer(recipient, amount);
|
||||
emit Released(canonicalToken, recipient, amount);
|
||||
emit OutstandingUpdated(
|
||||
canonicalToken,
|
||||
message.sourceChainSelector,
|
||||
outstandingMinted[canonicalToken][message.sourceChainSelector],
|
||||
totalOutstanding[canonicalToken],
|
||||
lockedBalance[canonicalToken]
|
||||
);
|
||||
}
|
||||
|
||||
function withdrawToken(address token, address to, uint256 amount) external onlyAdmin {
|
||||
if (supportedCanonicalToken[token] || lockedBalance[token] > 0) {
|
||||
uint256 currentBalance = IERC20(token).balanceOf(address(this));
|
||||
require(currentBalance >= lockedBalance[token], "CWMultiTokenBridgeL1: escrow invariant broken");
|
||||
require(amount <= currentBalance - lockedBalance[token], "CWMultiTokenBridgeL1: amount locked");
|
||||
}
|
||||
IERC20(token).safeTransfer(to, amount);
|
||||
}
|
||||
|
||||
function withdrawNative(address payable to, uint256 amount) external onlyAdmin {
|
||||
(bool ok, ) = to.call{value: amount}("");
|
||||
require(ok, "CWMultiTokenBridgeL1: native transfer failed");
|
||||
}
|
||||
|
||||
receive() external payable {}
|
||||
|
||||
function _buildMessage(
|
||||
address receiverBridge,
|
||||
address canonicalToken,
|
||||
address recipient,
|
||||
uint256 amount
|
||||
) internal view returns (IRouterClient.EVM2AnyMessage memory message) {
|
||||
message = IRouterClient.EVM2AnyMessage({
|
||||
receiver: abi.encode(receiverBridge),
|
||||
data: abi.encode(canonicalToken, recipient, amount),
|
||||
tokenAmounts: new IRouterClient.TokenAmount[](0),
|
||||
feeToken: feeToken,
|
||||
extraArgs: ""
|
||||
});
|
||||
}
|
||||
|
||||
function _collectAndApproveFee(uint256 fee) internal {
|
||||
if (fee == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (feeToken == address(0)) {
|
||||
require(msg.value >= fee, "CWMultiTokenBridgeL1: insufficient native fee");
|
||||
return;
|
||||
}
|
||||
|
||||
IERC20 feeErc20 = IERC20(feeToken);
|
||||
feeErc20.safeTransferFrom(msg.sender, address(this), fee);
|
||||
feeErc20.forceApprove(address(sendRouter), fee);
|
||||
}
|
||||
|
||||
function _refundNativeExcess(uint256 feeUsed) internal {
|
||||
if (feeToken != address(0) || msg.value <= feeUsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint256 refund = msg.value - feeUsed;
|
||||
(bool ok, ) = payable(msg.sender).call{value: refund}("");
|
||||
require(ok, "CWMultiTokenBridgeL1: refund failed");
|
||||
}
|
||||
|
||||
function _decodeSender(bytes memory senderData) internal pure returns (address senderAddress) {
|
||||
if (senderData.length == 0) {
|
||||
return address(0);
|
||||
}
|
||||
senderAddress = abi.decode(senderData, (address));
|
||||
}
|
||||
|
||||
function _availableToMint(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector,
|
||||
uint256 lockedBalanceAmount,
|
||||
uint256 totalOutstandingAmount,
|
||||
uint256 chainOutstanding
|
||||
) internal view returns (uint256) {
|
||||
if (!supportedCanonicalToken[canonicalToken]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint256 globalAvailable = lockedBalanceAmount > totalOutstandingAmount
|
||||
? lockedBalanceAmount - totalOutstandingAmount
|
||||
: 0;
|
||||
|
||||
uint256 chainCap = maxOutstanding[canonicalToken][destinationChainSelector];
|
||||
if (chainCap == 0) {
|
||||
return globalAvailable;
|
||||
}
|
||||
|
||||
if (chainOutstanding >= chainCap) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint256 chainAvailable = chainCap - chainOutstanding;
|
||||
return globalAvailable < chainAvailable ? globalAvailable : chainAvailable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
import "../ccip/IRouterClient.sol";
|
||||
|
||||
interface ICWMintBurnToken is IERC20 {
|
||||
function mint(address to, uint256 amount) external;
|
||||
function burnFrom(address from, uint256 amount) external;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title CWMultiTokenBridgeL2
|
||||
* @notice Destination/public-chain bridge for minting and burning cW* assets without prefunding.
|
||||
* @dev Supports multiple canonical -> mirrored token pairs behind one bridge address.
|
||||
*/
|
||||
contract CWMultiTokenBridgeL2 {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
IRouterClient public sendRouter;
|
||||
address public receiveRouter;
|
||||
address public feeToken;
|
||||
address public admin;
|
||||
|
||||
struct DestinationConfig {
|
||||
address receiverBridge;
|
||||
bool enabled;
|
||||
}
|
||||
|
||||
mapping(uint64 => DestinationConfig) public destinations;
|
||||
mapping(address => address) public canonicalToMirrored;
|
||||
mapping(address => address) public mirroredToCanonical;
|
||||
mapping(bytes32 => bool) public processed;
|
||||
mapping(address => bool) public tokenPairFrozen;
|
||||
mapping(uint64 => bool) public destinationFrozen;
|
||||
mapping(address => bool) public paused;
|
||||
mapping(address => uint256) public mintedTotal;
|
||||
mapping(address => uint256) public burnedTotal;
|
||||
|
||||
event TokenPairConfigured(address indexed canonicalToken, address indexed mirroredToken);
|
||||
event DestinationConfigured(uint64 indexed chainSelector, address receiverBridge, bool enabled);
|
||||
event Minted(address indexed canonicalToken, address indexed mirroredToken, address indexed recipient, uint256 amount);
|
||||
event Burned(address indexed canonicalToken, address indexed mirroredToken, address indexed user, uint256 amount);
|
||||
event MessageSent(
|
||||
bytes32 indexed messageId,
|
||||
address indexed canonicalToken,
|
||||
address indexed mirroredToken,
|
||||
uint64 destinationChainSelector,
|
||||
address recipient,
|
||||
uint256 amount
|
||||
);
|
||||
event TokenPairFrozen(address indexed canonicalToken, address indexed mirroredToken);
|
||||
event DestinationFrozen(uint64 indexed chainSelector, address receiverBridge);
|
||||
event MirroredTokenPauseUpdated(address indexed mirroredToken, bool paused);
|
||||
event SupplyAccountingUpdated(
|
||||
address indexed canonicalToken,
|
||||
address indexed mirroredToken,
|
||||
uint256 mintedTotalAmount,
|
||||
uint256 burnedTotalAmount,
|
||||
uint256 liveSupply
|
||||
);
|
||||
event SendRouterUpdated(address indexed newRouter);
|
||||
event ReceiveRouterUpdated(address indexed newRouter);
|
||||
event FeeTokenUpdated(address indexed newFeeToken);
|
||||
event AdminChanged(address indexed newAdmin);
|
||||
|
||||
modifier onlyAdmin() {
|
||||
require(msg.sender == admin, "CWMultiTokenBridgeL2: only admin");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyReceiveRouter() {
|
||||
require(msg.sender == receiveRouter, "CWMultiTokenBridgeL2: only receive router");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _sendRouter, address _receiveRouter, address _feeToken) {
|
||||
require(_sendRouter != address(0), "CWMultiTokenBridgeL2: zero send router");
|
||||
require(_receiveRouter != address(0), "CWMultiTokenBridgeL2: zero receive router");
|
||||
sendRouter = IRouterClient(_sendRouter);
|
||||
receiveRouter = _receiveRouter;
|
||||
feeToken = _feeToken;
|
||||
admin = msg.sender;
|
||||
}
|
||||
|
||||
function configureTokenPair(address canonicalToken, address mirroredToken) external onlyAdmin {
|
||||
require(canonicalToken != address(0), "CWMultiTokenBridgeL2: zero canonical");
|
||||
require(mirroredToken != address(0), "CWMultiTokenBridgeL2: zero mirrored");
|
||||
require(!tokenPairFrozen[canonicalToken], "CWMultiTokenBridgeL2: token pair frozen");
|
||||
|
||||
address previousCanonical = mirroredToCanonical[mirroredToken];
|
||||
if (previousCanonical != address(0) && previousCanonical != canonicalToken) {
|
||||
require(!tokenPairFrozen[previousCanonical], "CWMultiTokenBridgeL2: token pair frozen");
|
||||
delete canonicalToMirrored[previousCanonical];
|
||||
}
|
||||
|
||||
canonicalToMirrored[canonicalToken] = mirroredToken;
|
||||
mirroredToCanonical[mirroredToken] = canonicalToken;
|
||||
emit TokenPairConfigured(canonicalToken, mirroredToken);
|
||||
}
|
||||
|
||||
function configureDestination(uint64 chainSelector, address receiverBridge, bool enabled) external onlyAdmin {
|
||||
require(receiverBridge != address(0), "CWMultiTokenBridgeL2: zero bridge");
|
||||
require(!destinationFrozen[chainSelector], "CWMultiTokenBridgeL2: destination frozen");
|
||||
destinations[chainSelector] = DestinationConfig({
|
||||
receiverBridge: receiverBridge,
|
||||
enabled: enabled
|
||||
});
|
||||
emit DestinationConfigured(chainSelector, receiverBridge, enabled);
|
||||
}
|
||||
|
||||
function freezeTokenPair(address canonicalToken) external onlyAdmin {
|
||||
address mirroredToken = canonicalToMirrored[canonicalToken];
|
||||
require(mirroredToken != address(0), "CWMultiTokenBridgeL2: token not configured");
|
||||
tokenPairFrozen[canonicalToken] = true;
|
||||
emit TokenPairFrozen(canonicalToken, mirroredToken);
|
||||
}
|
||||
|
||||
function freezeDestination(uint64 chainSelector) external onlyAdmin {
|
||||
address receiverBridge = destinations[chainSelector].receiverBridge;
|
||||
require(receiverBridge != address(0), "CWMultiTokenBridgeL2: destination not configured");
|
||||
destinationFrozen[chainSelector] = true;
|
||||
emit DestinationFrozen(chainSelector, receiverBridge);
|
||||
}
|
||||
|
||||
function setTokenPaused(address mirroredToken, bool isPaused) external onlyAdmin {
|
||||
require(mirroredToCanonical[mirroredToken] != address(0), "CWMultiTokenBridgeL2: token not configured");
|
||||
paused[mirroredToken] = isPaused;
|
||||
emit MirroredTokenPauseUpdated(mirroredToken, isPaused);
|
||||
}
|
||||
|
||||
function setSendRouter(address newRouter) external onlyAdmin {
|
||||
require(newRouter != address(0), "CWMultiTokenBridgeL2: zero router");
|
||||
sendRouter = IRouterClient(newRouter);
|
||||
emit SendRouterUpdated(newRouter);
|
||||
}
|
||||
|
||||
function setReceiveRouter(address newRouter) external onlyAdmin {
|
||||
require(newRouter != address(0), "CWMultiTokenBridgeL2: zero router");
|
||||
receiveRouter = newRouter;
|
||||
emit ReceiveRouterUpdated(newRouter);
|
||||
}
|
||||
|
||||
function setFeeToken(address newFeeToken) external onlyAdmin {
|
||||
feeToken = newFeeToken;
|
||||
emit FeeTokenUpdated(newFeeToken);
|
||||
}
|
||||
|
||||
function changeAdmin(address newAdmin) external onlyAdmin {
|
||||
require(newAdmin != address(0), "CWMultiTokenBridgeL2: zero admin");
|
||||
admin = newAdmin;
|
||||
emit AdminChanged(newAdmin);
|
||||
}
|
||||
|
||||
function calculateFee(
|
||||
address mirroredToken,
|
||||
uint64 destinationChainSelector,
|
||||
address recipient,
|
||||
uint256 amount
|
||||
) external view returns (uint256) {
|
||||
address canonicalToken = mirroredToCanonical[mirroredToken];
|
||||
require(canonicalToken != address(0), "CWMultiTokenBridgeL2: token not configured");
|
||||
DestinationConfig memory dest = destinations[destinationChainSelector];
|
||||
require(dest.enabled, "CWMultiTokenBridgeL2: destination disabled");
|
||||
return sendRouter.getFee(destinationChainSelector, _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount));
|
||||
}
|
||||
|
||||
function circulatingSupply(address mirroredToken) external view returns (uint256) {
|
||||
uint256 totalMinted = mintedTotal[mirroredToken];
|
||||
uint256 totalBurned = burnedTotal[mirroredToken];
|
||||
return totalMinted > totalBurned ? totalMinted - totalBurned : 0;
|
||||
}
|
||||
|
||||
function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyReceiveRouter {
|
||||
require(!processed[message.messageId], "CWMultiTokenBridgeL2: replayed");
|
||||
|
||||
(address canonicalToken, address recipient, uint256 amount) =
|
||||
abi.decode(message.data, (address, address, uint256));
|
||||
require(recipient != address(0), "CWMultiTokenBridgeL2: zero recipient");
|
||||
require(amount > 0, "CWMultiTokenBridgeL2: zero amount");
|
||||
|
||||
address mirroredToken = canonicalToMirrored[canonicalToken];
|
||||
require(mirroredToken != address(0), "CWMultiTokenBridgeL2: token not configured");
|
||||
require(!paused[mirroredToken], "CWMultiTokenBridgeL2: token paused");
|
||||
|
||||
DestinationConfig memory peer = destinations[message.sourceChainSelector];
|
||||
require(peer.enabled, "CWMultiTokenBridgeL2: peer disabled");
|
||||
require(_decodeSender(message.sender) == peer.receiverBridge, "CWMultiTokenBridgeL2: peer mismatch");
|
||||
|
||||
processed[message.messageId] = true;
|
||||
ICWMintBurnToken(mirroredToken).mint(recipient, amount);
|
||||
mintedTotal[mirroredToken] += amount;
|
||||
emit Minted(canonicalToken, mirroredToken, recipient, amount);
|
||||
emit SupplyAccountingUpdated(
|
||||
canonicalToken,
|
||||
mirroredToken,
|
||||
mintedTotal[mirroredToken],
|
||||
burnedTotal[mirroredToken],
|
||||
IERC20(mirroredToken).totalSupply()
|
||||
);
|
||||
}
|
||||
|
||||
function burnAndSend(
|
||||
address mirroredToken,
|
||||
uint64 destinationChainSelector,
|
||||
address recipient,
|
||||
uint256 amount
|
||||
) external payable returns (bytes32 messageId) {
|
||||
require(recipient != address(0), "CWMultiTokenBridgeL2: zero recipient");
|
||||
require(amount > 0, "CWMultiTokenBridgeL2: zero amount");
|
||||
|
||||
address canonicalToken = mirroredToCanonical[mirroredToken];
|
||||
require(canonicalToken != address(0), "CWMultiTokenBridgeL2: token not configured");
|
||||
require(!paused[mirroredToken], "CWMultiTokenBridgeL2: token paused");
|
||||
|
||||
DestinationConfig memory dest = destinations[destinationChainSelector];
|
||||
require(dest.enabled, "CWMultiTokenBridgeL2: destination disabled");
|
||||
|
||||
ICWMintBurnToken(mirroredToken).burnFrom(msg.sender, amount);
|
||||
burnedTotal[mirroredToken] += amount;
|
||||
emit Burned(canonicalToken, mirroredToken, msg.sender, amount);
|
||||
emit SupplyAccountingUpdated(
|
||||
canonicalToken,
|
||||
mirroredToken,
|
||||
mintedTotal[mirroredToken],
|
||||
burnedTotal[mirroredToken],
|
||||
IERC20(mirroredToken).totalSupply()
|
||||
);
|
||||
|
||||
IRouterClient.EVM2AnyMessage memory message =
|
||||
_buildMessage(dest.receiverBridge, canonicalToken, recipient, amount);
|
||||
uint256 fee = sendRouter.getFee(destinationChainSelector, message);
|
||||
|
||||
_collectAndApproveFee(fee);
|
||||
|
||||
(messageId, ) = feeToken == address(0)
|
||||
? sendRouter.ccipSend{value: fee}(destinationChainSelector, message)
|
||||
: sendRouter.ccipSend(destinationChainSelector, message);
|
||||
|
||||
emit MessageSent(messageId, canonicalToken, mirroredToken, destinationChainSelector, recipient, amount);
|
||||
_refundNativeExcess(fee);
|
||||
return messageId;
|
||||
}
|
||||
|
||||
function withdrawToken(address token, address to, uint256 amount) external onlyAdmin {
|
||||
IERC20(token).safeTransfer(to, amount);
|
||||
}
|
||||
|
||||
function withdrawNative(address payable to, uint256 amount) external onlyAdmin {
|
||||
(bool ok, ) = to.call{value: amount}("");
|
||||
require(ok, "CWMultiTokenBridgeL2: native transfer failed");
|
||||
}
|
||||
|
||||
receive() external payable {}
|
||||
|
||||
function _buildMessage(
|
||||
address receiverBridge,
|
||||
address canonicalToken,
|
||||
address recipient,
|
||||
uint256 amount
|
||||
) internal view returns (IRouterClient.EVM2AnyMessage memory message) {
|
||||
message = IRouterClient.EVM2AnyMessage({
|
||||
receiver: abi.encode(receiverBridge),
|
||||
data: abi.encode(canonicalToken, recipient, amount),
|
||||
tokenAmounts: new IRouterClient.TokenAmount[](0),
|
||||
feeToken: feeToken,
|
||||
extraArgs: ""
|
||||
});
|
||||
}
|
||||
|
||||
function _collectAndApproveFee(uint256 fee) internal {
|
||||
if (fee == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (feeToken == address(0)) {
|
||||
require(msg.value >= fee, "CWMultiTokenBridgeL2: insufficient native fee");
|
||||
return;
|
||||
}
|
||||
|
||||
IERC20 feeErc20 = IERC20(feeToken);
|
||||
feeErc20.safeTransferFrom(msg.sender, address(this), fee);
|
||||
feeErc20.forceApprove(address(sendRouter), fee);
|
||||
}
|
||||
|
||||
function _refundNativeExcess(uint256 feeUsed) internal {
|
||||
if (feeToken != address(0) || msg.value <= feeUsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint256 refund = msg.value - feeUsed;
|
||||
(bool ok, ) = payable(msg.sender).call{value: refund}("");
|
||||
require(ok, "CWMultiTokenBridgeL2: refund failed");
|
||||
}
|
||||
|
||||
function _decodeSender(bytes memory senderData) internal pure returns (address senderAddress) {
|
||||
if (senderData.length == 0) {
|
||||
return address(0);
|
||||
}
|
||||
senderAddress = abi.decode(senderData, (address));
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../vendor/openzeppelin/ReentrancyGuardUpgradeable.sol";
|
||||
import "../registry/UniversalAssetRegistry.sol";
|
||||
import "../ccip/IRouterClient.sol";
|
||||
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import {AtomicTypes} from "./AtomicTypes.sol";
|
||||
import {IAtomicBridgeCoordinator} from "./interfaces/IAtomicBridgeCoordinator.sol";
|
||||
import {IAtomicLiquidityVault} from "./interfaces/IAtomicLiquidityVault.sol";
|
||||
import {IAtomicFulfillerRegistry} from "./interfaces/IAtomicFulfillerRegistry.sol";
|
||||
import {AtomicObligationEscrow} from "./AtomicObligationEscrow.sol";
|
||||
import {AtomicSettlementRouter} from "./AtomicSettlementRouter.sol";
|
||||
import {AtomicFeePolicy} from "./AtomicFeePolicy.sol";
|
||||
import {AtomicSlashingManager} from "./AtomicSlashingManager.sol";
|
||||
|
||||
contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridgeCoordinator {
|
||||
bytes32 public constant CORRIDOR_MANAGER_ROLE = keccak256("CORRIDOR_MANAGER_ROLE");
|
||||
bytes32 public constant SETTLEMENT_MANAGER_ROLE = keccak256("SETTLEMENT_MANAGER_ROLE");
|
||||
|
||||
IAtomicLiquidityVault public immutable liquidityVault;
|
||||
IAtomicFulfillerRegistry public immutable fulfillerRegistry;
|
||||
AtomicObligationEscrow public immutable obligationEscrow;
|
||||
AtomicSettlementRouter public immutable settlementRouter;
|
||||
AtomicFeePolicy public immutable feePolicy;
|
||||
AtomicSlashingManager public immutable slashingManager;
|
||||
address public immutable protocolTreasury;
|
||||
|
||||
struct CreateIntentParams {
|
||||
uint64 sourceChain;
|
||||
uint64 destinationChain;
|
||||
address assetIn;
|
||||
address assetOut;
|
||||
uint256 amountIn;
|
||||
uint256 minAmountOut;
|
||||
address recipient;
|
||||
uint256 deadline;
|
||||
bytes32 routeId;
|
||||
}
|
||||
|
||||
uint256 public intentNonce;
|
||||
mapping(bytes32 => AtomicTypes.CorridorConfig) private _corridors;
|
||||
mapping(bytes32 => AtomicTypes.AtomicIntent) private _intents;
|
||||
mapping(bytes32 => AtomicTypes.AtomicCommitment) private _commitments;
|
||||
mapping(bytes32 => AtomicTypes.AtomicObligation) private _obligations;
|
||||
|
||||
event CorridorConfigured(bytes32 indexed corridorId, address indexed assetIn, address indexed assetOut);
|
||||
event IntentCreated(bytes32 indexed obligationId, bytes32 indexed corridorId, bytes32 indexed intentId, address sender);
|
||||
event CommitmentAccepted(bytes32 indexed obligationId, address indexed fulfiller, uint256 bondAmount);
|
||||
event SettlementInitiated(bytes32 indexed obligationId, bytes32 indexed settlementId, bytes32 indexed settlementMode);
|
||||
event SettlementConfirmed(bytes32 indexed obligationId, uint256 replenishAmount);
|
||||
event IntentRefunded(bytes32 indexed obligationId, uint256 refundedAmount);
|
||||
event CorridorDegraded(bytes32 indexed corridorId, bytes32 indexed obligationId, bytes32 reason);
|
||||
|
||||
error CorridorDisabled();
|
||||
error CorridorDegradedError();
|
||||
error InvalidCorridor();
|
||||
error InvalidDeadline();
|
||||
error MaxNotionalExceeded();
|
||||
error ReservedLiquidityLimitExceeded();
|
||||
error SettlementBacklogExceeded();
|
||||
error InvalidStatus();
|
||||
error DeadlineNotReached();
|
||||
error SettlementTimeoutNotReached();
|
||||
error MinimumReplenishNotMet();
|
||||
|
||||
constructor(
|
||||
address liquidityVault_,
|
||||
address fulfillerRegistry_,
|
||||
address obligationEscrow_,
|
||||
address settlementRouter_,
|
||||
address feePolicy_,
|
||||
address slashingManager_,
|
||||
address protocolTreasury_,
|
||||
address admin
|
||||
) {
|
||||
liquidityVault = IAtomicLiquidityVault(liquidityVault_);
|
||||
fulfillerRegistry = IAtomicFulfillerRegistry(fulfillerRegistry_);
|
||||
obligationEscrow = AtomicObligationEscrow(obligationEscrow_);
|
||||
settlementRouter = AtomicSettlementRouter(settlementRouter_);
|
||||
feePolicy = AtomicFeePolicy(feePolicy_);
|
||||
slashingManager = AtomicSlashingManager(slashingManager_);
|
||||
protocolTreasury = protocolTreasury_;
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(CORRIDOR_MANAGER_ROLE, admin);
|
||||
_grantRole(SETTLEMENT_MANAGER_ROLE, admin);
|
||||
}
|
||||
|
||||
function getCorridorId(
|
||||
uint64 sourceChain,
|
||||
uint64 destinationChain,
|
||||
address assetIn,
|
||||
address assetOut
|
||||
) public pure returns (bytes32) {
|
||||
return keccak256(abi.encode(sourceChain, destinationChain, assetIn, assetOut));
|
||||
}
|
||||
|
||||
function configureCorridor(AtomicTypes.CorridorConfig calldata cfg) external onlyRole(CORRIDOR_MANAGER_ROLE) {
|
||||
bytes32 corridorId = getCorridorId(cfg.sourceChain, cfg.destinationChain, cfg.assetIn, cfg.assetOut);
|
||||
_corridors[corridorId] = cfg;
|
||||
emit CorridorConfigured(corridorId, cfg.assetIn, cfg.assetOut);
|
||||
}
|
||||
|
||||
function setCorridorDegraded(bytes32 corridorId, bool degraded) external onlyRole(CORRIDOR_MANAGER_ROLE) {
|
||||
_corridors[corridorId].degraded = degraded;
|
||||
}
|
||||
|
||||
function createIntent(CreateIntentParams calldata p) external nonReentrant returns (bytes32 obligationId) {
|
||||
bytes32 corridorId = getCorridorId(p.sourceChain, p.destinationChain, p.assetIn, p.assetOut);
|
||||
AtomicTypes.CorridorConfig memory cfg = _corridors[corridorId];
|
||||
if (!cfg.enabled) revert CorridorDisabled();
|
||||
if (cfg.degraded) revert CorridorDegradedError();
|
||||
if (cfg.assetIn != p.assetIn || cfg.assetOut != p.assetOut) revert InvalidCorridor();
|
||||
if (p.deadline <= block.timestamp || p.deadline > block.timestamp + cfg.fulfilmentTimeout) revert InvalidDeadline();
|
||||
if (p.amountIn > cfg.maxNotional) revert MaxNotionalExceeded();
|
||||
|
||||
AtomicTypes.CorridorLiquidityState memory state = liquidityVault.getCorridorLiquidityState(corridorId, p.assetOut);
|
||||
if (p.minAmountOut > state.freeLiquidity) revert ReservedLiquidityLimitExceeded();
|
||||
if (state.totalLiquidity > 0) {
|
||||
uint256 nextReserved = state.reservedLiquidity + p.minAmountOut;
|
||||
if ((nextReserved * 10_000) / state.totalLiquidity > cfg.maxReservedBps) {
|
||||
revert ReservedLiquidityLimitExceeded();
|
||||
}
|
||||
}
|
||||
if (state.settlementBacklog > cfg.maxSettlementBacklog) revert SettlementBacklogExceeded();
|
||||
|
||||
bytes32 intentId = keccak256(
|
||||
abi.encode(
|
||||
block.chainid,
|
||||
msg.sender,
|
||||
++intentNonce,
|
||||
corridorId,
|
||||
p.amountIn,
|
||||
p.minAmountOut,
|
||||
p.deadline,
|
||||
p.routeId
|
||||
)
|
||||
);
|
||||
obligationId = keccak256(abi.encode(intentId, p.recipient));
|
||||
|
||||
_intents[obligationId] = AtomicTypes.AtomicIntent({
|
||||
sourceChain: p.sourceChain,
|
||||
destinationChain: p.destinationChain,
|
||||
assetIn: p.assetIn,
|
||||
assetOut: p.assetOut,
|
||||
amountIn: p.amountIn,
|
||||
minAmountOut: p.minAmountOut,
|
||||
recipient: p.recipient,
|
||||
deadline: p.deadline,
|
||||
routeId: p.routeId,
|
||||
intentId: intentId
|
||||
});
|
||||
_obligations[obligationId] = AtomicTypes.AtomicObligation({
|
||||
obligationId: obligationId,
|
||||
intentId: intentId,
|
||||
sourceEscrow: p.amountIn,
|
||||
destinationReserve: p.minAmountOut,
|
||||
fulfiller: address(0),
|
||||
status: AtomicTypes.ObligationStatus.IntentCreated,
|
||||
settlementInitiatedAt: 0
|
||||
});
|
||||
|
||||
obligationEscrow.escrowFunds(obligationId, p.assetIn, msg.sender, p.amountIn);
|
||||
liquidityVault.reserveLiquidity(corridorId, p.assetOut, obligationId, p.minAmountOut);
|
||||
|
||||
emit IntentCreated(obligationId, corridorId, intentId, msg.sender);
|
||||
}
|
||||
|
||||
function submitCommitment(bytes32 obligationId, bytes32 settlementMode) external nonReentrant {
|
||||
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
||||
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
||||
if (obligation.status != AtomicTypes.ObligationStatus.IntentCreated) revert InvalidStatus();
|
||||
if (block.timestamp > intent.deadline) revert DeadlineNotReached();
|
||||
|
||||
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
||||
bytes32 mode = settlementMode == bytes32(0) ? _corridors[corridorId].defaultSettlementMode : settlementMode;
|
||||
uint256 requiredBond = feePolicy.requiredBond(corridorId, obligation.destinationReserve);
|
||||
fulfillerRegistry.lockBond(obligationId, msg.sender, corridorId, requiredBond);
|
||||
liquidityVault.fulfillReservedLiquidity(obligationId, intent.recipient);
|
||||
|
||||
_commitments[obligationId] = AtomicTypes.AtomicCommitment({
|
||||
intentId: intent.intentId,
|
||||
fulfiller: msg.sender,
|
||||
reservedLiquidity: obligation.destinationReserve,
|
||||
bondAmount: requiredBond,
|
||||
expiry: block.timestamp + _corridors[corridorId].settlementTimeout,
|
||||
settlementMode: mode
|
||||
});
|
||||
obligation.fulfiller = msg.sender;
|
||||
obligation.status = AtomicTypes.ObligationStatus.Fulfilled;
|
||||
|
||||
emit CommitmentAccepted(obligationId, msg.sender, requiredBond);
|
||||
}
|
||||
|
||||
function initiateSettlement(bytes32 obligationId, bytes calldata settlementData)
|
||||
external
|
||||
payable
|
||||
nonReentrant
|
||||
onlyRole(SETTLEMENT_MANAGER_ROLE)
|
||||
returns (bytes32 settlementId)
|
||||
{
|
||||
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
||||
AtomicTypes.AtomicCommitment memory commitment = _commitments[obligationId];
|
||||
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
||||
if (obligation.status != AtomicTypes.ObligationStatus.Fulfilled) revert InvalidStatus();
|
||||
|
||||
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
||||
uint256 settlementAmount = _releaseEscrowForSettlement(
|
||||
obligationId,
|
||||
corridorId,
|
||||
intent.amountIn,
|
||||
commitment.fulfiller
|
||||
);
|
||||
settlementId = _executeSettlement(
|
||||
obligationId,
|
||||
commitment.settlementMode,
|
||||
intent.assetIn,
|
||||
settlementAmount,
|
||||
intent.recipient,
|
||||
settlementData
|
||||
);
|
||||
|
||||
obligation.status = AtomicTypes.ObligationStatus.SettlementPending;
|
||||
obligation.settlementInitiatedAt = block.timestamp;
|
||||
emit SettlementInitiated(obligationId, settlementId, commitment.settlementMode);
|
||||
}
|
||||
|
||||
function _releaseEscrowForSettlement(
|
||||
bytes32 obligationId,
|
||||
bytes32 corridorId,
|
||||
uint256 amountIn,
|
||||
address fulfiller
|
||||
) internal returns (uint256 settlementAmount) {
|
||||
(uint256 fulfillerFee, uint256 protocolFee) = feePolicy.quoteFees(corridorId, amountIn);
|
||||
settlementAmount = amountIn - fulfillerFee - protocolFee;
|
||||
|
||||
if (protocolFee > 0) {
|
||||
obligationEscrow.release(obligationId, protocolTreasury, protocolFee);
|
||||
}
|
||||
if (fulfillerFee > 0) {
|
||||
obligationEscrow.release(obligationId, fulfiller, fulfillerFee);
|
||||
}
|
||||
obligationEscrow.release(obligationId, address(settlementRouter), settlementAmount);
|
||||
}
|
||||
|
||||
function _executeSettlement(
|
||||
bytes32 obligationId,
|
||||
bytes32 settlementMode,
|
||||
address assetIn,
|
||||
uint256 settlementAmount,
|
||||
address recipient,
|
||||
bytes calldata settlementData
|
||||
) internal returns (bytes32 settlementId) {
|
||||
settlementId = settlementRouter.executeSettlement{value: msg.value}(
|
||||
obligationId,
|
||||
settlementMode,
|
||||
assetIn,
|
||||
settlementAmount,
|
||||
recipient,
|
||||
settlementData
|
||||
);
|
||||
}
|
||||
|
||||
function confirmSettlement(bytes32 obligationId, uint256 replenishAmount)
|
||||
external
|
||||
nonReentrant
|
||||
onlyRole(SETTLEMENT_MANAGER_ROLE)
|
||||
{
|
||||
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
||||
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
||||
if (obligation.status != AtomicTypes.ObligationStatus.SettlementPending) revert InvalidStatus();
|
||||
if (replenishAmount < obligation.destinationReserve) revert MinimumReplenishNotMet();
|
||||
|
||||
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
||||
liquidityVault.reconcileSettlement(corridorId, intent.assetOut, replenishAmount, msg.sender);
|
||||
fulfillerRegistry.releaseBond(obligationId);
|
||||
obligation.status = AtomicTypes.ObligationStatus.Settled;
|
||||
emit SettlementConfirmed(obligationId, replenishAmount);
|
||||
}
|
||||
|
||||
function refundExpiredIntent(bytes32 obligationId) external nonReentrant {
|
||||
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
||||
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
||||
if (obligation.status != AtomicTypes.ObligationStatus.IntentCreated) revert InvalidStatus();
|
||||
if (block.timestamp <= intent.deadline) revert DeadlineNotReached();
|
||||
|
||||
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
||||
(, address payer,,,) = obligationEscrow.escrows(obligationId);
|
||||
liquidityVault.releaseReservation(obligationId);
|
||||
uint256 refunded = obligationEscrow.refundRemaining(obligationId, payer);
|
||||
_corridors[corridorId].degraded = true;
|
||||
obligation.status = AtomicTypes.ObligationStatus.Refunded;
|
||||
emit IntentRefunded(obligationId, refunded);
|
||||
emit CorridorDegraded(corridorId, obligationId, keccak256("FULFILMENT_TIMEOUT"));
|
||||
}
|
||||
|
||||
function handleSettlementTimeout(bytes32 obligationId) external nonReentrant {
|
||||
AtomicTypes.AtomicIntent memory intent = _intents[obligationId];
|
||||
AtomicTypes.AtomicCommitment memory commitment = _commitments[obligationId];
|
||||
AtomicTypes.AtomicObligation storage obligation = _obligations[obligationId];
|
||||
if (obligation.status != AtomicTypes.ObligationStatus.SettlementPending) revert InvalidStatus();
|
||||
if (block.timestamp <= commitment.expiry) revert SettlementTimeoutNotReached();
|
||||
|
||||
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
|
||||
_corridors[corridorId].degraded = true;
|
||||
slashingManager.slash(obligationId, protocolTreasury, keccak256("SETTLEMENT_TIMEOUT"));
|
||||
obligation.status = AtomicTypes.ObligationStatus.Slashed;
|
||||
emit CorridorDegraded(corridorId, obligationId, keccak256("SETTLEMENT_TIMEOUT"));
|
||||
}
|
||||
|
||||
function getCorridorConfig(bytes32 corridorId) external view returns (AtomicTypes.CorridorConfig memory) {
|
||||
return _corridors[corridorId];
|
||||
}
|
||||
|
||||
function getIntent(bytes32 obligationId) external view returns (AtomicTypes.AtomicIntent memory) {
|
||||
return _intents[obligationId];
|
||||
}
|
||||
|
||||
function getCommitment(bytes32 obligationId) external view returns (AtomicTypes.AtomicCommitment memory) {
|
||||
return _commitments[obligationId];
|
||||
}
|
||||
|
||||
function getObligation(bytes32 obligationId) external view returns (AtomicTypes.AtomicObligation memory) {
|
||||
return _obligations[obligationId];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
|
||||
contract AtomicFeePolicy is AccessControl {
|
||||
bytes32 public constant POLICY_MANAGER_ROLE = keccak256("POLICY_MANAGER_ROLE");
|
||||
|
||||
struct CorridorPolicy {
|
||||
uint16 fulfillerFeeBps;
|
||||
uint16 protocolFeeBps;
|
||||
uint16 bondBps;
|
||||
uint16 slashPenaltyBps;
|
||||
uint256 maxFulfilmentDelay;
|
||||
uint256 maxSettlementDelay;
|
||||
}
|
||||
|
||||
mapping(bytes32 => CorridorPolicy) public corridorPolicies;
|
||||
|
||||
event CorridorPolicySet(
|
||||
bytes32 indexed corridorId,
|
||||
uint16 fulfillerFeeBps,
|
||||
uint16 protocolFeeBps,
|
||||
uint16 bondBps,
|
||||
uint16 slashPenaltyBps,
|
||||
uint256 maxFulfilmentDelay,
|
||||
uint256 maxSettlementDelay
|
||||
);
|
||||
|
||||
constructor(address admin) {
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(POLICY_MANAGER_ROLE, admin);
|
||||
}
|
||||
|
||||
function setCorridorPolicy(
|
||||
bytes32 corridorId,
|
||||
uint16 fulfillerFeeBps,
|
||||
uint16 protocolFeeBps,
|
||||
uint16 bondBps,
|
||||
uint16 slashPenaltyBps,
|
||||
uint256 maxFulfilmentDelay,
|
||||
uint256 maxSettlementDelay
|
||||
) external onlyRole(POLICY_MANAGER_ROLE) {
|
||||
corridorPolicies[corridorId] = CorridorPolicy({
|
||||
fulfillerFeeBps: fulfillerFeeBps,
|
||||
protocolFeeBps: protocolFeeBps,
|
||||
bondBps: bondBps,
|
||||
slashPenaltyBps: slashPenaltyBps,
|
||||
maxFulfilmentDelay: maxFulfilmentDelay,
|
||||
maxSettlementDelay: maxSettlementDelay
|
||||
});
|
||||
emit CorridorPolicySet(
|
||||
corridorId,
|
||||
fulfillerFeeBps,
|
||||
protocolFeeBps,
|
||||
bondBps,
|
||||
slashPenaltyBps,
|
||||
maxFulfilmentDelay,
|
||||
maxSettlementDelay
|
||||
);
|
||||
}
|
||||
|
||||
function quoteFees(bytes32 corridorId, uint256 amountIn)
|
||||
external
|
||||
view
|
||||
returns (uint256 fulfillerFee, uint256 protocolFee)
|
||||
{
|
||||
CorridorPolicy memory policy = corridorPolicies[corridorId];
|
||||
fulfillerFee = (amountIn * policy.fulfillerFeeBps) / 10_000;
|
||||
protocolFee = (amountIn * policy.protocolFeeBps) / 10_000;
|
||||
}
|
||||
|
||||
function requiredBond(bytes32 corridorId, uint256 reservedLiquidity) external view returns (uint256) {
|
||||
CorridorPolicy memory policy = corridorPolicies[corridorId];
|
||||
uint256 effectiveBps = policy.bondBps;
|
||||
uint256 slashFloor = 10_000 + policy.slashPenaltyBps;
|
||||
if (effectiveBps < slashFloor) {
|
||||
effectiveBps = slashFloor;
|
||||
}
|
||||
return (reservedLiquidity * effectiveBps) / 10_000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// 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 {IAtomicFulfillerRegistry} from "./interfaces/IAtomicFulfillerRegistry.sol";
|
||||
|
||||
contract AtomicFulfillerRegistry is AccessControl, IAtomicFulfillerRegistry {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE");
|
||||
bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE");
|
||||
bytes32 public constant AUTH_MANAGER_ROLE = keccak256("AUTH_MANAGER_ROLE");
|
||||
|
||||
struct LockedBond {
|
||||
address fulfiller;
|
||||
bytes32 corridorId;
|
||||
uint256 amount;
|
||||
bool active;
|
||||
}
|
||||
|
||||
IERC20 public immutable bondToken;
|
||||
|
||||
mapping(address => uint256) public totalBond;
|
||||
mapping(address => uint256) public lockedBond;
|
||||
mapping(address => bool) public fulfillerActive;
|
||||
mapping(address => mapping(bytes32 => bool)) public corridorAuthorization;
|
||||
mapping(bytes32 => LockedBond) public lockedBonds;
|
||||
|
||||
event BondDeposited(address indexed fulfiller, uint256 amount);
|
||||
event BondWithdrawn(address indexed fulfiller, address indexed recipient, uint256 amount);
|
||||
event FulfillerActivationSet(address indexed fulfiller, bool active);
|
||||
event CorridorAuthorizationSet(address indexed fulfiller, bytes32 indexed corridorId, bool authorized);
|
||||
event BondLocked(bytes32 indexed obligationId, address indexed fulfiller, bytes32 indexed corridorId, uint256 amount);
|
||||
event BondReleased(bytes32 indexed obligationId, address indexed fulfiller, uint256 amount);
|
||||
event BondSlashed(bytes32 indexed obligationId, address indexed fulfiller, address indexed recipient, uint256 amount);
|
||||
|
||||
error InsufficientAvailableBond();
|
||||
error UnauthorizedFulfiller();
|
||||
error BondAlreadyLocked();
|
||||
error BondMissing();
|
||||
|
||||
constructor(address bondToken_, address admin) {
|
||||
bondToken = IERC20(bondToken_);
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(AUTH_MANAGER_ROLE, admin);
|
||||
}
|
||||
|
||||
function depositBond(uint256 amount) external {
|
||||
bondToken.safeTransferFrom(msg.sender, address(this), amount);
|
||||
totalBond[msg.sender] += amount;
|
||||
emit BondDeposited(msg.sender, amount);
|
||||
}
|
||||
|
||||
function withdrawBond(uint256 amount, address recipient) external {
|
||||
if (availableBond(msg.sender) < amount) revert InsufficientAvailableBond();
|
||||
totalBond[msg.sender] -= amount;
|
||||
bondToken.safeTransfer(recipient, amount);
|
||||
emit BondWithdrawn(msg.sender, recipient, amount);
|
||||
}
|
||||
|
||||
function setFulfillerActive(address fulfiller, bool active) external onlyRole(AUTH_MANAGER_ROLE) {
|
||||
fulfillerActive[fulfiller] = active;
|
||||
emit FulfillerActivationSet(fulfiller, active);
|
||||
}
|
||||
|
||||
function setCorridorAuthorization(address fulfiller, bytes32 corridorId, bool authorized)
|
||||
external
|
||||
onlyRole(AUTH_MANAGER_ROLE)
|
||||
{
|
||||
corridorAuthorization[fulfiller][corridorId] = authorized;
|
||||
emit CorridorAuthorizationSet(fulfiller, corridorId, authorized);
|
||||
}
|
||||
|
||||
function lockBond(bytes32 obligationId, address fulfiller, bytes32 corridorId, uint256 amount)
|
||||
external
|
||||
onlyRole(COORDINATOR_ROLE)
|
||||
{
|
||||
if (lockedBonds[obligationId].active) revert BondAlreadyLocked();
|
||||
if (!isFulfillerAuthorized(fulfiller, corridorId)) revert UnauthorizedFulfiller();
|
||||
if (availableBond(fulfiller) < amount) revert InsufficientAvailableBond();
|
||||
|
||||
lockedBond[fulfiller] += amount;
|
||||
lockedBonds[obligationId] = LockedBond({
|
||||
fulfiller: fulfiller,
|
||||
corridorId: corridorId,
|
||||
amount: amount,
|
||||
active: true
|
||||
});
|
||||
emit BondLocked(obligationId, fulfiller, corridorId, amount);
|
||||
}
|
||||
|
||||
function releaseBond(bytes32 obligationId)
|
||||
external
|
||||
onlyRole(COORDINATOR_ROLE)
|
||||
returns (uint256 amount)
|
||||
{
|
||||
LockedBond memory entry = lockedBonds[obligationId];
|
||||
if (!entry.active) revert BondMissing();
|
||||
lockedBond[entry.fulfiller] -= entry.amount;
|
||||
delete lockedBonds[obligationId];
|
||||
emit BondReleased(obligationId, entry.fulfiller, entry.amount);
|
||||
return entry.amount;
|
||||
}
|
||||
|
||||
function slashBond(bytes32 obligationId, address recipient)
|
||||
external
|
||||
onlyRole(SLASHER_ROLE)
|
||||
returns (uint256 amount)
|
||||
{
|
||||
LockedBond memory entry = lockedBonds[obligationId];
|
||||
if (!entry.active) revert BondMissing();
|
||||
lockedBond[entry.fulfiller] -= entry.amount;
|
||||
totalBond[entry.fulfiller] -= entry.amount;
|
||||
delete lockedBonds[obligationId];
|
||||
bondToken.safeTransfer(recipient, entry.amount);
|
||||
emit BondSlashed(obligationId, entry.fulfiller, recipient, entry.amount);
|
||||
return entry.amount;
|
||||
}
|
||||
|
||||
function availableBond(address fulfiller) public view returns (uint256) {
|
||||
return totalBond[fulfiller] - lockedBond[fulfiller];
|
||||
}
|
||||
|
||||
function isFulfillerAuthorized(address fulfiller, bytes32 corridorId) public view returns (bool) {
|
||||
return fulfillerActive[fulfiller] && corridorAuthorization[fulfiller][corridorId];
|
||||
}
|
||||
|
||||
function canCover(address fulfiller, uint256 amount) external view returns (bool) {
|
||||
return availableBond(fulfiller) >= amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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 {IAtomicLiquidityVault} from "./interfaces/IAtomicLiquidityVault.sol";
|
||||
import {AtomicTypes} from "./AtomicTypes.sol";
|
||||
|
||||
contract AtomicLiquidityVault is AccessControl, IAtomicLiquidityVault {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE");
|
||||
bytes32 public constant RECONCILER_ROLE = keccak256("RECONCILER_ROLE");
|
||||
bytes32 public constant BUFFER_MANAGER_ROLE = keccak256("BUFFER_MANAGER_ROLE");
|
||||
|
||||
struct StoredCorridorState {
|
||||
uint256 totalLiquidity;
|
||||
uint256 reservedLiquidity;
|
||||
uint256 targetBuffer;
|
||||
uint256 settlementBacklog;
|
||||
}
|
||||
|
||||
struct Reservation {
|
||||
bytes32 corridorId;
|
||||
address token;
|
||||
uint256 amount;
|
||||
bool exists;
|
||||
bool fulfilled;
|
||||
}
|
||||
|
||||
mapping(bytes32 => mapping(address => StoredCorridorState)) private _corridorState;
|
||||
mapping(bytes32 => Reservation) public reservations;
|
||||
|
||||
event CorridorFunded(bytes32 indexed corridorId, address indexed token, address indexed funder, uint256 amount);
|
||||
event LiquidityReserved(bytes32 indexed obligationId, bytes32 indexed corridorId, address indexed token, uint256 amount);
|
||||
event ReservedLiquidityFulfilled(bytes32 indexed obligationId, address indexed recipient, uint256 amount);
|
||||
event ReservationReleased(bytes32 indexed obligationId, uint256 amount);
|
||||
event SettlementReconciled(bytes32 indexed corridorId, address indexed token, uint256 amount);
|
||||
event TargetBufferSet(bytes32 indexed corridorId, address indexed token, uint256 targetBuffer);
|
||||
|
||||
error ReservationExists();
|
||||
error ReservationMissing();
|
||||
error ReservationAlreadyFulfilled();
|
||||
error InsufficientFreeLiquidity();
|
||||
|
||||
constructor(address admin) {
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(RECONCILER_ROLE, admin);
|
||||
_grantRole(BUFFER_MANAGER_ROLE, admin);
|
||||
}
|
||||
|
||||
function fundCorridor(bytes32 corridorId, address token, uint256 amount) external {
|
||||
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
|
||||
_corridorState[corridorId][token].totalLiquidity += amount;
|
||||
emit CorridorFunded(corridorId, token, msg.sender, amount);
|
||||
}
|
||||
|
||||
function setTargetBuffer(bytes32 corridorId, address token, uint256 targetBuffer)
|
||||
external
|
||||
onlyRole(BUFFER_MANAGER_ROLE)
|
||||
{
|
||||
_corridorState[corridorId][token].targetBuffer = targetBuffer;
|
||||
emit TargetBufferSet(corridorId, token, targetBuffer);
|
||||
}
|
||||
|
||||
function reserveLiquidity(bytes32 corridorId, address token, bytes32 obligationId, uint256 amount)
|
||||
external
|
||||
onlyRole(COORDINATOR_ROLE)
|
||||
{
|
||||
if (reservations[obligationId].exists) revert ReservationExists();
|
||||
if (freeLiquidity(corridorId, token) < amount) revert InsufficientFreeLiquidity();
|
||||
|
||||
reservations[obligationId] = Reservation({
|
||||
corridorId: corridorId,
|
||||
token: token,
|
||||
amount: amount,
|
||||
exists: true,
|
||||
fulfilled: false
|
||||
});
|
||||
_corridorState[corridorId][token].reservedLiquidity += amount;
|
||||
emit LiquidityReserved(obligationId, corridorId, token, amount);
|
||||
}
|
||||
|
||||
function fulfillReservedLiquidity(bytes32 obligationId, address recipient)
|
||||
external
|
||||
onlyRole(COORDINATOR_ROLE)
|
||||
returns (uint256 amount)
|
||||
{
|
||||
Reservation storage reservation = reservations[obligationId];
|
||||
if (!reservation.exists) revert ReservationMissing();
|
||||
if (reservation.fulfilled) revert ReservationAlreadyFulfilled();
|
||||
|
||||
reservation.fulfilled = true;
|
||||
StoredCorridorState storage state = _corridorState[reservation.corridorId][reservation.token];
|
||||
state.reservedLiquidity -= reservation.amount;
|
||||
state.totalLiquidity -= reservation.amount;
|
||||
state.settlementBacklog += reservation.amount;
|
||||
|
||||
IERC20(reservation.token).safeTransfer(recipient, reservation.amount);
|
||||
emit ReservedLiquidityFulfilled(obligationId, recipient, reservation.amount);
|
||||
return reservation.amount;
|
||||
}
|
||||
|
||||
function releaseReservation(bytes32 obligationId)
|
||||
external
|
||||
onlyRole(COORDINATOR_ROLE)
|
||||
returns (uint256 amount)
|
||||
{
|
||||
Reservation storage reservation = reservations[obligationId];
|
||||
if (!reservation.exists) revert ReservationMissing();
|
||||
if (reservation.fulfilled) revert ReservationAlreadyFulfilled();
|
||||
|
||||
amount = reservation.amount;
|
||||
_corridorState[reservation.corridorId][reservation.token].reservedLiquidity -= amount;
|
||||
delete reservations[obligationId];
|
||||
emit ReservationReleased(obligationId, amount);
|
||||
}
|
||||
|
||||
function reconcileSettlement(bytes32 corridorId, address token, uint256 amount, address from)
|
||||
external
|
||||
onlyRole(RECONCILER_ROLE)
|
||||
{
|
||||
IERC20(token).safeTransferFrom(from, address(this), amount);
|
||||
StoredCorridorState storage state = _corridorState[corridorId][token];
|
||||
state.totalLiquidity += amount;
|
||||
uint256 backlog = state.settlementBacklog;
|
||||
state.settlementBacklog = amount >= backlog ? 0 : backlog - amount;
|
||||
emit SettlementReconciled(corridorId, token, amount);
|
||||
}
|
||||
|
||||
function getCorridorLiquidityState(bytes32 corridorId, address token)
|
||||
external
|
||||
view
|
||||
returns (AtomicTypes.CorridorLiquidityState memory state)
|
||||
{
|
||||
StoredCorridorState memory stored = _corridorState[corridorId][token];
|
||||
state = AtomicTypes.CorridorLiquidityState({
|
||||
totalLiquidity: stored.totalLiquidity,
|
||||
reservedLiquidity: stored.reservedLiquidity,
|
||||
freeLiquidity: stored.totalLiquidity - stored.reservedLiquidity,
|
||||
targetBuffer: stored.targetBuffer,
|
||||
settlementBacklog: stored.settlementBacklog
|
||||
});
|
||||
}
|
||||
|
||||
function freeLiquidity(bytes32 corridorId, address token) public view returns (uint256) {
|
||||
StoredCorridorState memory state = _corridorState[corridorId][token];
|
||||
return state.totalLiquidity - state.reservedLiquidity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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";
|
||||
|
||||
contract AtomicObligationEscrow is AccessControl {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE");
|
||||
|
||||
struct EscrowRecord {
|
||||
address token;
|
||||
address payer;
|
||||
uint256 totalAmount;
|
||||
uint256 releasedAmount;
|
||||
bool exists;
|
||||
}
|
||||
|
||||
mapping(bytes32 => EscrowRecord) public escrows;
|
||||
|
||||
event EscrowFunded(bytes32 indexed obligationId, address indexed token, address indexed payer, uint256 amount);
|
||||
event EscrowReleased(bytes32 indexed obligationId, address indexed to, uint256 amount);
|
||||
event EscrowRefunded(bytes32 indexed obligationId, address indexed to, uint256 amount);
|
||||
|
||||
error EscrowExists();
|
||||
error EscrowMissing();
|
||||
error InsufficientEscrow();
|
||||
|
||||
constructor(address admin) {
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
}
|
||||
|
||||
function escrowFunds(bytes32 obligationId, address token, address from, uint256 amount)
|
||||
external
|
||||
onlyRole(COORDINATOR_ROLE)
|
||||
{
|
||||
if (escrows[obligationId].exists) revert EscrowExists();
|
||||
escrows[obligationId] = EscrowRecord({
|
||||
token: token,
|
||||
payer: from,
|
||||
totalAmount: amount,
|
||||
releasedAmount: 0,
|
||||
exists: true
|
||||
});
|
||||
IERC20(token).safeTransferFrom(from, address(this), amount);
|
||||
emit EscrowFunded(obligationId, token, from, amount);
|
||||
}
|
||||
|
||||
function release(bytes32 obligationId, address to, uint256 amount) external onlyRole(COORDINATOR_ROLE) {
|
||||
EscrowRecord storage record = escrows[obligationId];
|
||||
if (!record.exists) revert EscrowMissing();
|
||||
uint256 availableAmount = record.totalAmount - record.releasedAmount;
|
||||
if (availableAmount < amount) revert InsufficientEscrow();
|
||||
record.releasedAmount += amount;
|
||||
IERC20(record.token).safeTransfer(to, amount);
|
||||
emit EscrowReleased(obligationId, to, amount);
|
||||
}
|
||||
|
||||
function refundRemaining(bytes32 obligationId, address to)
|
||||
external
|
||||
onlyRole(COORDINATOR_ROLE)
|
||||
returns (uint256 refunded)
|
||||
{
|
||||
EscrowRecord storage record = escrows[obligationId];
|
||||
if (!record.exists) revert EscrowMissing();
|
||||
refunded = record.totalAmount - record.releasedAmount;
|
||||
record.releasedAmount = record.totalAmount;
|
||||
IERC20(record.token).safeTransfer(to, refunded);
|
||||
emit EscrowRefunded(obligationId, to, refunded);
|
||||
}
|
||||
|
||||
function remaining(bytes32 obligationId) external view returns (uint256) {
|
||||
EscrowRecord memory record = escrows[obligationId];
|
||||
if (!record.exists) revert EscrowMissing();
|
||||
return record.totalAmount - record.releasedAmount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IAtomicQuoteEngine} from "./interfaces/IAtomicQuoteEngine.sol";
|
||||
import {IAtomicBridgeCoordinator} from "./interfaces/IAtomicBridgeCoordinator.sol";
|
||||
import {IAtomicLiquidityVault} from "./interfaces/IAtomicLiquidityVault.sol";
|
||||
import {IAtomicFulfillerRegistry} from "./interfaces/IAtomicFulfillerRegistry.sol";
|
||||
import {AtomicFeePolicy} from "./AtomicFeePolicy.sol";
|
||||
import {AtomicTypes} from "./AtomicTypes.sol";
|
||||
|
||||
contract AtomicQuoteEngine is IAtomicQuoteEngine {
|
||||
IAtomicBridgeCoordinator public immutable coordinator;
|
||||
IAtomicLiquidityVault public immutable vault;
|
||||
IAtomicFulfillerRegistry public immutable fulfillerRegistry;
|
||||
AtomicFeePolicy public immutable feePolicy;
|
||||
|
||||
constructor(
|
||||
address coordinator_,
|
||||
address vault_,
|
||||
address fulfillerRegistry_,
|
||||
address feePolicy_
|
||||
) {
|
||||
coordinator = IAtomicBridgeCoordinator(coordinator_);
|
||||
vault = IAtomicLiquidityVault(vault_);
|
||||
fulfillerRegistry = IAtomicFulfillerRegistry(fulfillerRegistry_);
|
||||
feePolicy = AtomicFeePolicy(feePolicy_);
|
||||
}
|
||||
|
||||
function quote(
|
||||
bytes32 corridorId,
|
||||
uint256 amountIn,
|
||||
uint256 minAmountOut,
|
||||
address fulfiller
|
||||
) external view returns (AtomicTypes.AtomicQuote memory q) {
|
||||
AtomicTypes.CorridorConfig memory cfg = coordinator.getCorridorConfig(corridorId);
|
||||
AtomicTypes.CorridorLiquidityState memory state = vault.getCorridorLiquidityState(corridorId, cfg.assetOut);
|
||||
(uint256 fulfillerFee, uint256 protocolFee) = feePolicy.quoteFees(corridorId, amountIn);
|
||||
uint256 requiredBond = feePolicy.requiredBond(corridorId, minAmountOut);
|
||||
bool authorized = fulfillerRegistry.isFulfillerAuthorized(fulfiller, corridorId);
|
||||
bool bondSufficient = fulfillerRegistry.canCover(fulfiller, requiredBond);
|
||||
|
||||
q = AtomicTypes.AtomicQuote({
|
||||
routeClass: _routeClass(cfg, state, amountIn, minAmountOut, authorized, bondSufficient),
|
||||
availableLiquidity: state.freeLiquidity > state.targetBuffer ? state.freeLiquidity - state.targetBuffer : 0,
|
||||
freeLiquidity: state.freeLiquidity,
|
||||
fulfillerFee: fulfillerFee,
|
||||
protocolFee: protocolFee,
|
||||
requiredBond: requiredBond,
|
||||
settlementBacklog: state.settlementBacklog,
|
||||
deadlineWindow: cfg.fulfilmentTimeout,
|
||||
corridorEnabled: cfg.enabled,
|
||||
corridorDegraded: cfg.degraded,
|
||||
fulfillerAuthorized: authorized,
|
||||
fulfillerBondSufficient: bondSufficient
|
||||
});
|
||||
}
|
||||
|
||||
function _routeClass(
|
||||
AtomicTypes.CorridorConfig memory cfg,
|
||||
AtomicTypes.CorridorLiquidityState memory state,
|
||||
uint256 amountIn,
|
||||
uint256 minAmountOut,
|
||||
bool authorized,
|
||||
bool bondSufficient
|
||||
) internal pure returns (AtomicTypes.RouteClass) {
|
||||
if (!cfg.enabled || cfg.degraded) {
|
||||
return AtomicTypes.RouteClass.Blocked;
|
||||
}
|
||||
if (
|
||||
amountIn > cfg.maxNotional ||
|
||||
minAmountOut > state.freeLiquidity ||
|
||||
state.settlementBacklog > cfg.maxSettlementBacklog
|
||||
) {
|
||||
return AtomicTypes.RouteClass.Blocked;
|
||||
}
|
||||
if (!authorized || !bondSufficient) {
|
||||
return AtomicTypes.RouteClass.Planning;
|
||||
}
|
||||
return AtomicTypes.RouteClass.ExecutionReady;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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 {IAtomicSettlementAdapter} from "./interfaces/IAtomicSettlementAdapter.sol";
|
||||
|
||||
contract AtomicSettlementRouter is AccessControl {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE");
|
||||
bytes32 public constant ADAPTER_MANAGER_ROLE = keccak256("ADAPTER_MANAGER_ROLE");
|
||||
|
||||
mapping(bytes32 => address) public adapters;
|
||||
|
||||
event AdapterSet(bytes32 indexed settlementMode, address indexed adapter);
|
||||
event SettlementExecuted(
|
||||
bytes32 indexed obligationId,
|
||||
bytes32 indexed settlementMode,
|
||||
address indexed token,
|
||||
uint256 amount,
|
||||
bytes32 settlementId
|
||||
);
|
||||
|
||||
error MissingAdapter();
|
||||
|
||||
constructor(address admin) {
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(ADAPTER_MANAGER_ROLE, admin);
|
||||
}
|
||||
|
||||
function setAdapter(bytes32 settlementMode, address adapter) external onlyRole(ADAPTER_MANAGER_ROLE) {
|
||||
adapters[settlementMode] = adapter;
|
||||
emit AdapterSet(settlementMode, adapter);
|
||||
}
|
||||
|
||||
function executeSettlement(
|
||||
bytes32 obligationId,
|
||||
bytes32 settlementMode,
|
||||
address token,
|
||||
uint256 amount,
|
||||
address recipient,
|
||||
bytes calldata data
|
||||
) external payable onlyRole(COORDINATOR_ROLE) returns (bytes32 settlementId) {
|
||||
address adapter = adapters[settlementMode];
|
||||
if (adapter == address(0)) revert MissingAdapter();
|
||||
|
||||
IERC20(token).safeTransfer(adapter, amount);
|
||||
settlementId = IAtomicSettlementAdapter(adapter).executeSettlement{value: msg.value}(
|
||||
obligationId,
|
||||
token,
|
||||
amount,
|
||||
recipient,
|
||||
data
|
||||
);
|
||||
emit SettlementExecuted(obligationId, settlementMode, token, amount, settlementId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import {IAtomicFulfillerRegistry} from "./interfaces/IAtomicFulfillerRegistry.sol";
|
||||
|
||||
contract AtomicSlashingManager is AccessControl {
|
||||
bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE");
|
||||
|
||||
IAtomicFulfillerRegistry public immutable fulfillerRegistry;
|
||||
|
||||
event ObligationSlashed(bytes32 indexed obligationId, address indexed recipient, bytes32 indexed reason, uint256 amount);
|
||||
|
||||
constructor(address fulfillerRegistry_, address admin) {
|
||||
fulfillerRegistry = IAtomicFulfillerRegistry(fulfillerRegistry_);
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
}
|
||||
|
||||
function slash(bytes32 obligationId, address recipient, bytes32 reason)
|
||||
external
|
||||
onlyRole(COORDINATOR_ROLE)
|
||||
returns (uint256 amount)
|
||||
{
|
||||
amount = fulfillerRegistry.slashBond(obligationId, recipient);
|
||||
emit ObligationSlashed(obligationId, recipient, reason, amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
library AtomicTypes {
|
||||
enum ObligationStatus {
|
||||
None,
|
||||
IntentCreated,
|
||||
Fulfilled,
|
||||
SettlementPending,
|
||||
Settled,
|
||||
Refunded,
|
||||
Slashed
|
||||
}
|
||||
|
||||
enum RouteClass {
|
||||
Blocked,
|
||||
Planning,
|
||||
ExecutionReady
|
||||
}
|
||||
|
||||
struct AtomicIntent {
|
||||
uint64 sourceChain;
|
||||
uint64 destinationChain;
|
||||
address assetIn;
|
||||
address assetOut;
|
||||
uint256 amountIn;
|
||||
uint256 minAmountOut;
|
||||
address recipient;
|
||||
uint256 deadline;
|
||||
bytes32 routeId;
|
||||
bytes32 intentId;
|
||||
}
|
||||
|
||||
struct AtomicCommitment {
|
||||
bytes32 intentId;
|
||||
address fulfiller;
|
||||
uint256 reservedLiquidity;
|
||||
uint256 bondAmount;
|
||||
uint256 expiry;
|
||||
bytes32 settlementMode;
|
||||
}
|
||||
|
||||
struct AtomicObligation {
|
||||
bytes32 obligationId;
|
||||
bytes32 intentId;
|
||||
uint256 sourceEscrow;
|
||||
uint256 destinationReserve;
|
||||
address fulfiller;
|
||||
ObligationStatus status;
|
||||
uint256 settlementInitiatedAt;
|
||||
}
|
||||
|
||||
struct CorridorLiquidityState {
|
||||
uint256 totalLiquidity;
|
||||
uint256 reservedLiquidity;
|
||||
uint256 freeLiquidity;
|
||||
uint256 targetBuffer;
|
||||
uint256 settlementBacklog;
|
||||
}
|
||||
|
||||
struct CorridorConfig {
|
||||
bool enabled;
|
||||
bool degraded;
|
||||
uint64 sourceChain;
|
||||
uint64 destinationChain;
|
||||
address assetIn;
|
||||
address assetOut;
|
||||
uint256 maxNotional;
|
||||
uint16 maxReservedBps;
|
||||
uint256 targetBuffer;
|
||||
uint256 maxSettlementBacklog;
|
||||
uint16 maxOracleDriftBps;
|
||||
uint256 fulfilmentTimeout;
|
||||
uint256 settlementTimeout;
|
||||
bytes32 defaultSettlementMode;
|
||||
}
|
||||
|
||||
struct AtomicQuote {
|
||||
RouteClass routeClass;
|
||||
uint256 availableLiquidity;
|
||||
uint256 freeLiquidity;
|
||||
uint256 fulfillerFee;
|
||||
uint256 protocolFee;
|
||||
uint256 requiredBond;
|
||||
uint256 settlementBacklog;
|
||||
uint256 deadlineWindow;
|
||||
bool corridorEnabled;
|
||||
bool corridorDegraded;
|
||||
bool fulfillerAuthorized;
|
||||
bool fulfillerBondSufficient;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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 {IAtomicSettlementAdapter} from "../interfaces/IAtomicSettlementAdapter.sol";
|
||||
import {UniversalCCIPBridge} from "../../UniversalCCIPBridge.sol";
|
||||
|
||||
contract UniversalCcipAtomicSettlementAdapter is AccessControl, IAtomicSettlementAdapter {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 public constant ROUTER_ROLE = keccak256("ROUTER_ROLE");
|
||||
|
||||
UniversalCCIPBridge public immutable universalBridge;
|
||||
|
||||
struct SettlementParams {
|
||||
uint64 destinationChain;
|
||||
address destinationRecipient;
|
||||
bytes32 assetType;
|
||||
bool usePMM;
|
||||
bool useVault;
|
||||
bytes complianceProof;
|
||||
bytes vaultInstructions;
|
||||
}
|
||||
|
||||
event AtomicSettlementBridged(
|
||||
bytes32 indexed obligationId,
|
||||
bytes32 indexed messageId,
|
||||
address indexed token,
|
||||
uint256 amount,
|
||||
uint64 destinationChain,
|
||||
address recipient
|
||||
);
|
||||
|
||||
constructor(address universalBridge_, address admin) {
|
||||
universalBridge = UniversalCCIPBridge(payable(universalBridge_));
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
}
|
||||
|
||||
function executeSettlement(
|
||||
bytes32 obligationId,
|
||||
address token,
|
||||
uint256 amount,
|
||||
address recipient,
|
||||
bytes calldata data
|
||||
) external payable onlyRole(ROUTER_ROLE) returns (bytes32 settlementId) {
|
||||
SettlementParams memory params = abi.decode(data, (SettlementParams));
|
||||
address finalRecipient = params.destinationRecipient == address(0) ? recipient : params.destinationRecipient;
|
||||
IERC20(token).forceApprove(address(universalBridge), amount);
|
||||
settlementId = universalBridge.bridge{value: msg.value}(
|
||||
UniversalCCIPBridge.BridgeOperation({
|
||||
token: token,
|
||||
amount: amount,
|
||||
destinationChain: params.destinationChain,
|
||||
recipient: finalRecipient,
|
||||
assetType: params.assetType,
|
||||
usePMM: params.usePMM,
|
||||
useVault: params.useVault,
|
||||
complianceProof: params.complianceProof,
|
||||
vaultInstructions: params.vaultInstructions
|
||||
})
|
||||
);
|
||||
IERC20(token).forceApprove(address(universalBridge), 0);
|
||||
emit AtomicSettlementBridged(
|
||||
obligationId,
|
||||
settlementId,
|
||||
token,
|
||||
amount,
|
||||
params.destinationChain,
|
||||
finalRecipient
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {AtomicTypes} from "../AtomicTypes.sol";
|
||||
|
||||
interface IAtomicBridgeCoordinator {
|
||||
function getCorridorId(
|
||||
uint64 sourceChain,
|
||||
uint64 destinationChain,
|
||||
address assetIn,
|
||||
address assetOut
|
||||
) external pure returns (bytes32);
|
||||
|
||||
function getCorridorConfig(bytes32 corridorId) external view returns (AtomicTypes.CorridorConfig memory);
|
||||
|
||||
function getIntent(bytes32 obligationId) external view returns (AtomicTypes.AtomicIntent memory);
|
||||
|
||||
function getCommitment(bytes32 obligationId) external view returns (AtomicTypes.AtomicCommitment memory);
|
||||
|
||||
function getObligation(bytes32 obligationId) external view returns (AtomicTypes.AtomicObligation memory);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
interface IAtomicFulfillerRegistry {
|
||||
function depositBond(uint256 amount) external;
|
||||
|
||||
function withdrawBond(uint256 amount, address recipient) external;
|
||||
|
||||
function lockBond(bytes32 obligationId, address fulfiller, bytes32 corridorId, uint256 amount) external;
|
||||
|
||||
function releaseBond(bytes32 obligationId) external returns (uint256 amount);
|
||||
|
||||
function slashBond(bytes32 obligationId, address recipient) external returns (uint256 amount);
|
||||
|
||||
function availableBond(address fulfiller) external view returns (uint256);
|
||||
|
||||
function isFulfillerAuthorized(address fulfiller, bytes32 corridorId) external view returns (bool);
|
||||
|
||||
function canCover(address fulfiller, uint256 amount) external view returns (bool);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {AtomicTypes} from "../AtomicTypes.sol";
|
||||
|
||||
interface IAtomicLiquidityVault {
|
||||
function fundCorridor(bytes32 corridorId, address token, uint256 amount) external;
|
||||
|
||||
function reserveLiquidity(bytes32 corridorId, address token, bytes32 obligationId, uint256 amount) external;
|
||||
|
||||
function fulfillReservedLiquidity(bytes32 obligationId, address recipient) external returns (uint256 amount);
|
||||
|
||||
function releaseReservation(bytes32 obligationId) external returns (uint256 amount);
|
||||
|
||||
function reconcileSettlement(bytes32 corridorId, address token, uint256 amount, address from) external;
|
||||
|
||||
function getCorridorLiquidityState(bytes32 corridorId, address token)
|
||||
external
|
||||
view
|
||||
returns (AtomicTypes.CorridorLiquidityState memory);
|
||||
|
||||
function freeLiquidity(bytes32 corridorId, address token) external view returns (uint256);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {AtomicTypes} from "../AtomicTypes.sol";
|
||||
|
||||
interface IAtomicQuoteEngine {
|
||||
function quote(
|
||||
bytes32 corridorId,
|
||||
uint256 amountIn,
|
||||
uint256 minAmountOut,
|
||||
address fulfiller
|
||||
) external view returns (AtomicTypes.AtomicQuote memory);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
interface IAtomicSettlementAdapter {
|
||||
function executeSettlement(
|
||||
bytes32 obligationId,
|
||||
address token,
|
||||
uint256 amount,
|
||||
address recipient,
|
||||
bytes calldata data
|
||||
) external payable returns (bytes32 settlementId);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
interface IAtomicUnwindAdapter {
|
||||
function executeUnwind(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountIn,
|
||||
uint256 minAmountOut,
|
||||
address recipient,
|
||||
bytes calldata data
|
||||
) external returns (uint256 amountOut);
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "../../reserve/IReserveSystem.sol";
|
||||
import "./ICWReserveVerifier.sol";
|
||||
|
||||
interface ICWMultiTokenBridgeL1AccountingV2 {
|
||||
function supportedCanonicalToken(address token) external view returns (bool);
|
||||
function paused(address token) external view returns (bool);
|
||||
function lockedBalance(address token) external view returns (uint256);
|
||||
function totalOutstanding(address token) external view returns (uint256);
|
||||
function outstandingMinted(address token, uint64 destinationChainSelector) external view returns (uint256);
|
||||
function maxOutstanding(address token, uint64 destinationChainSelector) external view returns (uint256);
|
||||
}
|
||||
|
||||
interface IOwnableLikeV2 {
|
||||
function owner() external view returns (address);
|
||||
}
|
||||
|
||||
interface IBalanceOfAsset {
|
||||
function balanceOf(address account) external view returns (uint256);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title CWAssetReserveVerifier
|
||||
* @notice Generic reserve verifier for canonical c* -> mirrored cW* bridge lanes.
|
||||
* @dev Uses bridge accounting plus optional vault and reserve-system checks so one verifier
|
||||
* can cover stable, monetary-unit, and gas-native families behind a single L1 bridge.
|
||||
*/
|
||||
contract CWAssetReserveVerifier is AccessControl, ICWReserveVerifier {
|
||||
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
|
||||
uint256 public constant HARD_THRESHOLD_BPS = 10_000;
|
||||
|
||||
struct TokenConfig {
|
||||
bool enabled;
|
||||
address reserveAsset;
|
||||
bool requireVaultBacking;
|
||||
bool requireReserveSystemBalance;
|
||||
bool requireTokenOwnerMatchVault;
|
||||
}
|
||||
|
||||
struct VerificationStatus {
|
||||
bool supportedCanonicalToken;
|
||||
bool bridgePaused;
|
||||
bool escrowSufficient;
|
||||
bool chainCapSufficient;
|
||||
bool reserveSystemAdequate;
|
||||
bool vaultAdequate;
|
||||
bool tokenOwnerMatchesVault;
|
||||
bool passes;
|
||||
uint256 lockedBalanceAmount;
|
||||
uint256 totalOutstandingAmount;
|
||||
uint256 chainOutstandingAmount;
|
||||
uint256 maxOutstandingAmount;
|
||||
uint256 canonicalTotalSupply;
|
||||
uint256 reserveSystemBalance;
|
||||
uint256 vaultReserveBalance;
|
||||
uint256 vaultBackingRatio;
|
||||
}
|
||||
|
||||
ICWMultiTokenBridgeL1AccountingV2 public bridge;
|
||||
address public assetVault;
|
||||
IReserveSystem public reserveSystem;
|
||||
|
||||
mapping(address => TokenConfig) public tokenConfigs;
|
||||
|
||||
event BridgeUpdated(address indexed newBridge);
|
||||
event AssetVaultUpdated(address indexed newVault);
|
||||
event ReserveSystemUpdated(address indexed newReserveSystem);
|
||||
event TokenConfigured(
|
||||
address indexed canonicalToken,
|
||||
address indexed reserveAsset,
|
||||
bool requireVaultBacking,
|
||||
bool requireReserveSystemBalance,
|
||||
bool requireTokenOwnerMatchVault
|
||||
);
|
||||
event TokenDisabled(address indexed canonicalToken);
|
||||
|
||||
error ZeroAddress();
|
||||
error TokenNotConfigured();
|
||||
error VaultRequired();
|
||||
error ReserveSystemRequired();
|
||||
error ReserveAssetRequired();
|
||||
error UnsupportedCanonicalToken();
|
||||
error BridgeTokenPaused();
|
||||
error EscrowInvariantViolation();
|
||||
error ChainOutstandingCapExceeded();
|
||||
error TokenOwnerMismatch();
|
||||
error VaultBackingInsufficient();
|
||||
error ReserveSystemBackingInsufficient();
|
||||
|
||||
constructor(
|
||||
address admin,
|
||||
address bridge_,
|
||||
address assetVault_,
|
||||
address reserveSystem_
|
||||
) {
|
||||
if (admin == address(0) || bridge_ == address(0)) revert ZeroAddress();
|
||||
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(OPERATOR_ROLE, admin);
|
||||
|
||||
bridge = ICWMultiTokenBridgeL1AccountingV2(bridge_);
|
||||
assetVault = assetVault_;
|
||||
reserveSystem = IReserveSystem(reserveSystem_);
|
||||
}
|
||||
|
||||
function setBridge(address bridge_) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
if (bridge_ == address(0)) revert ZeroAddress();
|
||||
bridge = ICWMultiTokenBridgeL1AccountingV2(bridge_);
|
||||
emit BridgeUpdated(bridge_);
|
||||
}
|
||||
|
||||
function setAssetVault(address assetVault_) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
assetVault = assetVault_;
|
||||
emit AssetVaultUpdated(assetVault_);
|
||||
}
|
||||
|
||||
function setReserveSystem(address reserveSystem_) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
reserveSystem = IReserveSystem(reserveSystem_);
|
||||
emit ReserveSystemUpdated(reserveSystem_);
|
||||
}
|
||||
|
||||
function configureToken(
|
||||
address canonicalToken,
|
||||
address reserveAsset,
|
||||
bool requireVaultBacking,
|
||||
bool requireReserveSystemBalance,
|
||||
bool requireTokenOwnerMatchVault
|
||||
) external onlyRole(OPERATOR_ROLE) {
|
||||
if (canonicalToken == address(0)) revert ZeroAddress();
|
||||
if ((requireVaultBacking || requireTokenOwnerMatchVault) && assetVault == address(0)) revert VaultRequired();
|
||||
if (requireReserveSystemBalance && address(reserveSystem) == address(0)) revert ReserveSystemRequired();
|
||||
if ((requireVaultBacking || requireReserveSystemBalance) && reserveAsset == address(0)) revert ReserveAssetRequired();
|
||||
|
||||
tokenConfigs[canonicalToken] = TokenConfig({
|
||||
enabled: true,
|
||||
reserveAsset: reserveAsset,
|
||||
requireVaultBacking: requireVaultBacking,
|
||||
requireReserveSystemBalance: requireReserveSystemBalance,
|
||||
requireTokenOwnerMatchVault: requireTokenOwnerMatchVault
|
||||
});
|
||||
|
||||
emit TokenConfigured(
|
||||
canonicalToken,
|
||||
reserveAsset,
|
||||
requireVaultBacking,
|
||||
requireReserveSystemBalance,
|
||||
requireTokenOwnerMatchVault
|
||||
);
|
||||
}
|
||||
|
||||
function disableToken(address canonicalToken) external onlyRole(OPERATOR_ROLE) {
|
||||
delete tokenConfigs[canonicalToken];
|
||||
emit TokenDisabled(canonicalToken);
|
||||
}
|
||||
|
||||
function verifyLock(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector,
|
||||
uint256
|
||||
) external view override returns (bool verified) {
|
||||
TokenConfig memory config = tokenConfigs[canonicalToken];
|
||||
if (!config.enabled) revert TokenNotConfigured();
|
||||
|
||||
VerificationStatus memory status = _buildStatus(canonicalToken, destinationChainSelector, config);
|
||||
|
||||
if (!status.supportedCanonicalToken) revert UnsupportedCanonicalToken();
|
||||
if (status.bridgePaused) revert BridgeTokenPaused();
|
||||
if (!status.escrowSufficient) revert EscrowInvariantViolation();
|
||||
if (!status.chainCapSufficient) revert ChainOutstandingCapExceeded();
|
||||
if (config.requireTokenOwnerMatchVault && !status.tokenOwnerMatchesVault) revert TokenOwnerMismatch();
|
||||
if (config.requireVaultBacking && !status.vaultAdequate) revert VaultBackingInsufficient();
|
||||
if (config.requireReserveSystemBalance && !status.reserveSystemAdequate) revert ReserveSystemBackingInsufficient();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getVerificationStatus(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector
|
||||
) external view returns (VerificationStatus memory status) {
|
||||
TokenConfig memory config = tokenConfigs[canonicalToken];
|
||||
if (!config.enabled) revert TokenNotConfigured();
|
||||
return _buildStatus(canonicalToken, destinationChainSelector, config);
|
||||
}
|
||||
|
||||
function _buildStatus(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector,
|
||||
TokenConfig memory config
|
||||
) internal view returns (VerificationStatus memory status) {
|
||||
status.supportedCanonicalToken = bridge.supportedCanonicalToken(canonicalToken);
|
||||
status.bridgePaused = bridge.paused(canonicalToken);
|
||||
status.lockedBalanceAmount = bridge.lockedBalance(canonicalToken);
|
||||
status.totalOutstandingAmount = bridge.totalOutstanding(canonicalToken);
|
||||
status.chainOutstandingAmount = bridge.outstandingMinted(canonicalToken, destinationChainSelector);
|
||||
status.maxOutstandingAmount = bridge.maxOutstanding(canonicalToken, destinationChainSelector);
|
||||
status.escrowSufficient = status.lockedBalanceAmount >= status.totalOutstandingAmount;
|
||||
status.chainCapSufficient =
|
||||
status.maxOutstandingAmount == 0 || status.chainOutstandingAmount <= status.maxOutstandingAmount;
|
||||
status.canonicalTotalSupply = IERC20(canonicalToken).totalSupply();
|
||||
|
||||
status.reserveSystemAdequate = !config.requireReserveSystemBalance;
|
||||
if (config.requireReserveSystemBalance) {
|
||||
status.reserveSystemBalance = reserveSystem.getReserveBalance(config.reserveAsset);
|
||||
status.reserveSystemAdequate = status.reserveSystemBalance >= status.canonicalTotalSupply;
|
||||
}
|
||||
|
||||
status.vaultAdequate = !config.requireVaultBacking;
|
||||
status.tokenOwnerMatchesVault = !config.requireTokenOwnerMatchVault;
|
||||
|
||||
if (config.requireTokenOwnerMatchVault) {
|
||||
try IOwnableLikeV2(canonicalToken).owner() returns (address tokenOwner) {
|
||||
status.tokenOwnerMatchesVault = tokenOwner == assetVault;
|
||||
} catch {
|
||||
status.tokenOwnerMatchesVault = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.requireVaultBacking) {
|
||||
try IBalanceOfAsset(config.reserveAsset).balanceOf(assetVault) returns (uint256 reserveBalance) {
|
||||
status.vaultReserveBalance = reserveBalance;
|
||||
if (status.canonicalTotalSupply > 0) {
|
||||
status.vaultBackingRatio = (reserveBalance * HARD_THRESHOLD_BPS) / status.canonicalTotalSupply;
|
||||
} else {
|
||||
status.vaultBackingRatio = HARD_THRESHOLD_BPS;
|
||||
}
|
||||
status.vaultAdequate =
|
||||
status.vaultBackingRatio >= HARD_THRESHOLD_BPS && reserveBalance >= status.canonicalTotalSupply;
|
||||
} catch {
|
||||
status.vaultAdequate = false;
|
||||
}
|
||||
}
|
||||
|
||||
status.passes =
|
||||
status.supportedCanonicalToken &&
|
||||
!status.bridgePaused &&
|
||||
status.escrowSufficient &&
|
||||
status.chainCapSufficient &&
|
||||
status.tokenOwnerMatchesVault &&
|
||||
status.vaultAdequate &&
|
||||
status.reserveSystemAdequate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "../../reserve/IReserveSystem.sol";
|
||||
import "./ICWReserveVerifier.sol";
|
||||
|
||||
interface ICWMultiTokenBridgeL1Accounting {
|
||||
function supportedCanonicalToken(address token) external view returns (bool);
|
||||
function paused(address token) external view returns (bool);
|
||||
function lockedBalance(address token) external view returns (uint256);
|
||||
function totalOutstanding(address token) external view returns (uint256);
|
||||
function outstandingMinted(address token, uint64 destinationChainSelector) external view returns (uint256);
|
||||
function maxOutstanding(address token, uint64 destinationChainSelector) external view returns (uint256);
|
||||
}
|
||||
|
||||
interface IOwnableLike {
|
||||
function owner() external view returns (address);
|
||||
}
|
||||
|
||||
interface IStablecoinReserveVaultLike {
|
||||
function compliantUSDT() external view returns (address);
|
||||
function compliantUSDC() external view returns (address);
|
||||
function getBackingRatio(address token) external view returns (
|
||||
uint256 reserveBalance,
|
||||
uint256 tokenSupply,
|
||||
uint256 backingRatio
|
||||
);
|
||||
function checkReserveAdequacy() external view returns (bool usdtAdequate, bool usdcAdequate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title CWReserveVerifier
|
||||
* @notice Verifies bridge escrow and canonical reserve backing before allowing new cW minting.
|
||||
* @dev Intended for cUSDC/cUSDT -> cWUSDC/cWUSDT hard-peg flows on Chain 138.
|
||||
*/
|
||||
contract CWReserveVerifier is AccessControl, ICWReserveVerifier {
|
||||
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
|
||||
uint256 public constant HARD_THRESHOLD_BPS = 10000;
|
||||
|
||||
struct TokenConfig {
|
||||
bool enabled;
|
||||
address reserveAsset;
|
||||
bool requireVaultBacking;
|
||||
bool requireReserveSystemBalance;
|
||||
bool requireTokenOwnerMatchVault;
|
||||
}
|
||||
|
||||
struct VerificationStatus {
|
||||
bool supportedCanonicalToken;
|
||||
bool bridgePaused;
|
||||
bool escrowSufficient;
|
||||
bool chainCapSufficient;
|
||||
bool reserveSystemAdequate;
|
||||
bool vaultAdequate;
|
||||
bool tokenOwnerMatchesVault;
|
||||
bool passes;
|
||||
uint256 lockedBalanceAmount;
|
||||
uint256 totalOutstandingAmount;
|
||||
uint256 chainOutstandingAmount;
|
||||
uint256 maxOutstandingAmount;
|
||||
uint256 canonicalTotalSupply;
|
||||
uint256 reserveSystemBalance;
|
||||
uint256 vaultReserveBalance;
|
||||
uint256 vaultBackingRatio;
|
||||
}
|
||||
|
||||
ICWMultiTokenBridgeL1Accounting public bridge;
|
||||
IStablecoinReserveVaultLike public stablecoinReserveVault;
|
||||
IReserveSystem public reserveSystem;
|
||||
|
||||
mapping(address => TokenConfig) public tokenConfigs;
|
||||
|
||||
event BridgeUpdated(address indexed newBridge);
|
||||
event StablecoinReserveVaultUpdated(address indexed newVault);
|
||||
event ReserveSystemUpdated(address indexed newReserveSystem);
|
||||
event TokenConfigured(
|
||||
address indexed canonicalToken,
|
||||
address indexed reserveAsset,
|
||||
bool requireVaultBacking,
|
||||
bool requireReserveSystemBalance,
|
||||
bool requireTokenOwnerMatchVault
|
||||
);
|
||||
event TokenDisabled(address indexed canonicalToken);
|
||||
|
||||
error ZeroAddress();
|
||||
error TokenNotConfigured();
|
||||
error VaultRequired();
|
||||
error ReserveSystemRequired();
|
||||
error UnsupportedCanonicalToken();
|
||||
error BridgeTokenPaused();
|
||||
error EscrowInvariantViolation();
|
||||
error ChainOutstandingCapExceeded();
|
||||
error TokenOwnerMismatch();
|
||||
error VaultBackingInsufficient();
|
||||
error ReserveSystemBackingInsufficient();
|
||||
|
||||
constructor(
|
||||
address admin,
|
||||
address bridge_,
|
||||
address stablecoinReserveVault_,
|
||||
address reserveSystem_
|
||||
) {
|
||||
if (admin == address(0) || bridge_ == address(0)) {
|
||||
revert ZeroAddress();
|
||||
}
|
||||
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(OPERATOR_ROLE, admin);
|
||||
|
||||
bridge = ICWMultiTokenBridgeL1Accounting(bridge_);
|
||||
stablecoinReserveVault = IStablecoinReserveVaultLike(stablecoinReserveVault_);
|
||||
reserveSystem = IReserveSystem(reserveSystem_);
|
||||
}
|
||||
|
||||
function setBridge(address bridge_) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
if (bridge_ == address(0)) {
|
||||
revert ZeroAddress();
|
||||
}
|
||||
bridge = ICWMultiTokenBridgeL1Accounting(bridge_);
|
||||
emit BridgeUpdated(bridge_);
|
||||
}
|
||||
|
||||
function setStablecoinReserveVault(address stablecoinReserveVault_) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
stablecoinReserveVault = IStablecoinReserveVaultLike(stablecoinReserveVault_);
|
||||
emit StablecoinReserveVaultUpdated(stablecoinReserveVault_);
|
||||
}
|
||||
|
||||
function setReserveSystem(address reserveSystem_) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
reserveSystem = IReserveSystem(reserveSystem_);
|
||||
emit ReserveSystemUpdated(reserveSystem_);
|
||||
}
|
||||
|
||||
function configureToken(
|
||||
address canonicalToken,
|
||||
address reserveAsset,
|
||||
bool requireVaultBacking,
|
||||
bool requireReserveSystemBalance,
|
||||
bool requireTokenOwnerMatchVault
|
||||
) external onlyRole(OPERATOR_ROLE) {
|
||||
if (canonicalToken == address(0)) {
|
||||
revert ZeroAddress();
|
||||
}
|
||||
if ((requireVaultBacking || requireTokenOwnerMatchVault) && address(stablecoinReserveVault) == address(0)) {
|
||||
revert VaultRequired();
|
||||
}
|
||||
if (requireReserveSystemBalance && address(reserveSystem) == address(0)) {
|
||||
revert ReserveSystemRequired();
|
||||
}
|
||||
if (requireReserveSystemBalance && reserveAsset == address(0)) {
|
||||
revert ZeroAddress();
|
||||
}
|
||||
|
||||
tokenConfigs[canonicalToken] = TokenConfig({
|
||||
enabled: true,
|
||||
reserveAsset: reserveAsset,
|
||||
requireVaultBacking: requireVaultBacking,
|
||||
requireReserveSystemBalance: requireReserveSystemBalance,
|
||||
requireTokenOwnerMatchVault: requireTokenOwnerMatchVault
|
||||
});
|
||||
|
||||
emit TokenConfigured(
|
||||
canonicalToken,
|
||||
reserveAsset,
|
||||
requireVaultBacking,
|
||||
requireReserveSystemBalance,
|
||||
requireTokenOwnerMatchVault
|
||||
);
|
||||
}
|
||||
|
||||
function disableToken(address canonicalToken) external onlyRole(OPERATOR_ROLE) {
|
||||
delete tokenConfigs[canonicalToken];
|
||||
emit TokenDisabled(canonicalToken);
|
||||
}
|
||||
|
||||
function verifyLock(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector,
|
||||
uint256
|
||||
) external view override returns (bool verified) {
|
||||
TokenConfig memory config = tokenConfigs[canonicalToken];
|
||||
if (!config.enabled) {
|
||||
revert TokenNotConfigured();
|
||||
}
|
||||
|
||||
VerificationStatus memory status = _buildStatus(canonicalToken, destinationChainSelector, config);
|
||||
|
||||
if (!status.supportedCanonicalToken) {
|
||||
revert UnsupportedCanonicalToken();
|
||||
}
|
||||
if (status.bridgePaused) {
|
||||
revert BridgeTokenPaused();
|
||||
}
|
||||
if (!status.escrowSufficient) {
|
||||
revert EscrowInvariantViolation();
|
||||
}
|
||||
if (!status.chainCapSufficient) {
|
||||
revert ChainOutstandingCapExceeded();
|
||||
}
|
||||
if (config.requireTokenOwnerMatchVault && !status.tokenOwnerMatchesVault) {
|
||||
revert TokenOwnerMismatch();
|
||||
}
|
||||
if (config.requireVaultBacking && !status.vaultAdequate) {
|
||||
revert VaultBackingInsufficient();
|
||||
}
|
||||
if (config.requireReserveSystemBalance && !status.reserveSystemAdequate) {
|
||||
revert ReserveSystemBackingInsufficient();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getVerificationStatus(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector
|
||||
) external view returns (VerificationStatus memory status) {
|
||||
TokenConfig memory config = tokenConfigs[canonicalToken];
|
||||
if (!config.enabled) {
|
||||
revert TokenNotConfigured();
|
||||
}
|
||||
return _buildStatus(canonicalToken, destinationChainSelector, config);
|
||||
}
|
||||
|
||||
function _buildStatus(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector,
|
||||
TokenConfig memory config
|
||||
) internal view returns (VerificationStatus memory status) {
|
||||
status.supportedCanonicalToken = bridge.supportedCanonicalToken(canonicalToken);
|
||||
status.bridgePaused = bridge.paused(canonicalToken);
|
||||
status.lockedBalanceAmount = bridge.lockedBalance(canonicalToken);
|
||||
status.totalOutstandingAmount = bridge.totalOutstanding(canonicalToken);
|
||||
status.chainOutstandingAmount = bridge.outstandingMinted(canonicalToken, destinationChainSelector);
|
||||
status.maxOutstandingAmount = bridge.maxOutstanding(canonicalToken, destinationChainSelector);
|
||||
status.escrowSufficient = status.lockedBalanceAmount >= status.totalOutstandingAmount;
|
||||
status.chainCapSufficient = status.maxOutstandingAmount == 0
|
||||
|| status.chainOutstandingAmount <= status.maxOutstandingAmount;
|
||||
|
||||
status.canonicalTotalSupply = IERC20(canonicalToken).totalSupply();
|
||||
|
||||
status.reserveSystemAdequate = !config.requireReserveSystemBalance;
|
||||
if (config.requireReserveSystemBalance) {
|
||||
status.reserveSystemBalance = reserveSystem.getReserveBalance(config.reserveAsset);
|
||||
status.reserveSystemAdequate = status.reserveSystemBalance >= status.canonicalTotalSupply;
|
||||
}
|
||||
|
||||
status.vaultAdequate = !config.requireVaultBacking;
|
||||
status.tokenOwnerMatchesVault = !config.requireTokenOwnerMatchVault;
|
||||
if (config.requireVaultBacking || config.requireTokenOwnerMatchVault) {
|
||||
address vaultAddress = address(stablecoinReserveVault);
|
||||
|
||||
if (config.requireTokenOwnerMatchVault) {
|
||||
try IOwnableLike(canonicalToken).owner() returns (address tokenOwner) {
|
||||
status.tokenOwnerMatchesVault = tokenOwner == vaultAddress;
|
||||
} catch {
|
||||
status.tokenOwnerMatchesVault = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.requireVaultBacking) {
|
||||
bool tokenTrackedByVault;
|
||||
bool tokenAdequate;
|
||||
|
||||
try stablecoinReserveVault.compliantUSDT() returns (address compliantUSDT) {
|
||||
if (canonicalToken == compliantUSDT) {
|
||||
tokenTrackedByVault = true;
|
||||
try stablecoinReserveVault.checkReserveAdequacy() returns (bool usdtAdequate, bool) {
|
||||
tokenAdequate = usdtAdequate;
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (!tokenTrackedByVault) {
|
||||
try stablecoinReserveVault.compliantUSDC() returns (address compliantUSDC) {
|
||||
if (canonicalToken == compliantUSDC) {
|
||||
tokenTrackedByVault = true;
|
||||
try stablecoinReserveVault.checkReserveAdequacy() returns (bool, bool usdcAdequate) {
|
||||
tokenAdequate = usdcAdequate;
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (tokenTrackedByVault) {
|
||||
try stablecoinReserveVault.getBackingRatio(canonicalToken) returns (
|
||||
uint256 reserveBalance,
|
||||
uint256,
|
||||
uint256 backingRatio
|
||||
) {
|
||||
status.vaultReserveBalance = reserveBalance;
|
||||
status.vaultBackingRatio = backingRatio;
|
||||
status.vaultAdequate = tokenAdequate
|
||||
&& backingRatio >= HARD_THRESHOLD_BPS
|
||||
&& reserveBalance >= status.canonicalTotalSupply;
|
||||
} catch {
|
||||
status.vaultAdequate = false;
|
||||
}
|
||||
} else {
|
||||
status.vaultAdequate = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
status.passes =
|
||||
status.supportedCanonicalToken
|
||||
&& !status.bridgePaused
|
||||
&& status.escrowSufficient
|
||||
&& status.chainCapSufficient
|
||||
&& status.tokenOwnerMatchesVault
|
||||
&& status.vaultAdequate
|
||||
&& status.reserveSystemAdequate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @title ICWReserveVerifier
|
||||
* @notice Interface for canonical reserve verification used by the cW hard-peg bridge path.
|
||||
*/
|
||||
interface ICWReserveVerifier {
|
||||
/**
|
||||
* @notice Verify that a new outbound c* -> cW* lock is still safe after bridge accounting is applied.
|
||||
* @param canonicalToken Canonical token locked on Chain 138
|
||||
* @param destinationChainSelector Destination chain selector for the wrapped mint
|
||||
* @param amount Amount being wrapped
|
||||
* @return verified True when the outbound lock satisfies all configured reserve checks
|
||||
*/
|
||||
function verifyLock(
|
||||
address canonicalToken,
|
||||
uint64 destinationChainSelector,
|
||||
uint256 amount
|
||||
) external view returns (bool verified);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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/extensions/IERC20Metadata.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/Pausable.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
|
||||
interface ICWMintBurnMetadata is IERC20Metadata {
|
||||
function mint(address to, uint256 amount) external;
|
||||
function burnFrom(address from, uint256 amount) external;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title USDWPublicWrapVault
|
||||
* @notice Locks native public-chain USDW and mints the shared cWUSDW mirror 1:1 after decimals normalization.
|
||||
* @dev The same cWUSDW supply can be minted by both this vault and the GRU bridge. This contract deliberately
|
||||
* does not expose an admin path to withdraw the underlying USDW reserve because that requires a fuller
|
||||
* liability model across wrap-originated and bridge-originated cWUSDW supply.
|
||||
*/
|
||||
contract USDWPublicWrapVault is AccessControl, Pausable, ReentrancyGuard {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 public constant RESERVE_OPERATOR_ROLE = keccak256("RESERVE_OPERATOR_ROLE");
|
||||
bytes32 public constant EMERGENCY_ADMIN_ROLE = keccak256("EMERGENCY_ADMIN_ROLE");
|
||||
|
||||
IERC20 public immutable nativeUsdw;
|
||||
ICWMintBurnMetadata public immutable wrappedUsdw;
|
||||
uint8 public immutable nativeDecimals;
|
||||
uint8 public immutable wrappedDecimals;
|
||||
|
||||
event LiquiditySeeded(address indexed operator, uint256 nativeAmount, uint256 wrappedEquivalent);
|
||||
event Wrapped(
|
||||
address indexed caller,
|
||||
address indexed recipient,
|
||||
uint256 nativeAmount,
|
||||
uint256 wrappedAmount
|
||||
);
|
||||
event Unwrapped(
|
||||
address indexed caller,
|
||||
address indexed recipient,
|
||||
uint256 wrappedAmount,
|
||||
uint256 nativeAmount
|
||||
);
|
||||
event NonUnderlyingTokenRecovered(address indexed token, address indexed recipient, uint256 amount);
|
||||
|
||||
error InsufficientUnderlyingLiquidity(uint256 requested, uint256 available);
|
||||
error NonCanonicalAmount(uint256 amount);
|
||||
error UnderlyingTokenProtected();
|
||||
error UnsupportedDecimals(uint8 nativeDecimals, uint8 wrappedDecimals);
|
||||
error ZeroAddress();
|
||||
error ZeroAmount();
|
||||
error ZeroRecipient();
|
||||
|
||||
constructor(address admin, address nativeUsdw_, address wrappedUsdw_) {
|
||||
if (admin == address(0) || nativeUsdw_ == address(0) || wrappedUsdw_ == address(0)) revert ZeroAddress();
|
||||
|
||||
nativeUsdw = IERC20(nativeUsdw_);
|
||||
wrappedUsdw = ICWMintBurnMetadata(wrappedUsdw_);
|
||||
nativeDecimals = IERC20Metadata(nativeUsdw_).decimals();
|
||||
wrappedDecimals = IERC20Metadata(wrappedUsdw_).decimals();
|
||||
|
||||
uint8 diff = nativeDecimals > wrappedDecimals
|
||||
? nativeDecimals - wrappedDecimals
|
||||
: wrappedDecimals - nativeDecimals;
|
||||
if (diff > 77) revert UnsupportedDecimals(nativeDecimals, wrappedDecimals);
|
||||
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(RESERVE_OPERATOR_ROLE, admin);
|
||||
_grantRole(EMERGENCY_ADMIN_ROLE, admin);
|
||||
}
|
||||
|
||||
function seedLiquidity(uint256 nativeAmount) external onlyRole(RESERVE_OPERATOR_ROLE) nonReentrant {
|
||||
if (nativeAmount == 0) revert ZeroAmount();
|
||||
|
||||
nativeUsdw.safeTransferFrom(msg.sender, address(this), nativeAmount);
|
||||
emit LiquiditySeeded(msg.sender, nativeAmount, _toWrappedFloor(nativeAmount));
|
||||
}
|
||||
|
||||
function wrap(uint256 nativeAmount, address recipient) external whenNotPaused nonReentrant returns (uint256 wrappedAmount) {
|
||||
if (nativeAmount == 0) revert ZeroAmount();
|
||||
if (recipient == address(0)) revert ZeroRecipient();
|
||||
|
||||
wrappedAmount = _toWrappedExact(nativeAmount);
|
||||
nativeUsdw.safeTransferFrom(msg.sender, address(this), nativeAmount);
|
||||
wrappedUsdw.mint(recipient, wrappedAmount);
|
||||
|
||||
emit Wrapped(msg.sender, recipient, nativeAmount, wrappedAmount);
|
||||
}
|
||||
|
||||
function unwrap(uint256 wrappedAmount, address recipient) external whenNotPaused nonReentrant returns (uint256 nativeAmount) {
|
||||
if (wrappedAmount == 0) revert ZeroAmount();
|
||||
if (recipient == address(0)) revert ZeroRecipient();
|
||||
|
||||
nativeAmount = _toNativeExact(wrappedAmount);
|
||||
uint256 available = nativeUsdw.balanceOf(address(this));
|
||||
if (available < nativeAmount) revert InsufficientUnderlyingLiquidity(nativeAmount, available);
|
||||
|
||||
wrappedUsdw.burnFrom(msg.sender, wrappedAmount);
|
||||
nativeUsdw.safeTransfer(recipient, nativeAmount);
|
||||
|
||||
emit Unwrapped(msg.sender, recipient, wrappedAmount, nativeAmount);
|
||||
}
|
||||
|
||||
function pause() external onlyRole(EMERGENCY_ADMIN_ROLE) {
|
||||
_pause();
|
||||
}
|
||||
|
||||
function unpause() external onlyRole(EMERGENCY_ADMIN_ROLE) {
|
||||
_unpause();
|
||||
}
|
||||
|
||||
function recoverNonUnderlyingToken(
|
||||
address token,
|
||||
address recipient,
|
||||
uint256 amount
|
||||
) external onlyRole(EMERGENCY_ADMIN_ROLE) nonReentrant {
|
||||
if (token == address(nativeUsdw)) revert UnderlyingTokenProtected();
|
||||
if (recipient == address(0)) revert ZeroRecipient();
|
||||
IERC20(token).safeTransfer(recipient, amount);
|
||||
emit NonUnderlyingTokenRecovered(token, recipient, amount);
|
||||
}
|
||||
|
||||
function availableUnderlyingLiquidity() external view returns (uint256) {
|
||||
return nativeUsdw.balanceOf(address(this));
|
||||
}
|
||||
|
||||
function availableWrappedLiquidity() external view returns (uint256) {
|
||||
return _toWrappedFloor(nativeUsdw.balanceOf(address(this)));
|
||||
}
|
||||
|
||||
function wrappedSupply() external view returns (uint256) {
|
||||
return wrappedUsdw.totalSupply();
|
||||
}
|
||||
|
||||
function liquidityCoverageBps() external view returns (uint256) {
|
||||
uint256 supply = wrappedUsdw.totalSupply();
|
||||
if (supply == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (_toWrappedFloor(nativeUsdw.balanceOf(address(this))) * 10_000) / supply;
|
||||
}
|
||||
|
||||
function previewWrap(uint256 nativeAmount) external view returns (uint256) {
|
||||
if (nativeAmount == 0) revert ZeroAmount();
|
||||
return _toWrappedExact(nativeAmount);
|
||||
}
|
||||
|
||||
function previewUnwrap(uint256 wrappedAmount) external view returns (uint256) {
|
||||
if (wrappedAmount == 0) revert ZeroAmount();
|
||||
return _toNativeExact(wrappedAmount);
|
||||
}
|
||||
|
||||
function _toWrappedExact(uint256 nativeAmount) internal view returns (uint256) {
|
||||
if (nativeDecimals == wrappedDecimals) {
|
||||
return nativeAmount;
|
||||
}
|
||||
if (nativeDecimals > wrappedDecimals) {
|
||||
uint256 divisor = 10 ** (nativeDecimals - wrappedDecimals);
|
||||
if (nativeAmount % divisor != 0) revert NonCanonicalAmount(nativeAmount);
|
||||
return nativeAmount / divisor;
|
||||
}
|
||||
return nativeAmount * (10 ** (wrappedDecimals - nativeDecimals));
|
||||
}
|
||||
|
||||
function _toNativeExact(uint256 wrappedAmount) internal view returns (uint256) {
|
||||
if (nativeDecimals == wrappedDecimals) {
|
||||
return wrappedAmount;
|
||||
}
|
||||
if (wrappedDecimals > nativeDecimals) {
|
||||
uint256 divisor = 10 ** (wrappedDecimals - nativeDecimals);
|
||||
if (wrappedAmount % divisor != 0) revert NonCanonicalAmount(wrappedAmount);
|
||||
return wrappedAmount / divisor;
|
||||
}
|
||||
return wrappedAmount * (10 ** (nativeDecimals - wrappedDecimals));
|
||||
}
|
||||
|
||||
function _toWrappedFloor(uint256 nativeAmount) internal view returns (uint256) {
|
||||
if (nativeDecimals == wrappedDecimals) {
|
||||
return nativeAmount;
|
||||
}
|
||||
if (nativeDecimals > wrappedDecimals) {
|
||||
return nativeAmount / (10 ** (nativeDecimals - wrappedDecimals));
|
||||
}
|
||||
return nativeAmount * (10 ** (wrappedDecimals - nativeDecimals));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
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 "./LiquidityPoolETH.sol";
|
||||
import "./RouteTypesV2.sol";
|
||||
import "./interfaces/IRouteExecutorAdapter.sol";
|
||||
import "./interfaces/IWETH.sol";
|
||||
|
||||
contract EnhancedSwapRouterV2 is AccessControl, ReentrancyGuard {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 public constant ROUTING_MANAGER_ROLE = keccak256("ROUTING_MANAGER_ROLE");
|
||||
bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE");
|
||||
|
||||
struct ProviderRouteConfig {
|
||||
address target;
|
||||
bytes providerData;
|
||||
bool enabled;
|
||||
}
|
||||
|
||||
address public immutable weth;
|
||||
address public immutable usdt;
|
||||
address public immutable usdc;
|
||||
address public immutable dai;
|
||||
|
||||
mapping(uint8 => address) public providerAdapters;
|
||||
mapping(uint8 => bool) public providerEnabled;
|
||||
mapping(address => mapping(address => mapping(uint8 => ProviderRouteConfig))) private providerRoutes;
|
||||
mapping(uint256 => RouteTypesV2.Provider[]) private sizeBasedRouting;
|
||||
|
||||
event ProviderAdapterSet(RouteTypesV2.Provider indexed provider, address indexed adapter);
|
||||
event ProviderRouteSet(
|
||||
address indexed tokenIn,
|
||||
address indexed tokenOut,
|
||||
RouteTypesV2.Provider indexed provider,
|
||||
address target,
|
||||
bool enabled
|
||||
);
|
||||
event ProviderToggled(RouteTypesV2.Provider indexed provider, bool enabled);
|
||||
event RoutingConfigUpdated(uint256 indexed sizeCategory, RouteTypesV2.Provider[] providers);
|
||||
event RouteExecuted(
|
||||
bytes32 indexed routeHash,
|
||||
address indexed caller,
|
||||
address indexed recipient,
|
||||
address inputToken,
|
||||
address outputToken,
|
||||
uint256 amountIn,
|
||||
uint256 amountOut
|
||||
);
|
||||
|
||||
error ZeroAddress();
|
||||
error ZeroAmount();
|
||||
error InvalidPlan();
|
||||
error InvalidStablecoin();
|
||||
error ProviderDisabled();
|
||||
error ProviderAdapterNotConfigured();
|
||||
error RouteNotConfigured();
|
||||
error RouteValidationFailed(string reason);
|
||||
error InsufficientOutput();
|
||||
|
||||
constructor(address _weth, address _usdt, address _usdc, address _dai) {
|
||||
if (_weth == address(0) || _usdt == address(0) || _usdc == address(0) || _dai == address(0)) {
|
||||
revert ZeroAddress();
|
||||
}
|
||||
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
_grantRole(ROUTING_MANAGER_ROLE, msg.sender);
|
||||
|
||||
weth = _weth;
|
||||
usdt = _usdt;
|
||||
usdc = _usdc;
|
||||
dai = _dai;
|
||||
|
||||
providerEnabled[uint8(RouteTypesV2.Provider.Dodo)] = true;
|
||||
providerEnabled[uint8(RouteTypesV2.Provider.UniswapV3)] = true;
|
||||
providerEnabled[uint8(RouteTypesV2.Provider.Balancer)] = true;
|
||||
providerEnabled[uint8(RouteTypesV2.Provider.Curve)] = true;
|
||||
providerEnabled[uint8(RouteTypesV2.Provider.OneInch)] = false;
|
||||
providerEnabled[uint8(RouteTypesV2.Provider.Partner)] = false;
|
||||
providerEnabled[uint8(RouteTypesV2.Provider.DodoV3)] = true;
|
||||
|
||||
_initializeDefaultRouting();
|
||||
}
|
||||
|
||||
function setProviderAdapter(
|
||||
RouteTypesV2.Provider provider,
|
||||
address adapter
|
||||
) external onlyRole(ROUTING_MANAGER_ROLE) {
|
||||
if (adapter == address(0)) revert ZeroAddress();
|
||||
providerAdapters[uint8(provider)] = adapter;
|
||||
emit ProviderAdapterSet(provider, adapter);
|
||||
}
|
||||
|
||||
function setProviderRoute(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
RouteTypesV2.Provider provider,
|
||||
address target,
|
||||
bytes calldata providerData,
|
||||
bool enabled
|
||||
) external onlyRole(ROUTING_MANAGER_ROLE) {
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || target == address(0)) revert ZeroAddress();
|
||||
providerRoutes[tokenIn][tokenOut][uint8(provider)] = ProviderRouteConfig({
|
||||
target: target,
|
||||
providerData: providerData,
|
||||
enabled: enabled
|
||||
});
|
||||
emit ProviderRouteSet(tokenIn, tokenOut, provider, target, enabled);
|
||||
}
|
||||
|
||||
function getProviderRoute(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
RouteTypesV2.Provider provider
|
||||
) external view returns (address target, bytes memory providerData, bool enabled) {
|
||||
ProviderRouteConfig storage config = providerRoutes[tokenIn][tokenOut][uint8(provider)];
|
||||
return (config.target, config.providerData, config.enabled);
|
||||
}
|
||||
|
||||
function setProviderEnabled(
|
||||
RouteTypesV2.Provider provider,
|
||||
bool enabled
|
||||
) external onlyRole(ROUTING_MANAGER_ROLE) {
|
||||
providerEnabled[uint8(provider)] = enabled;
|
||||
emit ProviderToggled(provider, enabled);
|
||||
}
|
||||
|
||||
function setRoutingConfig(
|
||||
uint256 sizeCategory,
|
||||
RouteTypesV2.Provider[] calldata providers
|
||||
) external onlyRole(ROUTING_MANAGER_ROLE) {
|
||||
require(sizeCategory < 3, "EnhancedSwapRouterV2: invalid size category");
|
||||
require(providers.length > 0, "EnhancedSwapRouterV2: empty providers");
|
||||
|
||||
delete sizeBasedRouting[sizeCategory];
|
||||
for (uint256 i = 0; i < providers.length; i++) {
|
||||
sizeBasedRouting[sizeCategory].push(providers[i]);
|
||||
}
|
||||
|
||||
emit RoutingConfigUpdated(sizeCategory, providers);
|
||||
}
|
||||
|
||||
function executeRoute(
|
||||
RouteTypesV2.RoutePlan calldata plan
|
||||
) external payable nonReentrant returns (uint256 amountOut) {
|
||||
RouteTypesV2.RoutePlan memory memoryPlan = _copyPlan(plan);
|
||||
return _executeRoute(memoryPlan, msg.sender, msg.value);
|
||||
}
|
||||
|
||||
function quoteConfiguredProviders(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountIn
|
||||
) external view returns (RouteTypesV2.ProviderQuote[] memory quotes) {
|
||||
RouteTypesV2.ProviderQuote[] memory temp = new RouteTypesV2.ProviderQuote[](uint256(type(RouteTypesV2.Provider).max) + 1);
|
||||
uint256 count = 0;
|
||||
|
||||
for (uint8 providerId = 0; providerId <= uint8(type(RouteTypesV2.Provider).max); providerId++) {
|
||||
if (!providerEnabled[providerId]) {
|
||||
continue;
|
||||
}
|
||||
address adapter = providerAdapters[providerId];
|
||||
if (adapter == address(0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ProviderRouteConfig storage routeConfig = providerRoutes[tokenIn][tokenOut][providerId];
|
||||
if (!routeConfig.enabled || routeConfig.target == address(0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RouteTypesV2.RouteLeg memory leg = RouteTypesV2.RouteLeg({
|
||||
provider: RouteTypesV2.Provider(providerId),
|
||||
tokenIn: tokenIn,
|
||||
tokenOut: tokenOut,
|
||||
amountSource: RouteTypesV2.AmountSource.UserInput,
|
||||
minAmountOut: 0,
|
||||
target: routeConfig.target,
|
||||
providerData: routeConfig.providerData
|
||||
});
|
||||
|
||||
(bool ok,) = IRouteExecutorAdapter(adapter).validate(leg);
|
||||
if (!ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(uint256 amountOut, uint256 estimatedGas) = IRouteExecutorAdapter(adapter).quote(leg, amountIn);
|
||||
temp[count] = RouteTypesV2.ProviderQuote({
|
||||
provider: RouteTypesV2.Provider(providerId),
|
||||
target: routeConfig.target,
|
||||
amountOut: amountOut,
|
||||
estimatedGas: estimatedGas,
|
||||
executable: amountOut > 0,
|
||||
providerData: routeConfig.providerData
|
||||
});
|
||||
count++;
|
||||
}
|
||||
|
||||
quotes = new RouteTypesV2.ProviderQuote[](count);
|
||||
for (uint256 i = 0; i < count; i++) {
|
||||
quotes[i] = temp[i];
|
||||
}
|
||||
}
|
||||
|
||||
function swapTokenToToken(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountIn,
|
||||
uint256 amountOutMin
|
||||
) external returns (uint256 amountOut) {
|
||||
return swapTokenToToken(tokenIn, tokenOut, amountIn, amountOutMin, _defaultPreferredProvider());
|
||||
}
|
||||
|
||||
function swapTokenToToken(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountIn,
|
||||
uint256 amountOutMin,
|
||||
RouteTypesV2.Provider preferredProvider
|
||||
) public returns (uint256 amountOut) {
|
||||
if (amountIn == 0) revert ZeroAmount();
|
||||
ProviderRouteConfig memory routeConfig = _getRouteConfig(tokenIn, tokenOut, preferredProvider);
|
||||
RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
|
||||
legs[0] = RouteTypesV2.RouteLeg({
|
||||
provider: preferredProvider,
|
||||
tokenIn: tokenIn,
|
||||
tokenOut: tokenOut,
|
||||
amountSource: RouteTypesV2.AmountSource.UserInput,
|
||||
minAmountOut: amountOutMin,
|
||||
target: routeConfig.target,
|
||||
providerData: routeConfig.providerData
|
||||
});
|
||||
|
||||
RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
|
||||
chainId: block.chainid,
|
||||
inputToken: tokenIn,
|
||||
outputToken: tokenOut,
|
||||
amountIn: amountIn,
|
||||
minAmountOut: amountOutMin,
|
||||
recipient: msg.sender,
|
||||
deadline: block.timestamp + 300,
|
||||
legs: legs
|
||||
});
|
||||
|
||||
return _executeRoute(plan, msg.sender, 0);
|
||||
}
|
||||
|
||||
function swapToStablecoin(
|
||||
LiquidityPoolETH.AssetType inputAsset,
|
||||
address stablecoinToken,
|
||||
uint256 amountIn,
|
||||
uint256 amountOutMin
|
||||
) external payable returns (uint256 amountOut) {
|
||||
return swapToStablecoin(inputAsset, stablecoinToken, amountIn, amountOutMin, _defaultPreferredProvider());
|
||||
}
|
||||
|
||||
function swapToStablecoin(
|
||||
LiquidityPoolETH.AssetType inputAsset,
|
||||
address stablecoinToken,
|
||||
uint256 amountIn,
|
||||
uint256 amountOutMin,
|
||||
RouteTypesV2.Provider preferredProvider
|
||||
) public payable returns (uint256 amountOut) {
|
||||
if (amountIn == 0) revert ZeroAmount();
|
||||
if (!_isValidStablecoin(stablecoinToken)) revert InvalidStablecoin();
|
||||
|
||||
address inputToken = inputAsset == LiquidityPoolETH.AssetType.ETH ? address(0) : weth;
|
||||
ProviderRouteConfig memory routeConfig = _getRouteConfig(weth, stablecoinToken, preferredProvider);
|
||||
RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
|
||||
legs[0] = RouteTypesV2.RouteLeg({
|
||||
provider: preferredProvider,
|
||||
tokenIn: weth,
|
||||
tokenOut: stablecoinToken,
|
||||
amountSource: RouteTypesV2.AmountSource.UserInput,
|
||||
minAmountOut: amountOutMin,
|
||||
target: routeConfig.target,
|
||||
providerData: routeConfig.providerData
|
||||
});
|
||||
|
||||
RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
|
||||
chainId: block.chainid,
|
||||
inputToken: inputToken,
|
||||
outputToken: stablecoinToken,
|
||||
amountIn: amountIn,
|
||||
minAmountOut: amountOutMin,
|
||||
recipient: msg.sender,
|
||||
deadline: block.timestamp + 300,
|
||||
legs: legs
|
||||
});
|
||||
|
||||
return _executeRoute(plan, msg.sender, msg.value);
|
||||
}
|
||||
|
||||
function _executeRoute(
|
||||
RouteTypesV2.RoutePlan memory plan,
|
||||
address payer,
|
||||
uint256 nativeValue
|
||||
) internal returns (uint256 amountOut) {
|
||||
_validatePlan(plan);
|
||||
|
||||
address currentToken = plan.inputToken;
|
||||
uint256 currentAmount = plan.amountIn;
|
||||
if (currentToken == address(0)) {
|
||||
require(nativeValue == currentAmount, "EnhancedSwapRouterV2: native amount mismatch");
|
||||
IWETH(weth).deposit{value: currentAmount}();
|
||||
currentToken = weth;
|
||||
} else {
|
||||
IERC20(currentToken).safeTransferFrom(payer, address(this), currentAmount);
|
||||
}
|
||||
|
||||
for (uint256 i = 0; i < plan.legs.length; i++) {
|
||||
RouteTypesV2.RouteLeg memory leg = plan.legs[i];
|
||||
if (leg.tokenIn != currentToken) revert InvalidPlan();
|
||||
if (i == 0 && leg.amountSource != RouteTypesV2.AmountSource.UserInput) revert InvalidPlan();
|
||||
if (i > 0 && leg.amountSource != RouteTypesV2.AmountSource.PreviousLeg) revert InvalidPlan();
|
||||
|
||||
uint8 providerId = uint8(leg.provider);
|
||||
if (!providerEnabled[providerId]) revert ProviderDisabled();
|
||||
|
||||
address adapter = providerAdapters[providerId];
|
||||
if (adapter == address(0)) revert ProviderAdapterNotConfigured();
|
||||
|
||||
(bool ok, string memory reason) = IRouteExecutorAdapter(adapter).validate(leg);
|
||||
if (!ok) revert RouteValidationFailed(reason);
|
||||
|
||||
IERC20(currentToken).safeTransfer(adapter, currentAmount);
|
||||
uint256 balanceBefore = IERC20(leg.tokenOut).balanceOf(address(this));
|
||||
uint256 reportedAmountOut = IRouteExecutorAdapter(adapter).execute(leg, currentAmount);
|
||||
uint256 balanceAfter = IERC20(leg.tokenOut).balanceOf(address(this));
|
||||
uint256 actualAmountOut = balanceAfter - balanceBefore;
|
||||
if (actualAmountOut == 0) {
|
||||
actualAmountOut = reportedAmountOut;
|
||||
}
|
||||
if (actualAmountOut < leg.minAmountOut) revert InsufficientOutput();
|
||||
|
||||
currentToken = leg.tokenOut;
|
||||
currentAmount = actualAmountOut;
|
||||
}
|
||||
|
||||
if (currentToken != plan.outputToken) revert InvalidPlan();
|
||||
if (currentAmount < plan.minAmountOut) revert InsufficientOutput();
|
||||
|
||||
address recipient = plan.recipient == address(0) ? payer : plan.recipient;
|
||||
IERC20(currentToken).safeTransfer(recipient, currentAmount);
|
||||
|
||||
emit RouteExecuted(
|
||||
keccak256(abi.encode(plan.chainId, plan.inputToken, plan.outputToken, plan.amountIn, plan.deadline)),
|
||||
payer,
|
||||
recipient,
|
||||
plan.inputToken == address(0) ? weth : plan.inputToken,
|
||||
currentToken,
|
||||
plan.amountIn,
|
||||
currentAmount
|
||||
);
|
||||
|
||||
return currentAmount;
|
||||
}
|
||||
|
||||
function _getRouteConfig(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
RouteTypesV2.Provider preferredProvider
|
||||
) internal view returns (ProviderRouteConfig memory) {
|
||||
RouteTypesV2.Provider[] memory providers = _getRoutingProviders(preferredProvider);
|
||||
for (uint256 i = 0; i < providers.length; i++) {
|
||||
uint8 providerId = uint8(providers[i]);
|
||||
ProviderRouteConfig storage config = providerRoutes[tokenIn][tokenOut][providerId];
|
||||
if (config.enabled && config.target != address(0) && providerEnabled[providerId]) {
|
||||
return ProviderRouteConfig({
|
||||
target: config.target,
|
||||
providerData: config.providerData,
|
||||
enabled: config.enabled
|
||||
});
|
||||
}
|
||||
}
|
||||
revert RouteNotConfigured();
|
||||
}
|
||||
|
||||
function _getRoutingProviders(
|
||||
RouteTypesV2.Provider preferredProvider
|
||||
) internal view returns (RouteTypesV2.Provider[] memory providers) {
|
||||
if (providerEnabled[uint8(preferredProvider)]) {
|
||||
providers = new RouteTypesV2.Provider[](1);
|
||||
providers[0] = preferredProvider;
|
||||
return providers;
|
||||
}
|
||||
|
||||
providers = sizeBasedRouting[0];
|
||||
if (providers.length == 0) {
|
||||
providers = new RouteTypesV2.Provider[](5);
|
||||
providers[0] = RouteTypesV2.Provider.Dodo;
|
||||
providers[1] = RouteTypesV2.Provider.UniswapV3;
|
||||
providers[2] = RouteTypesV2.Provider.Balancer;
|
||||
providers[3] = RouteTypesV2.Provider.Curve;
|
||||
providers[4] = RouteTypesV2.Provider.OneInch;
|
||||
}
|
||||
}
|
||||
|
||||
function _validatePlan(RouteTypesV2.RoutePlan memory plan) internal view {
|
||||
if (plan.amountIn == 0 || plan.legs.length == 0) revert InvalidPlan();
|
||||
if (plan.outputToken == address(0)) revert InvalidPlan();
|
||||
if (plan.chainId != block.chainid) revert InvalidPlan();
|
||||
if (plan.deadline < block.timestamp) revert InvalidPlan();
|
||||
}
|
||||
|
||||
function _initializeDefaultRouting() internal {
|
||||
sizeBasedRouting[0].push(RouteTypesV2.Provider.Dodo);
|
||||
sizeBasedRouting[0].push(RouteTypesV2.Provider.UniswapV3);
|
||||
|
||||
sizeBasedRouting[1].push(RouteTypesV2.Provider.Dodo);
|
||||
sizeBasedRouting[1].push(RouteTypesV2.Provider.Balancer);
|
||||
sizeBasedRouting[1].push(RouteTypesV2.Provider.UniswapV3);
|
||||
|
||||
sizeBasedRouting[2].push(RouteTypesV2.Provider.Dodo);
|
||||
sizeBasedRouting[2].push(RouteTypesV2.Provider.Curve);
|
||||
sizeBasedRouting[2].push(RouteTypesV2.Provider.Balancer);
|
||||
}
|
||||
|
||||
function _copyPlan(
|
||||
RouteTypesV2.RoutePlan calldata plan
|
||||
) internal pure returns (RouteTypesV2.RoutePlan memory copied) {
|
||||
copied.chainId = plan.chainId;
|
||||
copied.inputToken = plan.inputToken;
|
||||
copied.outputToken = plan.outputToken;
|
||||
copied.amountIn = plan.amountIn;
|
||||
copied.minAmountOut = plan.minAmountOut;
|
||||
copied.recipient = plan.recipient;
|
||||
copied.deadline = plan.deadline;
|
||||
copied.legs = new RouteTypesV2.RouteLeg[](plan.legs.length);
|
||||
|
||||
for (uint256 i = 0; i < plan.legs.length; i++) {
|
||||
copied.legs[i] = RouteTypesV2.RouteLeg({
|
||||
provider: plan.legs[i].provider,
|
||||
tokenIn: plan.legs[i].tokenIn,
|
||||
tokenOut: plan.legs[i].tokenOut,
|
||||
amountSource: plan.legs[i].amountSource,
|
||||
minAmountOut: plan.legs[i].minAmountOut,
|
||||
target: plan.legs[i].target,
|
||||
providerData: plan.legs[i].providerData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _defaultPreferredProvider() internal pure returns (RouteTypesV2.Provider) {
|
||||
return RouteTypesV2.Provider.Dodo;
|
||||
}
|
||||
|
||||
function _isValidStablecoin(address token) internal view returns (bool) {
|
||||
return token == usdt || token == usdc || token == dai;
|
||||
}
|
||||
|
||||
receive() external payable {}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
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 "./RouteTypesV2.sol";
|
||||
import "./interfaces/IEnhancedSwapRouterV2.sol";
|
||||
import "./interfaces/IBridgeIntentExecutor.sol";
|
||||
|
||||
contract IntentBridgeCoordinatorV2 is AccessControl, ReentrancyGuard {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 public constant EXECUTOR_MANAGER_ROLE = keccak256("EXECUTOR_MANAGER_ROLE");
|
||||
|
||||
IEnhancedSwapRouterV2 public immutable swapRouter;
|
||||
mapping(bytes32 => address) public bridgeExecutors;
|
||||
|
||||
event BridgeExecutorSet(bytes32 indexed bridgeType, address indexed executor);
|
||||
event IntentExecuted(
|
||||
bytes32 indexed intentHash,
|
||||
bytes32 indexed bridgeReference,
|
||||
bytes32 indexed destinationPlanHash,
|
||||
address inputToken,
|
||||
address bridgeToken,
|
||||
uint256 amountIn,
|
||||
uint256 bridgedAmount,
|
||||
address recipient
|
||||
);
|
||||
|
||||
error ZeroAddress();
|
||||
error InvalidIntent();
|
||||
error BridgeExecutorNotConfigured();
|
||||
error BridgeValidationFailed(string reason);
|
||||
|
||||
constructor(address _swapRouter) {
|
||||
if (_swapRouter == address(0)) revert ZeroAddress();
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
_grantRole(EXECUTOR_MANAGER_ROLE, msg.sender);
|
||||
swapRouter = IEnhancedSwapRouterV2(_swapRouter);
|
||||
}
|
||||
|
||||
function setBridgeExecutor(
|
||||
bytes32 bridgeType,
|
||||
address executor
|
||||
) external onlyRole(EXECUTOR_MANAGER_ROLE) {
|
||||
if (executor == address(0)) revert ZeroAddress();
|
||||
bridgeExecutors[bridgeType] = executor;
|
||||
emit BridgeExecutorSet(bridgeType, executor);
|
||||
}
|
||||
|
||||
function executeIntent(
|
||||
RouteTypesV2.BridgeIntentPlan calldata intent
|
||||
) external payable nonReentrant returns (bytes32 bridgeReference, bytes32 destinationPlanHash) {
|
||||
if (intent.deadline < block.timestamp) revert InvalidIntent();
|
||||
if (intent.recipient == address(0)) revert InvalidIntent();
|
||||
if (intent.sourcePlan.deadline < block.timestamp) revert InvalidIntent();
|
||||
if (intent.sourcePlan.recipient != address(this)) revert InvalidIntent();
|
||||
|
||||
address bridgeExecutor = bridgeExecutors[intent.bridgeType];
|
||||
if (bridgeExecutor == address(0)) revert BridgeExecutorNotConfigured();
|
||||
|
||||
uint256 bridgedAmount;
|
||||
address bridgeToken;
|
||||
if (intent.sourcePlan.inputToken == address(0)) {
|
||||
bridgedAmount = swapRouter.executeRoute{value: intent.sourcePlan.amountIn}(intent.sourcePlan);
|
||||
bridgeToken = intent.sourcePlan.outputToken;
|
||||
} else {
|
||||
IERC20(intent.sourcePlan.inputToken).safeTransferFrom(
|
||||
msg.sender,
|
||||
address(this),
|
||||
intent.sourcePlan.amountIn
|
||||
);
|
||||
IERC20(intent.sourcePlan.inputToken).forceApprove(address(swapRouter), 0);
|
||||
IERC20(intent.sourcePlan.inputToken).forceApprove(address(swapRouter), intent.sourcePlan.amountIn);
|
||||
bridgedAmount = swapRouter.executeRoute(intent.sourcePlan);
|
||||
bridgeToken = intent.sourcePlan.outputToken;
|
||||
}
|
||||
|
||||
(bool ok, string memory reason) = IBridgeIntentExecutor(bridgeExecutor).validateBridge(
|
||||
intent.bridgeType,
|
||||
intent.bridgeData,
|
||||
bridgeToken,
|
||||
bridgedAmount,
|
||||
intent.recipient
|
||||
);
|
||||
if (!ok) revert BridgeValidationFailed(reason);
|
||||
|
||||
IERC20(bridgeToken).forceApprove(bridgeExecutor, 0);
|
||||
IERC20(bridgeToken).forceApprove(bridgeExecutor, bridgedAmount);
|
||||
|
||||
bridgeReference = IBridgeIntentExecutor(bridgeExecutor).executeBridge(
|
||||
intent.bridgeType,
|
||||
intent.bridgeData,
|
||||
bridgeToken,
|
||||
bridgedAmount,
|
||||
intent.recipient
|
||||
);
|
||||
destinationPlanHash = keccak256(
|
||||
abi.encode(
|
||||
intent.destinationPlan.chainId,
|
||||
intent.destinationPlan.inputToken,
|
||||
intent.destinationPlan.outputToken,
|
||||
intent.destinationPlan.amountIn,
|
||||
intent.destinationPlan.minAmountOut,
|
||||
intent.destinationPlan.recipient,
|
||||
intent.destinationPlan.deadline,
|
||||
intent.destinationPlan.legs
|
||||
)
|
||||
);
|
||||
|
||||
emit IntentExecuted(
|
||||
keccak256(abi.encode(intent.bridgeType, intent.recipient, intent.deadline)),
|
||||
bridgeReference,
|
||||
destinationPlanHash,
|
||||
intent.sourcePlan.inputToken == address(0) ? address(0) : intent.sourcePlan.inputToken,
|
||||
bridgeToken,
|
||||
intent.sourcePlan.amountIn,
|
||||
bridgedAmount,
|
||||
intent.recipient
|
||||
);
|
||||
}
|
||||
|
||||
receive() external payable {}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
library RouteTypesV2 {
|
||||
enum Provider {
|
||||
Dodo,
|
||||
UniswapV3,
|
||||
Balancer,
|
||||
Curve,
|
||||
OneInch,
|
||||
Partner,
|
||||
DodoV3
|
||||
}
|
||||
|
||||
enum AmountSource {
|
||||
UserInput,
|
||||
PreviousLeg
|
||||
}
|
||||
|
||||
struct RouteLeg {
|
||||
Provider provider;
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
AmountSource amountSource;
|
||||
uint256 minAmountOut;
|
||||
address target;
|
||||
bytes providerData;
|
||||
}
|
||||
|
||||
struct RoutePlan {
|
||||
uint256 chainId;
|
||||
address inputToken;
|
||||
address outputToken;
|
||||
uint256 amountIn;
|
||||
uint256 minAmountOut;
|
||||
address recipient;
|
||||
uint256 deadline;
|
||||
RouteLeg[] legs;
|
||||
}
|
||||
|
||||
struct ExecutionConstraints {
|
||||
uint256 maxSlippageBps;
|
||||
Provider[] allowedProviders;
|
||||
uint256 maxLegs;
|
||||
bytes32 complianceProfile;
|
||||
}
|
||||
|
||||
struct ProviderQuote {
|
||||
Provider provider;
|
||||
address target;
|
||||
uint256 amountOut;
|
||||
uint256 estimatedGas;
|
||||
bool executable;
|
||||
bytes providerData;
|
||||
}
|
||||
|
||||
struct BridgeIntentPlan {
|
||||
RoutePlan sourcePlan;
|
||||
bytes32 bridgeType;
|
||||
bytes bridgeData;
|
||||
RoutePlan destinationPlan;
|
||||
address recipient;
|
||||
uint256 deadline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/IBalancerVault.sol";
|
||||
import "../RouteTypesV2.sol";
|
||||
import "../interfaces/IRouteExecutorAdapter.sol";
|
||||
|
||||
contract BalancerRouteExecutorAdapter is IRouteExecutorAdapter {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
function validate(
|
||||
RouteTypesV2.RouteLeg calldata leg
|
||||
) external pure override returns (bool ok, string memory reason) {
|
||||
if (leg.provider != RouteTypesV2.Provider.Balancer) {
|
||||
return (false, "BalancerRouteExecutorAdapter: invalid provider");
|
||||
}
|
||||
if (leg.target == address(0)) {
|
||||
return (false, "BalancerRouteExecutorAdapter: zero target");
|
||||
}
|
||||
if (leg.providerData.length != 32) {
|
||||
return (false, "BalancerRouteExecutorAdapter: invalid providerData");
|
||||
}
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
function quote(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external view override returns (uint256 amountOut, uint256 gasEstimate) {
|
||||
bytes32 poolId = abi.decode(leg.providerData, (bytes32));
|
||||
(address[] memory tokens, uint256[] memory balances,) = IBalancerVault(leg.target).getPoolTokens(poolId);
|
||||
uint256 inIndex = type(uint256).max;
|
||||
uint256 outIndex = type(uint256).max;
|
||||
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
if (tokens[i] == leg.tokenIn) inIndex = i;
|
||||
if (tokens[i] == leg.tokenOut) outIndex = i;
|
||||
}
|
||||
|
||||
if (inIndex != type(uint256).max && outIndex != type(uint256).max && balances[inIndex] > 0) {
|
||||
uint256 grossOut = (amountIn * balances[outIndex]) / balances[inIndex];
|
||||
amountOut = (grossOut * 9950) / 10000;
|
||||
}
|
||||
|
||||
gasEstimate = 230000;
|
||||
}
|
||||
|
||||
function execute(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external override returns (uint256 amountOut) {
|
||||
bytes32 poolId = abi.decode(leg.providerData, (bytes32));
|
||||
IERC20(leg.tokenIn).forceApprove(leg.target, 0);
|
||||
IERC20(leg.tokenIn).forceApprove(leg.target, amountIn);
|
||||
|
||||
amountOut = IBalancerVault(leg.target).swap(
|
||||
IBalancerVault.SingleSwap({
|
||||
poolId: poolId,
|
||||
kind: IBalancerVault.SwapKind.GIVEN_IN,
|
||||
assetIn: leg.tokenIn,
|
||||
assetOut: leg.tokenOut,
|
||||
amount: amountIn,
|
||||
userData: ""
|
||||
}),
|
||||
IBalancerVault.FundManagement({
|
||||
sender: address(this),
|
||||
fromInternalBalance: false,
|
||||
recipient: payable(msg.sender),
|
||||
toInternalBalance: false
|
||||
}),
|
||||
leg.minAmountOut,
|
||||
block.timestamp + 300
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/ICurvePool.sol";
|
||||
import "../RouteTypesV2.sol";
|
||||
import "../interfaces/IRouteExecutorAdapter.sol";
|
||||
|
||||
contract CurveRouteExecutorAdapter is IRouteExecutorAdapter {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
function validate(
|
||||
RouteTypesV2.RouteLeg calldata leg
|
||||
) external pure override returns (bool ok, string memory reason) {
|
||||
if (leg.provider != RouteTypesV2.Provider.Curve) {
|
||||
return (false, "CurveRouteExecutorAdapter: invalid provider");
|
||||
}
|
||||
if (leg.target == address(0)) {
|
||||
return (false, "CurveRouteExecutorAdapter: zero target");
|
||||
}
|
||||
if (leg.providerData.length == 0) {
|
||||
return (false, "CurveRouteExecutorAdapter: missing providerData");
|
||||
}
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
function quote(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external view override returns (uint256 amountOut, uint256 gasEstimate) {
|
||||
(int128 i, int128 j,) = abi.decode(leg.providerData, (int128, int128, bool));
|
||||
amountOut = ICurvePool(leg.target).get_dy(i, j, amountIn);
|
||||
gasEstimate = 190000;
|
||||
}
|
||||
|
||||
function execute(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external override returns (uint256 amountOut) {
|
||||
(int128 i, int128 j, bool useUnderlying) = abi.decode(leg.providerData, (int128, int128, bool));
|
||||
IERC20(leg.tokenIn).forceApprove(leg.target, 0);
|
||||
IERC20(leg.tokenIn).forceApprove(leg.target, amountIn);
|
||||
|
||||
if (useUnderlying) {
|
||||
amountOut = ICurvePool(leg.target).exchange_underlying(i, j, amountIn, leg.minAmountOut);
|
||||
} else {
|
||||
amountOut = ICurvePool(leg.target).exchange(i, j, amountIn, leg.minAmountOut);
|
||||
}
|
||||
|
||||
IERC20(leg.tokenOut).safeTransfer(msg.sender, amountOut);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../../../liquidity/interfaces/ILiquidityProvider.sol";
|
||||
import "../RouteTypesV2.sol";
|
||||
import "../interfaces/IRouteExecutorAdapter.sol";
|
||||
|
||||
contract DodoRouteExecutorAdapter is IRouteExecutorAdapter {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
function validate(
|
||||
RouteTypesV2.RouteLeg calldata leg
|
||||
) external view override returns (bool ok, string memory reason) {
|
||||
if (leg.provider != RouteTypesV2.Provider.Dodo) {
|
||||
return (false, "DodoRouteExecutorAdapter: invalid provider");
|
||||
}
|
||||
if (leg.target == address(0)) {
|
||||
return (false, "DodoRouteExecutorAdapter: zero target");
|
||||
}
|
||||
if (leg.tokenIn == address(0) || leg.tokenOut == address(0)) {
|
||||
return (false, "DodoRouteExecutorAdapter: zero token");
|
||||
}
|
||||
if (leg.providerData.length != 32) {
|
||||
return (false, "DodoRouteExecutorAdapter: invalid providerData");
|
||||
}
|
||||
|
||||
address poolAddress = abi.decode(leg.providerData, (address));
|
||||
if (poolAddress == address(0)) {
|
||||
return (false, "DodoRouteExecutorAdapter: zero pool");
|
||||
}
|
||||
if (!ILiquidityProvider(leg.target).supportsTokenPair(leg.tokenIn, leg.tokenOut)) {
|
||||
return (false, "DodoRouteExecutorAdapter: unsupported pair");
|
||||
}
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
function quote(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external view override returns (uint256 amountOut, uint256 gasEstimate) {
|
||||
(amountOut,) = ILiquidityProvider(leg.target).getQuote(leg.tokenIn, leg.tokenOut, amountIn);
|
||||
gasEstimate = ILiquidityProvider(leg.target).estimateGas(leg.tokenIn, leg.tokenOut, amountIn);
|
||||
}
|
||||
|
||||
function execute(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external override returns (uint256 amountOut) {
|
||||
IERC20(leg.tokenIn).forceApprove(leg.target, 0);
|
||||
IERC20(leg.tokenIn).forceApprove(leg.target, amountIn);
|
||||
amountOut = ILiquidityProvider(leg.target).executeSwap(
|
||||
leg.tokenIn,
|
||||
leg.tokenOut,
|
||||
amountIn,
|
||||
leg.minAmountOut
|
||||
);
|
||||
IERC20(leg.tokenOut).safeTransfer(msg.sender, amountOut);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../RouteTypesV2.sol";
|
||||
import "../interfaces/IRouteExecutorAdapter.sol";
|
||||
|
||||
interface ID3ProxyView {
|
||||
function _DODO_APPROVE_PROXY_() external view returns (address);
|
||||
|
||||
function sellTokens(
|
||||
address pool,
|
||||
address to,
|
||||
address fromToken,
|
||||
address toToken,
|
||||
uint256 fromAmount,
|
||||
uint256 minReceiveAmount,
|
||||
bytes calldata data,
|
||||
uint256 deadLine
|
||||
) external payable returns (uint256 receiveToAmount);
|
||||
}
|
||||
|
||||
interface IDODOApproveProxyView {
|
||||
function _DODO_APPROVE_() external view returns (address);
|
||||
}
|
||||
|
||||
interface ID3MMQuoter {
|
||||
function querySellTokens(
|
||||
address fromToken,
|
||||
address toToken,
|
||||
uint256 fromAmount
|
||||
) external view returns (uint256 payFromAmount, uint256 receiveToAmount, uint256 vusdAmount, uint256 swapFee, uint256 mtFee);
|
||||
}
|
||||
|
||||
contract DodoV3RouteExecutorAdapter is IRouteExecutorAdapter {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
uint256 internal constant DEFAULT_GAS_ESTIMATE = 330000;
|
||||
|
||||
function validate(
|
||||
RouteTypesV2.RouteLeg calldata leg
|
||||
) external view override returns (bool ok, string memory reason) {
|
||||
if (leg.provider != RouteTypesV2.Provider.DodoV3) {
|
||||
return (false, "DodoV3RouteExecutorAdapter: invalid provider");
|
||||
}
|
||||
if (leg.target == address(0)) {
|
||||
return (false, "DodoV3RouteExecutorAdapter: zero target");
|
||||
}
|
||||
if (leg.tokenIn == address(0) || leg.tokenOut == address(0)) {
|
||||
return (false, "DodoV3RouteExecutorAdapter: zero token");
|
||||
}
|
||||
if (leg.providerData.length != 32) {
|
||||
return (false, "DodoV3RouteExecutorAdapter: invalid providerData");
|
||||
}
|
||||
|
||||
address poolAddress = abi.decode(leg.providerData, (address));
|
||||
if (poolAddress == address(0)) {
|
||||
return (false, "DodoV3RouteExecutorAdapter: zero pool");
|
||||
}
|
||||
|
||||
address approveProxy = ID3ProxyView(leg.target)._DODO_APPROVE_PROXY_();
|
||||
if (approveProxy == address(0)) {
|
||||
return (false, "DodoV3RouteExecutorAdapter: zero approve proxy");
|
||||
}
|
||||
address approve = IDODOApproveProxyView(approveProxy)._DODO_APPROVE_();
|
||||
if (approve == address(0)) {
|
||||
return (false, "DodoV3RouteExecutorAdapter: zero approve");
|
||||
}
|
||||
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
function quote(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external view override returns (uint256 amountOut, uint256 gasEstimate) {
|
||||
address poolAddress = abi.decode(leg.providerData, (address));
|
||||
(, amountOut,,,) = ID3MMQuoter(poolAddress).querySellTokens(leg.tokenIn, leg.tokenOut, amountIn);
|
||||
gasEstimate = DEFAULT_GAS_ESTIMATE;
|
||||
}
|
||||
|
||||
function execute(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external override returns (uint256 amountOut) {
|
||||
address poolAddress = abi.decode(leg.providerData, (address));
|
||||
address approveProxy = ID3ProxyView(leg.target)._DODO_APPROVE_PROXY_();
|
||||
address approve = IDODOApproveProxyView(approveProxy)._DODO_APPROVE_();
|
||||
|
||||
IERC20(leg.tokenIn).forceApprove(approve, 0);
|
||||
IERC20(leg.tokenIn).forceApprove(approve, amountIn);
|
||||
|
||||
amountOut = ID3ProxyView(leg.target).sellTokens(
|
||||
poolAddress,
|
||||
address(this),
|
||||
leg.tokenIn,
|
||||
leg.tokenOut,
|
||||
amountIn,
|
||||
leg.minAmountOut,
|
||||
bytes(""),
|
||||
block.timestamp + 300
|
||||
);
|
||||
|
||||
IERC20(leg.tokenIn).forceApprove(approve, 0);
|
||||
IERC20(leg.tokenOut).safeTransfer(msg.sender, amountOut);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/IAggregationRouter.sol";
|
||||
import "../RouteTypesV2.sol";
|
||||
import "../interfaces/IRouteExecutorAdapter.sol";
|
||||
|
||||
contract OneInchRouteExecutorAdapter is IRouteExecutorAdapter {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
function validate(
|
||||
RouteTypesV2.RouteLeg calldata leg
|
||||
) external pure override returns (bool ok, string memory reason) {
|
||||
if (leg.provider != RouteTypesV2.Provider.OneInch) {
|
||||
return (false, "OneInchRouteExecutorAdapter: invalid provider");
|
||||
}
|
||||
if (leg.target == address(0)) {
|
||||
return (false, "OneInchRouteExecutorAdapter: zero target");
|
||||
}
|
||||
if (leg.providerData.length == 0) {
|
||||
return (false, "OneInchRouteExecutorAdapter: missing providerData");
|
||||
}
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
function quote(
|
||||
RouteTypesV2.RouteLeg calldata,
|
||||
uint256
|
||||
) external pure override returns (uint256 amountOut, uint256 gasEstimate) {
|
||||
return (0, 320000);
|
||||
}
|
||||
|
||||
function execute(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external override returns (uint256 amountOut) {
|
||||
(address executor, address allowanceTarget, bytes memory data) =
|
||||
abi.decode(leg.providerData, (address, address, bytes));
|
||||
address spender = allowanceTarget == address(0) ? leg.target : allowanceTarget;
|
||||
|
||||
IERC20(leg.tokenIn).forceApprove(spender, 0);
|
||||
IERC20(leg.tokenIn).forceApprove(spender, amountIn);
|
||||
|
||||
IAggregationRouter.SwapDescription memory desc = IAggregationRouter.SwapDescription({
|
||||
srcToken: leg.tokenIn,
|
||||
dstToken: leg.tokenOut,
|
||||
srcReceiver: address(this),
|
||||
dstReceiver: msg.sender,
|
||||
amount: amountIn,
|
||||
minReturnAmount: leg.minAmountOut,
|
||||
flags: 0,
|
||||
permit: ""
|
||||
});
|
||||
|
||||
(amountOut,) = IAggregationRouter(leg.target).swap(executor, desc, "", data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/ISwapRouter.sol";
|
||||
import "../RouteTypesV2.sol";
|
||||
import "../interfaces/IRouteExecutorAdapter.sol";
|
||||
|
||||
contract UniswapV3RouteExecutorAdapter is IRouteExecutorAdapter {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
function validate(
|
||||
RouteTypesV2.RouteLeg calldata leg
|
||||
) external pure override returns (bool ok, string memory reason) {
|
||||
if (leg.provider != RouteTypesV2.Provider.UniswapV3) {
|
||||
return (false, "UniswapV3RouteExecutorAdapter: invalid provider");
|
||||
}
|
||||
if (leg.target == address(0)) {
|
||||
return (false, "UniswapV3RouteExecutorAdapter: zero target");
|
||||
}
|
||||
if (leg.providerData.length == 0) {
|
||||
return (false, "UniswapV3RouteExecutorAdapter: missing providerData");
|
||||
}
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
function quote(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external view override returns (uint256 amountOut, uint256 gasEstimate) {
|
||||
(bytes memory path, uint24 fee, address quoter, bool usePath) =
|
||||
abi.decode(leg.providerData, (bytes, uint24, address, bool));
|
||||
|
||||
if (quoter != address(0)) {
|
||||
bytes memory data;
|
||||
bool ok;
|
||||
if (usePath) {
|
||||
(ok, data) = quoter.staticcall(
|
||||
abi.encodeWithSignature("quoteExactInput(bytes,uint256)", path, amountIn)
|
||||
);
|
||||
} else {
|
||||
(ok, data) = quoter.staticcall(
|
||||
abi.encodeWithSignature(
|
||||
"quoteExactInputSingle(address,address,uint24,uint256,uint160)",
|
||||
leg.tokenIn,
|
||||
leg.tokenOut,
|
||||
fee,
|
||||
amountIn,
|
||||
uint160(0)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (ok && data.length >= 32) {
|
||||
amountOut = abi.decode(data, (uint256));
|
||||
}
|
||||
}
|
||||
|
||||
gasEstimate = usePath ? 220000 : 175000;
|
||||
}
|
||||
|
||||
function execute(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external override returns (uint256 amountOut) {
|
||||
(bytes memory path, uint24 fee,, bool usePath) =
|
||||
abi.decode(leg.providerData, (bytes, uint24, address, bool));
|
||||
|
||||
IERC20(leg.tokenIn).forceApprove(leg.target, 0);
|
||||
IERC20(leg.tokenIn).forceApprove(leg.target, amountIn);
|
||||
|
||||
if (usePath) {
|
||||
amountOut = ISwapRouter(leg.target).exactInput(
|
||||
ISwapRouter.ExactInputParams({
|
||||
path: path,
|
||||
recipient: msg.sender,
|
||||
deadline: block.timestamp + 300,
|
||||
amountIn: amountIn,
|
||||
amountOutMinimum: leg.minAmountOut
|
||||
})
|
||||
);
|
||||
} else {
|
||||
amountOut = ISwapRouter(leg.target).exactInputSingle(
|
||||
ISwapRouter.ExactInputSingleParams({
|
||||
tokenIn: leg.tokenIn,
|
||||
tokenOut: leg.tokenOut,
|
||||
fee: fee,
|
||||
recipient: msg.sender,
|
||||
deadline: block.timestamp + 300,
|
||||
amountIn: amountIn,
|
||||
amountOutMinimum: leg.minAmountOut,
|
||||
sqrtPriceLimitX96: 0
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ import "./ICommodityPegManager.sol";
|
||||
interface IDODOPMMPoolStabilizer {
|
||||
function _BASE_TOKEN_() external view returns (address);
|
||||
function _QUOTE_TOKEN_() external view returns (address);
|
||||
function sellBase(uint256 amount) external returns (uint256);
|
||||
function sellQuote(uint256 amount) external returns (uint256);
|
||||
function sellBase(address to) external returns (uint256);
|
||||
function sellQuote(address to) external returns (uint256);
|
||||
function getMidPrice() external view returns (uint256);
|
||||
}
|
||||
|
||||
@@ -190,8 +190,8 @@ contract Stabilizer is AccessControl, ReentrancyGuard {
|
||||
if (IERC20(tokenIn).balanceOf(address(this)) < tradeSize) revert InsufficientBalance();
|
||||
IERC20(tokenIn).safeTransfer(pool, tradeSize);
|
||||
amountOut = tokenIn == base
|
||||
? IDODOPMMPoolStabilizer(pool).sellBase(tradeSize)
|
||||
: IDODOPMMPoolStabilizer(pool).sellQuote(tradeSize);
|
||||
? IDODOPMMPoolStabilizer(pool).sellBase(address(this))
|
||||
: IDODOPMMPoolStabilizer(pool).sellQuote(address(this));
|
||||
if (amountOut < minAmountOut) revert SlippageExceeded();
|
||||
|
||||
emit PrivateSwapExecuted(tokenIn, tokenOut, tradeSize, amountOut);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
interface IBridgeIntentExecutor {
|
||||
function validateBridge(
|
||||
bytes32 bridgeType,
|
||||
bytes calldata bridgeData,
|
||||
address token,
|
||||
uint256 amount,
|
||||
address recipient
|
||||
) external view returns (bool ok, string memory reason);
|
||||
|
||||
function executeBridge(
|
||||
bytes32 bridgeType,
|
||||
bytes calldata bridgeData,
|
||||
address token,
|
||||
uint256 amount,
|
||||
address recipient
|
||||
) external payable returns (bytes32 referenceId);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "../RouteTypesV2.sol";
|
||||
import "../LiquidityPoolETH.sol";
|
||||
|
||||
interface IEnhancedSwapRouterV2 {
|
||||
function executeRoute(
|
||||
RouteTypesV2.RoutePlan calldata plan
|
||||
) external payable returns (uint256 amountOut);
|
||||
|
||||
function quoteConfiguredProviders(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountIn
|
||||
) external view returns (RouteTypesV2.ProviderQuote[] memory quotes);
|
||||
|
||||
function swapTokenToToken(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountIn,
|
||||
uint256 amountOutMin
|
||||
) external returns (uint256 amountOut);
|
||||
|
||||
function swapToStablecoin(
|
||||
LiquidityPoolETH.AssetType inputAsset,
|
||||
address stablecoinToken,
|
||||
uint256 amountIn,
|
||||
uint256 amountOutMin
|
||||
) external payable returns (uint256 amountOut);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "../RouteTypesV2.sol";
|
||||
|
||||
interface IRouteExecutorAdapter {
|
||||
function validate(RouteTypesV2.RouteLeg calldata leg) external view returns (bool ok, string memory reason);
|
||||
|
||||
function quote(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external view returns (uint256 amountOut, uint256 gasEstimate);
|
||||
|
||||
function execute(
|
||||
RouteTypesV2.RouteLeg calldata leg,
|
||||
uint256 amountIn
|
||||
) external returns (uint256 amountOut);
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/IAggregationRouter.sol";
|
||||
import "../interfaces/IBalancerVault.sol";
|
||||
import "../interfaces/ICurvePool.sol";
|
||||
import "../interfaces/ISwapRouter.sol";
|
||||
|
||||
library Chain138PilotVenueMath {
|
||||
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
|
||||
require(tokenA != address(0) && tokenB != address(0) && tokenA != tokenB, "Chain138PilotVenueMath: invalid tokens");
|
||||
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
|
||||
}
|
||||
|
||||
function pairKey(address tokenA, address tokenB, uint24 fee) internal pure returns (bytes32) {
|
||||
(address token0, address token1) = sortTokens(tokenA, tokenB);
|
||||
return keccak256(abi.encodePacked(token0, token1, fee));
|
||||
}
|
||||
|
||||
function pairKey(address tokenA, address tokenB) internal pure returns (bytes32) {
|
||||
(address token0, address token1) = sortTokens(tokenA, tokenB);
|
||||
return keccak256(abi.encodePacked(token0, token1));
|
||||
}
|
||||
|
||||
function constantProductQuote(
|
||||
uint256 reserveIn,
|
||||
uint256 reserveOut,
|
||||
uint256 amountIn,
|
||||
uint256 feeBps
|
||||
) internal pure returns (uint256 amountOut) {
|
||||
if (reserveIn == 0 || reserveOut == 0 || amountIn == 0 || feeBps >= 10_000) {
|
||||
return 0;
|
||||
}
|
||||
uint256 amountInWithFee = amountIn * (10_000 - feeBps);
|
||||
return (reserveOut * amountInWithFee) / (reserveIn * 10_000 + amountInWithFee);
|
||||
}
|
||||
}
|
||||
|
||||
contract Chain138PilotOwned {
|
||||
address public owner;
|
||||
|
||||
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner, "Chain138PilotOwned: not owner");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
owner = msg.sender;
|
||||
emit OwnershipTransferred(address(0), msg.sender);
|
||||
}
|
||||
|
||||
function transferOwnership(address newOwner) external onlyOwner {
|
||||
require(newOwner != address(0), "Chain138PilotOwned: zero owner");
|
||||
emit OwnershipTransferred(owner, newOwner);
|
||||
owner = newOwner;
|
||||
}
|
||||
}
|
||||
|
||||
contract Chain138PilotUniswapV3Router is Chain138PilotOwned, ISwapRouter {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
struct Pool {
|
||||
address token0;
|
||||
address token1;
|
||||
uint24 fee;
|
||||
uint256 reserve0;
|
||||
uint256 reserve1;
|
||||
bool exists;
|
||||
}
|
||||
|
||||
mapping(bytes32 => Pool) private pools;
|
||||
|
||||
event PairSeeded(bytes32 indexed poolKey, address indexed token0, address indexed token1, uint24 fee, uint256 amount0, uint256 amount1);
|
||||
event PairFunded(bytes32 indexed poolKey, uint256 amount0, uint256 amount1);
|
||||
event Swapped(bytes32 indexed poolKey, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut);
|
||||
|
||||
function seedPair(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint24 fee,
|
||||
uint256 amountA,
|
||||
uint256 amountB
|
||||
) external onlyOwner returns (bytes32 poolKey) {
|
||||
require(amountA > 0 && amountB > 0, "Chain138PilotUniswapV3Router: zero seed");
|
||||
poolKey = Chain138PilotVenueMath.pairKey(tokenA, tokenB, fee);
|
||||
Pool storage pool = pools[poolKey];
|
||||
(address token0, address token1) = Chain138PilotVenueMath.sortTokens(tokenA, tokenB);
|
||||
|
||||
if (!pool.exists) {
|
||||
pool.token0 = token0;
|
||||
pool.token1 = token1;
|
||||
pool.fee = fee;
|
||||
pool.exists = true;
|
||||
emit PairSeeded(
|
||||
poolKey,
|
||||
token0,
|
||||
token1,
|
||||
fee,
|
||||
tokenA == token0 ? amountA : amountB,
|
||||
tokenA == token0 ? amountB : amountA
|
||||
);
|
||||
}
|
||||
|
||||
_fundPool(poolKey, tokenA, tokenB, amountA, amountB);
|
||||
}
|
||||
|
||||
function fundPair(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint24 fee,
|
||||
uint256 amountA,
|
||||
uint256 amountB
|
||||
) external onlyOwner {
|
||||
bytes32 poolKey = Chain138PilotVenueMath.pairKey(tokenA, tokenB, fee);
|
||||
require(pools[poolKey].exists, "Chain138PilotUniswapV3Router: pair missing");
|
||||
_fundPool(poolKey, tokenA, tokenB, amountA, amountB);
|
||||
}
|
||||
|
||||
function getPairReserves(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint24 fee
|
||||
) external view returns (uint256 reserveIn, uint256 reserveOut, bool exists) {
|
||||
bytes32 poolKey = Chain138PilotVenueMath.pairKey(tokenA, tokenB, fee);
|
||||
Pool storage pool = pools[poolKey];
|
||||
if (!pool.exists) {
|
||||
return (0, 0, false);
|
||||
}
|
||||
if (tokenA == pool.token0) {
|
||||
return (pool.reserve0, pool.reserve1, true);
|
||||
}
|
||||
return (pool.reserve1, pool.reserve0, true);
|
||||
}
|
||||
|
||||
function quoteExactInputSingle(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
uint256 amountIn,
|
||||
uint160
|
||||
) external view returns (uint256 amountOut) {
|
||||
(uint256 reserveIn, uint256 reserveOut, bool exists) = this.getPairReserves(tokenIn, tokenOut, fee);
|
||||
require(exists, "Chain138PilotUniswapV3Router: pair missing");
|
||||
return Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, amountIn, fee / 100);
|
||||
}
|
||||
|
||||
function quoteExactInput(bytes calldata path, uint256 amountIn) external view returns (uint256 amountOut) {
|
||||
(address tokenIn, address tokenOut, uint24 fee) = _decodeSingleHopPath(path);
|
||||
return this.quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, 0);
|
||||
}
|
||||
|
||||
function exactInputSingle(
|
||||
ExactInputSingleParams calldata params
|
||||
) external payable override returns (uint256 amountOut) {
|
||||
amountOut = _swap(
|
||||
params.tokenIn,
|
||||
params.tokenOut,
|
||||
params.fee,
|
||||
params.amountIn,
|
||||
params.amountOutMinimum,
|
||||
params.recipient
|
||||
);
|
||||
}
|
||||
|
||||
function exactInput(
|
||||
ExactInputParams calldata params
|
||||
) external payable override returns (uint256 amountOut) {
|
||||
(address tokenIn, address tokenOut, uint24 fee) = _decodeSingleHopPath(params.path);
|
||||
amountOut = _swap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
fee,
|
||||
params.amountIn,
|
||||
params.amountOutMinimum,
|
||||
params.recipient
|
||||
);
|
||||
}
|
||||
|
||||
function _fundPool(
|
||||
bytes32 poolKey,
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint256 amountA,
|
||||
uint256 amountB
|
||||
) internal {
|
||||
Pool storage pool = pools[poolKey];
|
||||
IERC20(tokenA).safeTransferFrom(msg.sender, address(this), amountA);
|
||||
IERC20(tokenB).safeTransferFrom(msg.sender, address(this), amountB);
|
||||
|
||||
if (tokenA == pool.token0) {
|
||||
pool.reserve0 += amountA;
|
||||
pool.reserve1 += amountB;
|
||||
emit PairFunded(poolKey, amountA, amountB);
|
||||
} else {
|
||||
pool.reserve0 += amountB;
|
||||
pool.reserve1 += amountA;
|
||||
emit PairFunded(poolKey, amountB, amountA);
|
||||
}
|
||||
}
|
||||
|
||||
function _swap(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
uint256 amountIn,
|
||||
uint256 minAmountOut,
|
||||
address recipient
|
||||
) internal returns (uint256 amountOut) {
|
||||
bytes32 poolKey = Chain138PilotVenueMath.pairKey(tokenIn, tokenOut, fee);
|
||||
Pool storage pool = pools[poolKey];
|
||||
require(pool.exists, "Chain138PilotUniswapV3Router: pair missing");
|
||||
|
||||
bool inputIsToken0 = tokenIn == pool.token0;
|
||||
uint256 reserveIn = inputIsToken0 ? pool.reserve0 : pool.reserve1;
|
||||
uint256 reserveOut = inputIsToken0 ? pool.reserve1 : pool.reserve0;
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
amountOut = Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, amountIn, fee / 100);
|
||||
require(amountOut >= minAmountOut, "Chain138PilotUniswapV3Router: insufficient output");
|
||||
|
||||
if (inputIsToken0) {
|
||||
pool.reserve0 += amountIn;
|
||||
pool.reserve1 -= amountOut;
|
||||
} else {
|
||||
pool.reserve1 += amountIn;
|
||||
pool.reserve0 -= amountOut;
|
||||
}
|
||||
|
||||
IERC20(tokenOut).safeTransfer(recipient, amountOut);
|
||||
emit Swapped(poolKey, tokenIn, tokenOut, amountIn, amountOut);
|
||||
}
|
||||
|
||||
function _decodeSingleHopPath(bytes memory path) internal pure returns (address tokenIn, address tokenOut, uint24 fee) {
|
||||
require(path.length == 43, "Chain138PilotUniswapV3Router: single hop only");
|
||||
assembly {
|
||||
tokenIn := shr(96, mload(add(path, 32)))
|
||||
fee := shr(232, mload(add(path, 52)))
|
||||
tokenOut := shr(96, mload(add(path, 55)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contract Chain138PilotBalancerVault is Chain138PilotOwned, IBalancerVault {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
struct Pool {
|
||||
address token0;
|
||||
address token1;
|
||||
uint256 reserve0;
|
||||
uint256 reserve1;
|
||||
uint256 feeBps;
|
||||
bool exists;
|
||||
}
|
||||
|
||||
mapping(bytes32 => Pool) private pools;
|
||||
|
||||
event PoolSeeded(bytes32 indexed poolId, address indexed token0, address indexed token1, uint256 amount0, uint256 amount1, uint256 feeBps);
|
||||
event PoolFunded(bytes32 indexed poolId, uint256 amount0, uint256 amount1);
|
||||
event Swapped(bytes32 indexed poolId, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut);
|
||||
|
||||
function seedPool(
|
||||
bytes32 poolId,
|
||||
address token0,
|
||||
address token1,
|
||||
uint256 amount0,
|
||||
uint256 amount1,
|
||||
uint256 feeBps
|
||||
) external onlyOwner {
|
||||
require(poolId != bytes32(0), "Chain138PilotBalancerVault: zero poolId");
|
||||
require(amount0 > 0 && amount1 > 0, "Chain138PilotBalancerVault: zero seed");
|
||||
Pool storage pool = pools[poolId];
|
||||
if (!pool.exists) {
|
||||
pool.token0 = token0;
|
||||
pool.token1 = token1;
|
||||
pool.feeBps = feeBps;
|
||||
pool.exists = true;
|
||||
emit PoolSeeded(poolId, token0, token1, amount0, amount1, feeBps);
|
||||
}
|
||||
_fund(poolId, amount0, amount1);
|
||||
}
|
||||
|
||||
function fundPool(bytes32 poolId, uint256 amount0, uint256 amount1) external onlyOwner {
|
||||
require(pools[poolId].exists, "Chain138PilotBalancerVault: pool missing");
|
||||
_fund(poolId, amount0, amount1);
|
||||
}
|
||||
|
||||
function swap(
|
||||
SingleSwap memory singleSwap,
|
||||
FundManagement memory funds,
|
||||
uint256 limit,
|
||||
uint256 deadline
|
||||
) external payable override returns (uint256 amountCalculated) {
|
||||
require(deadline >= block.timestamp, "Chain138PilotBalancerVault: expired");
|
||||
Pool storage pool = pools[singleSwap.poolId];
|
||||
require(pool.exists, "Chain138PilotBalancerVault: pool missing");
|
||||
require(singleSwap.kind == SwapKind.GIVEN_IN, "Chain138PilotBalancerVault: GIVEN_IN only");
|
||||
|
||||
bool inputIsToken0;
|
||||
if (singleSwap.assetIn == pool.token0 && singleSwap.assetOut == pool.token1) {
|
||||
inputIsToken0 = true;
|
||||
} else if (singleSwap.assetIn == pool.token1 && singleSwap.assetOut == pool.token0) {
|
||||
inputIsToken0 = false;
|
||||
} else {
|
||||
revert("Chain138PilotBalancerVault: bad assets");
|
||||
}
|
||||
|
||||
uint256 reserveIn = inputIsToken0 ? pool.reserve0 : pool.reserve1;
|
||||
uint256 reserveOut = inputIsToken0 ? pool.reserve1 : pool.reserve0;
|
||||
IERC20(singleSwap.assetIn).safeTransferFrom(funds.sender, address(this), singleSwap.amount);
|
||||
amountCalculated = Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, singleSwap.amount, pool.feeBps);
|
||||
require(amountCalculated >= limit, "Chain138PilotBalancerVault: below limit");
|
||||
|
||||
if (inputIsToken0) {
|
||||
pool.reserve0 += singleSwap.amount;
|
||||
pool.reserve1 -= amountCalculated;
|
||||
} else {
|
||||
pool.reserve1 += singleSwap.amount;
|
||||
pool.reserve0 -= amountCalculated;
|
||||
}
|
||||
|
||||
IERC20(singleSwap.assetOut).safeTransfer(funds.recipient, amountCalculated);
|
||||
emit Swapped(singleSwap.poolId, singleSwap.assetIn, singleSwap.assetOut, singleSwap.amount, amountCalculated);
|
||||
}
|
||||
|
||||
function getPool(bytes32 poolId) external view override returns (address poolAddress, uint8 specialization) {
|
||||
require(pools[poolId].exists, "Chain138PilotBalancerVault: pool missing");
|
||||
return (address(this), 0);
|
||||
}
|
||||
|
||||
function queryBatchSwap(
|
||||
SwapKind kind,
|
||||
SingleSwap[] memory swaps,
|
||||
address[] memory
|
||||
) external view override returns (int256[] memory assetDeltas) {
|
||||
require(kind == SwapKind.GIVEN_IN, "Chain138PilotBalancerVault: GIVEN_IN only");
|
||||
assetDeltas = new int256[](swaps.length > 0 ? 2 : 0);
|
||||
if (swaps.length == 0) {
|
||||
return assetDeltas;
|
||||
}
|
||||
Pool storage pool = pools[swaps[0].poolId];
|
||||
bool inputIsToken0 = swaps[0].assetIn == pool.token0;
|
||||
uint256 reserveIn = inputIsToken0 ? pool.reserve0 : pool.reserve1;
|
||||
uint256 reserveOut = inputIsToken0 ? pool.reserve1 : pool.reserve0;
|
||||
uint256 amountOut = Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, swaps[0].amount, pool.feeBps);
|
||||
assetDeltas[0] = int256(swaps[0].amount);
|
||||
assetDeltas[1] = -int256(amountOut);
|
||||
}
|
||||
|
||||
function getPoolTokens(
|
||||
bytes32 poolId
|
||||
) external view override returns (address[] memory tokens, uint256[] memory balances, uint256 lastChangeBlock) {
|
||||
Pool storage pool = pools[poolId];
|
||||
require(pool.exists, "Chain138PilotBalancerVault: pool missing");
|
||||
tokens = new address[](2);
|
||||
balances = new uint256[](2);
|
||||
tokens[0] = pool.token0;
|
||||
tokens[1] = pool.token1;
|
||||
balances[0] = pool.reserve0;
|
||||
balances[1] = pool.reserve1;
|
||||
return (tokens, balances, block.number);
|
||||
}
|
||||
|
||||
function _fund(bytes32 poolId, uint256 amount0, uint256 amount1) internal {
|
||||
Pool storage pool = pools[poolId];
|
||||
IERC20(pool.token0).safeTransferFrom(msg.sender, address(this), amount0);
|
||||
IERC20(pool.token1).safeTransferFrom(msg.sender, address(this), amount1);
|
||||
pool.reserve0 += amount0;
|
||||
pool.reserve1 += amount1;
|
||||
emit PoolFunded(poolId, amount0, amount1);
|
||||
}
|
||||
}
|
||||
|
||||
contract Chain138PilotCurve3Pool is Chain138PilotOwned, ICurvePool {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address[3] private _coins;
|
||||
uint256[3] private _reserves;
|
||||
uint256 public immutable feeBps;
|
||||
|
||||
event Funded(uint256 amount0, uint256 amount1, uint256 amount2);
|
||||
event Exchanged(int128 indexed i, int128 indexed j, uint256 dx, uint256 dy);
|
||||
|
||||
constructor(address token0, address token1, address token2, uint256 feeBps_) {
|
||||
require(token0 != address(0) && token1 != address(0), "Chain138PilotCurve3Pool: invalid tokens");
|
||||
_coins[0] = token0;
|
||||
_coins[1] = token1;
|
||||
_coins[2] = token2;
|
||||
feeBps = feeBps_;
|
||||
}
|
||||
|
||||
function fund(uint256 amount0, uint256 amount1, uint256 amount2) external onlyOwner {
|
||||
if (amount0 > 0) IERC20(_coins[0]).safeTransferFrom(msg.sender, address(this), amount0);
|
||||
if (amount1 > 0) IERC20(_coins[1]).safeTransferFrom(msg.sender, address(this), amount1);
|
||||
if (_coins[2] != address(0) && amount2 > 0) IERC20(_coins[2]).safeTransferFrom(msg.sender, address(this), amount2);
|
||||
_reserves[0] += amount0;
|
||||
_reserves[1] += amount1;
|
||||
_reserves[2] += amount2;
|
||||
emit Funded(amount0, amount1, amount2);
|
||||
}
|
||||
|
||||
function reserves(uint256 index) external view returns (uint256) {
|
||||
return _reserves[index];
|
||||
}
|
||||
|
||||
function exchange(
|
||||
int128 i,
|
||||
int128 j,
|
||||
uint256 dx,
|
||||
uint256 min_dy
|
||||
) external payable override returns (uint256 dy) {
|
||||
dy = _exchange(i, j, dx, min_dy, false);
|
||||
}
|
||||
|
||||
function exchange_underlying(
|
||||
int128 i,
|
||||
int128 j,
|
||||
uint256 dx,
|
||||
uint256 min_dy
|
||||
) external payable override returns (uint256 dy) {
|
||||
dy = _exchange(i, j, dx, min_dy, true);
|
||||
}
|
||||
|
||||
function get_dy(
|
||||
int128 i,
|
||||
int128 j,
|
||||
uint256 dx
|
||||
) public view override returns (uint256 dy) {
|
||||
uint256 ui = uint256(uint128(i));
|
||||
uint256 uj = uint256(uint128(j));
|
||||
require(ui < 3 && uj < 3 && ui != uj, "Chain138PilotCurve3Pool: bad indexes");
|
||||
require(_coins[ui] != address(0) && _coins[uj] != address(0), "Chain138PilotCurve3Pool: token missing");
|
||||
|
||||
uint256 reserveIn = _reserves[ui];
|
||||
uint256 reserveOut = _reserves[uj];
|
||||
require(reserveIn > 0 && reserveOut > 0, "Chain138PilotCurve3Pool: empty reserves");
|
||||
|
||||
uint256 grossOut = (dx * reserveOut) / reserveIn;
|
||||
dy = (grossOut * (10_000 - feeBps)) / 10_000;
|
||||
if (dy > reserveOut) {
|
||||
dy = reserveOut;
|
||||
}
|
||||
}
|
||||
|
||||
function coins(uint256 i) external view override returns (address) {
|
||||
return _coins[i];
|
||||
}
|
||||
|
||||
function _exchange(
|
||||
int128 i,
|
||||
int128 j,
|
||||
uint256 dx,
|
||||
uint256 min_dy,
|
||||
bool
|
||||
) internal returns (uint256 dy) {
|
||||
uint256 ui = uint256(uint128(i));
|
||||
uint256 uj = uint256(uint128(j));
|
||||
dy = get_dy(i, j, dx);
|
||||
require(dy >= min_dy, "Chain138PilotCurve3Pool: insufficient output");
|
||||
|
||||
IERC20(_coins[ui]).safeTransferFrom(msg.sender, address(this), dx);
|
||||
_reserves[ui] += dx;
|
||||
_reserves[uj] -= dy;
|
||||
IERC20(_coins[uj]).safeTransfer(msg.sender, dy);
|
||||
emit Exchanged(i, j, dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
contract Chain138PilotOneInchAggregationRouter is Chain138PilotOwned, IAggregationRouter {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
struct Route {
|
||||
address token0;
|
||||
address token1;
|
||||
uint256 reserve0;
|
||||
uint256 reserve1;
|
||||
uint256 feeBps;
|
||||
bool exists;
|
||||
}
|
||||
|
||||
mapping(bytes32 => Route) private routes;
|
||||
|
||||
event RouteSeeded(address indexed token0, address indexed token1, uint256 amount0, uint256 amount1, uint256 feeBps);
|
||||
event RouteFunded(address indexed token0, address indexed token1, uint256 amount0, uint256 amount1);
|
||||
event Swapped(address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, address indexed recipient);
|
||||
|
||||
function seedRoute(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint256 amountA,
|
||||
uint256 amountB,
|
||||
uint256 feeBps
|
||||
) external onlyOwner {
|
||||
require(amountA > 0 && amountB > 0, "Chain138PilotOneInchAggregationRouter: zero seed");
|
||||
bytes32 key = Chain138PilotVenueMath.pairKey(tokenA, tokenB);
|
||||
Route storage route = routes[key];
|
||||
(address token0, address token1) = Chain138PilotVenueMath.sortTokens(tokenA, tokenB);
|
||||
if (!route.exists) {
|
||||
route.token0 = token0;
|
||||
route.token1 = token1;
|
||||
route.feeBps = feeBps;
|
||||
route.exists = true;
|
||||
emit RouteSeeded(
|
||||
token0,
|
||||
token1,
|
||||
tokenA == token0 ? amountA : amountB,
|
||||
tokenA == token0 ? amountB : amountA,
|
||||
feeBps
|
||||
);
|
||||
}
|
||||
_fundRoute(key, tokenA, tokenB, amountA, amountB);
|
||||
}
|
||||
|
||||
function fundRoute(address tokenA, address tokenB, uint256 amountA, uint256 amountB) external onlyOwner {
|
||||
bytes32 key = Chain138PilotVenueMath.pairKey(tokenA, tokenB);
|
||||
require(routes[key].exists, "Chain138PilotOneInchAggregationRouter: route missing");
|
||||
_fundRoute(key, tokenA, tokenB, amountA, amountB);
|
||||
}
|
||||
|
||||
function getRouteReserves(
|
||||
address tokenA,
|
||||
address tokenB
|
||||
) external view returns (uint256 reserveIn, uint256 reserveOut, bool exists) {
|
||||
bytes32 key = Chain138PilotVenueMath.pairKey(tokenA, tokenB);
|
||||
Route storage route = routes[key];
|
||||
if (!route.exists) {
|
||||
return (0, 0, false);
|
||||
}
|
||||
if (tokenA == route.token0) {
|
||||
return (route.reserve0, route.reserve1, true);
|
||||
}
|
||||
return (route.reserve1, route.reserve0, true);
|
||||
}
|
||||
|
||||
function quote(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256 amountOut) {
|
||||
(uint256 reserveIn, uint256 reserveOut, bool exists) = this.getRouteReserves(tokenIn, tokenOut);
|
||||
require(exists, "Chain138PilotOneInchAggregationRouter: route missing");
|
||||
return Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, amountIn, routes[Chain138PilotVenueMath.pairKey(tokenIn, tokenOut)].feeBps);
|
||||
}
|
||||
|
||||
function swap(
|
||||
address,
|
||||
SwapDescription calldata desc,
|
||||
bytes calldata,
|
||||
bytes calldata
|
||||
) external payable override returns (uint256 returnAmount, uint256 spentAmount) {
|
||||
bytes32 key = Chain138PilotVenueMath.pairKey(desc.srcToken, desc.dstToken);
|
||||
Route storage route = routes[key];
|
||||
require(route.exists, "Chain138PilotOneInchAggregationRouter: route missing");
|
||||
|
||||
bool inputIsToken0 = desc.srcToken == route.token0;
|
||||
uint256 reserveIn = inputIsToken0 ? route.reserve0 : route.reserve1;
|
||||
uint256 reserveOut = inputIsToken0 ? route.reserve1 : route.reserve0;
|
||||
|
||||
IERC20(desc.srcToken).safeTransferFrom(msg.sender, address(this), desc.amount);
|
||||
returnAmount = Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, desc.amount, route.feeBps);
|
||||
require(returnAmount >= desc.minReturnAmount, "Chain138PilotOneInchAggregationRouter: insufficient output");
|
||||
|
||||
if (inputIsToken0) {
|
||||
route.reserve0 += desc.amount;
|
||||
route.reserve1 -= returnAmount;
|
||||
} else {
|
||||
route.reserve1 += desc.amount;
|
||||
route.reserve0 -= returnAmount;
|
||||
}
|
||||
|
||||
IERC20(desc.dstToken).safeTransfer(desc.dstReceiver, returnAmount);
|
||||
emit Swapped(desc.srcToken, desc.dstToken, desc.amount, returnAmount, desc.dstReceiver);
|
||||
return (returnAmount, desc.amount);
|
||||
}
|
||||
|
||||
function _fundRoute(
|
||||
bytes32 key,
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint256 amountA,
|
||||
uint256 amountB
|
||||
) internal {
|
||||
Route storage route = routes[key];
|
||||
IERC20(tokenA).safeTransferFrom(msg.sender, address(this), amountA);
|
||||
IERC20(tokenB).safeTransferFrom(msg.sender, address(this), amountB);
|
||||
|
||||
if (tokenA == route.token0) {
|
||||
route.reserve0 += amountA;
|
||||
route.reserve1 += amountB;
|
||||
emit RouteFunded(route.token0, route.token1, amountA, amountB);
|
||||
} else {
|
||||
route.reserve0 += amountB;
|
||||
route.reserve1 += amountA;
|
||||
emit RouteFunded(route.token0, route.token1, amountB, amountA);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,8 +15,12 @@ import "../reserve/IReserveSystem.sol";
|
||||
interface IDODOPMMPool {
|
||||
function _BASE_TOKEN_() external view returns (address);
|
||||
function _QUOTE_TOKEN_() external view returns (address);
|
||||
function sellBase(uint256 amount) external returns (uint256);
|
||||
function sellQuote(uint256 amount) external returns (uint256);
|
||||
function querySellBase(address trader, uint256 payBaseAmount) external view returns (uint256 receiveQuoteAmount, uint256 mtFee);
|
||||
function querySellQuote(address trader, uint256 payQuoteAmount) external view returns (uint256 receiveBaseAmount, uint256 mtFee);
|
||||
/// @notice Official DODO DVM: excess base in pool vs reserve is sold; `to` receives quote.
|
||||
function sellBase(address to) external returns (uint256 receiveQuoteAmount);
|
||||
/// @notice Official DODO DVM: excess quote in pool vs reserve is sold; `to` receives base.
|
||||
function sellQuote(address to) external returns (uint256 receiveBaseAmount);
|
||||
function buyShares(address to) external returns (uint256 baseShare, uint256 quoteShare, uint256 lpShare);
|
||||
function getVaultReserve() external view returns (uint256 baseReserve, uint256 quoteReserve);
|
||||
function getMidPrice() external view returns (uint256);
|
||||
@@ -54,6 +58,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
|
||||
bytes32 public constant POOL_MANAGER_ROLE = keccak256("POOL_MANAGER_ROLE");
|
||||
bytes32 public constant SWAP_OPERATOR_ROLE = keccak256("SWAP_OPERATOR_ROLE");
|
||||
uint256 private constant POOL_SURFACE_SAMPLE_AMOUNT = 1;
|
||||
|
||||
// DODO contracts
|
||||
address public immutable dodoVendingMachine;
|
||||
@@ -68,6 +73,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
// Pool mappings
|
||||
mapping(address => mapping(address => address)) public pools; // token0 => token1 => pool
|
||||
mapping(address => bool) public isRegisteredPool;
|
||||
mapping(address => bool) public hasStandardPoolSurface;
|
||||
|
||||
// Pool configuration
|
||||
struct PoolConfig {
|
||||
@@ -116,6 +122,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
address indexed quoteToken,
|
||||
address importer
|
||||
);
|
||||
event PoolSurfaceValidated(address indexed pool, bool standardSurface);
|
||||
|
||||
constructor(
|
||||
address admin,
|
||||
@@ -170,6 +177,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
isOpenTWAP // Enable TWAP
|
||||
);
|
||||
|
||||
_requireBasicPoolSurface(pool, compliantUSDT, officialUSDT);
|
||||
_recordPool(pool, compliantUSDT, officialUSDT, lpFeeRate, initialPrice, k, isOpenTWAP);
|
||||
|
||||
emit PoolCreated(pool, compliantUSDT, officialUSDT, msg.sender);
|
||||
@@ -199,6 +207,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
isOpenTWAP
|
||||
);
|
||||
|
||||
_requireBasicPoolSurface(pool, compliantUSDC, officialUSDC);
|
||||
_recordPool(pool, compliantUSDC, officialUSDC, lpFeeRate, initialPrice, k, isOpenTWAP);
|
||||
|
||||
emit PoolCreated(pool, compliantUSDC, officialUSDC, msg.sender);
|
||||
@@ -228,6 +237,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
isOpenTWAP
|
||||
);
|
||||
|
||||
_requireBasicPoolSurface(pool, compliantUSDT, compliantUSDC);
|
||||
_recordPool(pool, compliantUSDT, compliantUSDC, lpFeeRate, initialPrice, k, isOpenTWAP);
|
||||
|
||||
emit PoolCreated(pool, compliantUSDT, compliantUSDC, msg.sender);
|
||||
@@ -264,6 +274,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
isOpenTWAP
|
||||
);
|
||||
|
||||
_requireBasicPoolSurface(pool, baseToken, quoteToken);
|
||||
_recordPool(pool, baseToken, quoteToken, lpFeeRate, initialPrice, k, isOpenTWAP);
|
||||
|
||||
emit PoolCreated(pool, baseToken, quoteToken, msg.sender);
|
||||
@@ -300,6 +311,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
(baseToken, quoteToken) = (quoteToken, baseToken);
|
||||
}
|
||||
|
||||
_requireBasicPoolSurface(pool, baseToken, quoteToken);
|
||||
_recordPool(pool, baseToken, quoteToken, lpFeeRate, initialPrice, k, isOpenTWAP);
|
||||
emit PoolImported(pool, baseToken, quoteToken, msg.sender);
|
||||
}
|
||||
@@ -326,10 +338,22 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
|
||||
// Call buyShares on DODO pool to add liquidity
|
||||
(baseShare, quoteShare, lpShare) = IDODOPMMPool(pool).buyShares(msg.sender);
|
||||
_setStandardPoolSurface(pool, config.baseToken, config.quoteToken);
|
||||
|
||||
emit LiquidityAdded(pool, msg.sender, baseAmount, quoteAmount, lpShare);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Re-run full standard-surface validation for an already-registered pool.
|
||||
* @dev Useful during migrations when a replacement pool has been seeded and should
|
||||
* now qualify as a standard DODO venue for routers and indexers.
|
||||
*/
|
||||
function refreshPoolSurface(address pool) external onlyRole(POOL_MANAGER_ROLE) returns (bool standardSurface) {
|
||||
require(isRegisteredPool[pool], "DODOPMMIntegration: pool not registered");
|
||||
PoolConfig memory config = poolConfigs[pool];
|
||||
standardSurface = _setStandardPoolSurface(pool, config.baseToken, config.quoteToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Swap cUSDT for official USDT via DODO PMM
|
||||
* @param pool Pool address
|
||||
@@ -348,8 +372,8 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
// Transfer cUSDT to pool
|
||||
IERC20(compliantUSDT).safeTransferFrom(msg.sender, pool, amountIn);
|
||||
|
||||
// Execute swap (sell base token)
|
||||
amountOut = IDODOPMMPool(pool).sellBase(amountIn);
|
||||
// Execute swap (sell base token) — DVM sends quote to msg.sender
|
||||
amountOut = IDODOPMMPool(pool).sellBase(msg.sender);
|
||||
|
||||
require(amountOut >= minAmountOut, "DODOPMMIntegration: insufficient output");
|
||||
|
||||
@@ -374,8 +398,8 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
// Transfer USDT to pool
|
||||
IERC20(officialUSDT).safeTransferFrom(msg.sender, pool, amountIn);
|
||||
|
||||
// Execute swap (sell quote token)
|
||||
amountOut = IDODOPMMPool(pool).sellQuote(amountIn);
|
||||
// Execute swap (sell quote token) — DVM sends base to msg.sender
|
||||
amountOut = IDODOPMMPool(pool).sellQuote(msg.sender);
|
||||
|
||||
require(amountOut >= minAmountOut, "DODOPMMIntegration: insufficient output");
|
||||
|
||||
@@ -398,7 +422,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
require(amountIn > 0, "DODOPMMIntegration: zero amount");
|
||||
|
||||
IERC20(compliantUSDC).safeTransferFrom(msg.sender, pool, amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellBase(amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellBase(msg.sender);
|
||||
|
||||
require(amountOut >= minAmountOut, "DODOPMMIntegration: insufficient output");
|
||||
|
||||
@@ -421,7 +445,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
require(amountIn > 0, "DODOPMMIntegration: zero amount");
|
||||
|
||||
IERC20(officialUSDC).safeTransferFrom(msg.sender, pool, amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellQuote(amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellQuote(msg.sender);
|
||||
|
||||
require(amountOut >= minAmountOut, "DODOPMMIntegration: insufficient output");
|
||||
|
||||
@@ -441,9 +465,8 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
require(amountIn > 0, "DODOPMMIntegration: zero amount");
|
||||
|
||||
IERC20(compliantUSDT).safeTransferFrom(msg.sender, pool, amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellBase(amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellBase(msg.sender);
|
||||
require(amountOut >= minAmountOut, "DODOPMMIntegration: insufficient output");
|
||||
IERC20(compliantUSDC).safeTransfer(msg.sender, amountOut);
|
||||
emit SwapExecuted(pool, compliantUSDT, compliantUSDC, amountIn, amountOut, msg.sender);
|
||||
}
|
||||
|
||||
@@ -460,9 +483,8 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
require(amountIn > 0, "DODOPMMIntegration: zero amount");
|
||||
|
||||
IERC20(compliantUSDC).safeTransferFrom(msg.sender, pool, amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellQuote(amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellQuote(msg.sender);
|
||||
require(amountOut >= minAmountOut, "DODOPMMIntegration: insufficient output");
|
||||
IERC20(compliantUSDT).safeTransfer(msg.sender, amountOut);
|
||||
emit SwapExecuted(pool, compliantUSDC, compliantUSDT, amountIn, amountOut, msg.sender);
|
||||
}
|
||||
|
||||
@@ -486,16 +508,15 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
if (tokenIn == config.baseToken) {
|
||||
tokenOut = config.quoteToken;
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, pool, amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellBase(amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellBase(msg.sender);
|
||||
} else if (tokenIn == config.quoteToken) {
|
||||
tokenOut = config.baseToken;
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, pool, amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellQuote(amountIn);
|
||||
amountOut = IDODOPMMPool(pool).sellQuote(msg.sender);
|
||||
} else {
|
||||
revert("DODOPMMIntegration: token not in pool");
|
||||
}
|
||||
require(amountOut >= minAmountOut, "DODOPMMIntegration: insufficient output");
|
||||
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
|
||||
emit SwapExecuted(pool, tokenIn, tokenOut, amountIn, amountOut, msg.sender);
|
||||
}
|
||||
|
||||
@@ -574,6 +595,7 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
pools[config.baseToken][config.quoteToken] = address(0);
|
||||
pools[config.quoteToken][config.baseToken] = address(0);
|
||||
isRegisteredPool[pool] = false;
|
||||
hasStandardPoolSurface[pool] = false;
|
||||
|
||||
delete poolConfigs[pool];
|
||||
|
||||
@@ -605,4 +627,55 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
|
||||
createdAt: block.timestamp
|
||||
});
|
||||
}
|
||||
|
||||
function _requireBasicPoolSurface(
|
||||
address pool,
|
||||
address expectedBaseToken,
|
||||
address expectedQuoteToken
|
||||
) internal view {
|
||||
require(pool != address(0), "DODOPMMIntegration: zero pool");
|
||||
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();
|
||||
IDODOPMMPool(pool)._BASE_RESERVE_();
|
||||
IDODOPMMPool(pool)._QUOTE_RESERVE_();
|
||||
}
|
||||
|
||||
function _setStandardPoolSurface(
|
||||
address pool,
|
||||
address expectedBaseToken,
|
||||
address expectedQuoteToken
|
||||
) internal returns (bool standardSurface) {
|
||||
standardSurface = _hasStandardPoolSurface(pool, expectedBaseToken, expectedQuoteToken);
|
||||
require(standardSurface, "DODOPMMIntegration: pool missing standard DODO surface");
|
||||
hasStandardPoolSurface[pool] = true;
|
||||
emit PoolSurfaceValidated(pool, true);
|
||||
}
|
||||
|
||||
function _hasStandardPoolSurface(
|
||||
address pool,
|
||||
address expectedBaseToken,
|
||||
address expectedQuoteToken
|
||||
) internal view returns (bool) {
|
||||
_requireBasicPoolSurface(pool, expectedBaseToken, expectedQuoteToken);
|
||||
|
||||
(uint256 baseReserve, uint256 quoteReserve) = IDODOPMMPool(pool).getVaultReserve();
|
||||
if (baseReserve == 0 || quoteReserve == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try IDODOPMMPool(pool).querySellBase(address(this), POOL_SURFACE_SAMPLE_AMOUNT) returns (uint256, uint256) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
try IDODOPMMPool(pool).querySellQuote(address(this), POOL_SURFACE_SAMPLE_AMOUNT) returns (uint256, uint256) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
/**
|
||||
* @title MockDVMPool
|
||||
* @notice Minimal mock of a DODO PMM pool so DODOPMMIntegration can deploy and create pools on Chain 138
|
||||
@@ -12,6 +14,8 @@ contract MockDVMPool {
|
||||
uint256 public baseReserve;
|
||||
uint256 public quoteReserve;
|
||||
uint256 public midPrice;
|
||||
uint256 public totalSupply;
|
||||
mapping(address => uint256) private _shares;
|
||||
uint256 public constant _K_ = 0.5e18; // 50% slippage factor (DODO convention)
|
||||
uint256 public constant _LP_FEE_RATE_ = 3; // 0.03% in basis points
|
||||
|
||||
@@ -47,6 +51,10 @@ contract MockDVMPool {
|
||||
return (baseReserve, quoteReserve);
|
||||
}
|
||||
|
||||
function balanceOf(address owner) external view returns (uint256) {
|
||||
return _shares[owner];
|
||||
}
|
||||
|
||||
function getMidPrice() external view returns (uint256) {
|
||||
return midPrice;
|
||||
}
|
||||
@@ -56,15 +64,71 @@ contract MockDVMPool {
|
||||
return midPrice;
|
||||
}
|
||||
|
||||
function sellBase(uint256) external returns (uint256) {
|
||||
return 0;
|
||||
function querySellBase(address, uint256 amount) external view returns (uint256, uint256) {
|
||||
return ((amount * midPrice) / 1e18, 0);
|
||||
}
|
||||
|
||||
function sellQuote(uint256) external returns (uint256) {
|
||||
return 0;
|
||||
function querySellQuote(address, uint256 amount) external view returns (uint256, uint256) {
|
||||
if (midPrice == 0) {
|
||||
return (0, 0);
|
||||
}
|
||||
return ((amount * 1e18) / midPrice, 0);
|
||||
}
|
||||
|
||||
function buyShares(address) external returns (uint256, uint256, uint256) {
|
||||
return (0, 0, 0);
|
||||
function sellBase(address to) external returns (uint256 receiveQuoteAmount) {
|
||||
uint256 baseBal = IERC20(baseToken).balanceOf(address(this));
|
||||
require(baseBal >= baseReserve, "MockDVMPool: base");
|
||||
uint256 baseInput = baseBal - baseReserve;
|
||||
if (baseInput == 0) return 0;
|
||||
(receiveQuoteAmount,) = this.querySellBase(address(0), baseInput);
|
||||
require(IERC20(quoteToken).transfer(to, receiveQuoteAmount), "MockDVMPool: q");
|
||||
baseReserve = IERC20(baseToken).balanceOf(address(this));
|
||||
quoteReserve = IERC20(quoteToken).balanceOf(address(this));
|
||||
}
|
||||
|
||||
function sellQuote(address to) external returns (uint256 receiveBaseAmount) {
|
||||
uint256 quoteBal = IERC20(quoteToken).balanceOf(address(this));
|
||||
require(quoteBal >= quoteReserve, "MockDVMPool: quote");
|
||||
uint256 quoteInput = quoteBal - quoteReserve;
|
||||
if (quoteInput == 0) return 0;
|
||||
(receiveBaseAmount,) = this.querySellQuote(address(0), quoteInput);
|
||||
require(IERC20(baseToken).transfer(to, receiveBaseAmount), "MockDVMPool: b");
|
||||
baseReserve = IERC20(baseToken).balanceOf(address(this));
|
||||
quoteReserve = IERC20(quoteToken).balanceOf(address(this));
|
||||
}
|
||||
|
||||
function buyShares(address to) external returns (uint256, uint256, uint256) {
|
||||
uint256 baseBal = IERC20(baseToken).balanceOf(address(this));
|
||||
uint256 quoteBal = IERC20(quoteToken).balanceOf(address(this));
|
||||
require(baseBal >= baseReserve && quoteBal >= quoteReserve, "MockDVMPool: reserve");
|
||||
|
||||
uint256 baseInput = baseBal - baseReserve;
|
||||
uint256 quoteInput = quoteBal - quoteReserve;
|
||||
require(baseInput > 0 && quoteInput > 0, "MockDVMPool: no input");
|
||||
|
||||
uint256 lpShare;
|
||||
if (totalSupply == 0) {
|
||||
lpShare = baseBal;
|
||||
require(lpShare > 2001, "MockDVMPool: MINT_AMOUNT_NOT_ENOUGH");
|
||||
totalSupply = lpShare;
|
||||
_shares[address(0)] = 1001;
|
||||
lpShare -= 1001;
|
||||
_shares[to] += lpShare;
|
||||
} else {
|
||||
uint256 baseShares = (baseInput * totalSupply) / baseReserve;
|
||||
uint256 quoteShares = (quoteInput * totalSupply) / quoteReserve;
|
||||
lpShare = baseShares < quoteShares ? baseShares : quoteShares;
|
||||
totalSupply += lpShare;
|
||||
_shares[to] += lpShare;
|
||||
}
|
||||
|
||||
baseReserve = baseBal;
|
||||
quoteReserve = quoteBal;
|
||||
return (baseInput, quoteInput, lpShare);
|
||||
}
|
||||
|
||||
function sync() external {
|
||||
baseReserve = IERC20(baseToken).balanceOf(address(this));
|
||||
quoteReserve = IERC20(quoteToken).balanceOf(address(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
interface IAavePoolLike {
|
||||
function flashLoanSimple(
|
||||
address receiverAddress,
|
||||
address asset,
|
||||
uint256 amount,
|
||||
bytes calldata params,
|
||||
uint16 referralCode
|
||||
) external;
|
||||
}
|
||||
|
||||
interface IAaveFlashLoanSimpleReceiver {
|
||||
function executeOperation(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 premium,
|
||||
address initiator,
|
||||
bytes calldata params
|
||||
) external returns (bool);
|
||||
}
|
||||
|
||||
interface IAaveDODOQuotePushSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
interface IAaveExternalUnwinder {
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
interface IAaveAtomicBridgeCoordinator {
|
||||
struct CreateIntentParams {
|
||||
uint64 sourceChain;
|
||||
uint64 destinationChain;
|
||||
address assetIn;
|
||||
address assetOut;
|
||||
uint256 amountIn;
|
||||
uint256 minAmountOut;
|
||||
address recipient;
|
||||
uint256 deadline;
|
||||
bytes32 routeId;
|
||||
}
|
||||
|
||||
function createIntent(CreateIntentParams calldata p) external returns (bytes32 obligationId);
|
||||
|
||||
function submitCommitment(bytes32 obligationId, bytes32 settlementMode) external;
|
||||
|
||||
function obligationEscrow() external view returns (address);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title AaveQuotePushFlashReceiver
|
||||
* @notice Aave V3 flashLoanSimple receiver for the quote-push workflow:
|
||||
* flash borrow quote -> buy PMM base -> unwind base externally -> repay lender, retaining any surplus.
|
||||
*/
|
||||
contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable pool;
|
||||
|
||||
struct QuotePushParams {
|
||||
address integration;
|
||||
address pmmPool;
|
||||
address baseToken;
|
||||
address externalUnwinder;
|
||||
uint256 minOutPmm;
|
||||
uint256 minOutUnwind;
|
||||
bytes unwindData;
|
||||
AtomicBridgeParams atomicBridge;
|
||||
}
|
||||
|
||||
struct AtomicBridgeParams {
|
||||
address coordinator;
|
||||
uint64 sourceChain;
|
||||
uint64 destinationChain;
|
||||
address destinationAsset;
|
||||
uint256 bridgeAmount;
|
||||
uint256 minDestinationAmount;
|
||||
address destinationRecipient;
|
||||
uint256 destinationDeadline;
|
||||
bytes32 routeId;
|
||||
bytes32 settlementMode;
|
||||
bool submitCommitment;
|
||||
}
|
||||
|
||||
error UntrustedPool();
|
||||
error UntrustedInitiator();
|
||||
error BadParams();
|
||||
error InsufficientToRepay();
|
||||
error InvalidAtomicBridge();
|
||||
|
||||
event QuotePushExecuted(
|
||||
address indexed quoteToken,
|
||||
address indexed baseToken,
|
||||
uint256 borrowedAmount,
|
||||
uint256 premium,
|
||||
uint256 baseOut,
|
||||
uint256 unwindOut,
|
||||
uint256 surplus
|
||||
);
|
||||
event AtomicBridgeTriggered(
|
||||
bytes32 indexed obligationId,
|
||||
address indexed coordinator,
|
||||
address indexed destinationRecipient,
|
||||
uint256 bridgeAmount,
|
||||
uint256 minDestinationAmount
|
||||
);
|
||||
|
||||
constructor(address pool_) {
|
||||
pool = pool_;
|
||||
}
|
||||
|
||||
function flashQuotePush(address asset, uint256 amount, QuotePushParams calldata params) external {
|
||||
IAavePoolLike(pool).flashLoanSimple(address(this), asset, amount, abi.encode(address(this), params), 0);
|
||||
}
|
||||
|
||||
function executeOperation(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 premium,
|
||||
address initiator,
|
||||
bytes calldata params
|
||||
) external returns (bool) {
|
||||
if (msg.sender != pool) revert UntrustedPool();
|
||||
(address expectedInitiator, QuotePushParams memory p) = abi.decode(params, (address, QuotePushParams));
|
||||
if (initiator != expectedInitiator) revert UntrustedInitiator();
|
||||
if (
|
||||
p.integration == address(0) || p.pmmPool == address(0) || p.baseToken == address(0)
|
||||
|| p.externalUnwinder == address(0)
|
||||
) revert BadParams();
|
||||
if (p.baseToken == asset) revert BadParams();
|
||||
|
||||
uint256 baseOut = _swapQuoteForBase(asset, amount, p.integration, p.pmmPool, p.minOutPmm);
|
||||
|
||||
uint256 baseBal = IERC20(p.baseToken).balanceOf(address(this));
|
||||
if (p.atomicBridge.coordinator != address(0)) {
|
||||
_triggerAtomicBridge(p.baseToken, baseBal, p.atomicBridge);
|
||||
}
|
||||
|
||||
uint256 unwindOut = _unwindBaseIntoQuote(p.baseToken, asset, p.externalUnwinder, p.minOutUnwind, p.unwindData);
|
||||
uint256 surplus = _approveRepayment(asset, amount + premium);
|
||||
emit QuotePushExecuted(asset, p.baseToken, amount, premium, baseOut, unwindOut, surplus);
|
||||
return true;
|
||||
}
|
||||
|
||||
function _swapQuoteForBase(address asset, uint256 amount, address integration, address pmmPool, uint256 minOutPmm)
|
||||
internal
|
||||
returns (uint256 baseOut)
|
||||
{
|
||||
IERC20(asset).forceApprove(integration, amount);
|
||||
baseOut = IAaveDODOQuotePushSwapExactIn(integration).swapExactIn(pmmPool, asset, amount, minOutPmm);
|
||||
}
|
||||
|
||||
function _unwindBaseIntoQuote(
|
||||
address baseToken,
|
||||
address quoteToken,
|
||||
address externalUnwinder,
|
||||
uint256 minOutUnwind,
|
||||
bytes memory unwindData
|
||||
) internal returns (uint256 unwindOut) {
|
||||
uint256 remainingBase = IERC20(baseToken).balanceOf(address(this));
|
||||
IERC20(baseToken).forceApprove(externalUnwinder, remainingBase);
|
||||
unwindOut =
|
||||
IAaveExternalUnwinder(externalUnwinder).unwind(baseToken, quoteToken, remainingBase, minOutUnwind, unwindData);
|
||||
}
|
||||
|
||||
function _approveRepayment(address quoteToken, uint256 need) internal returns (uint256 surplus) {
|
||||
IERC20 quote = IERC20(quoteToken);
|
||||
uint256 quoteBal = quote.balanceOf(address(this));
|
||||
if (quoteBal < need) revert InsufficientToRepay();
|
||||
surplus = quoteBal - need;
|
||||
quote.forceApprove(pool, need);
|
||||
}
|
||||
|
||||
function _triggerAtomicBridge(address baseToken, uint256 baseBal, AtomicBridgeParams memory atomicBridge) internal {
|
||||
if (
|
||||
atomicBridge.destinationAsset == address(0) || atomicBridge.destinationRecipient == address(0)
|
||||
|| atomicBridge.bridgeAmount == 0 || atomicBridge.bridgeAmount > baseBal
|
||||
|| atomicBridge.destinationDeadline <= block.timestamp
|
||||
) revert InvalidAtomicBridge();
|
||||
|
||||
address escrowAddress = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).obligationEscrow();
|
||||
IERC20(baseToken).forceApprove(escrowAddress, atomicBridge.bridgeAmount);
|
||||
bytes32 obligationId = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).createIntent(
|
||||
IAaveAtomicBridgeCoordinator.CreateIntentParams({
|
||||
sourceChain: atomicBridge.sourceChain,
|
||||
destinationChain: atomicBridge.destinationChain,
|
||||
assetIn: baseToken,
|
||||
assetOut: atomicBridge.destinationAsset,
|
||||
amountIn: atomicBridge.bridgeAmount,
|
||||
minAmountOut: atomicBridge.minDestinationAmount,
|
||||
recipient: atomicBridge.destinationRecipient,
|
||||
deadline: atomicBridge.destinationDeadline,
|
||||
routeId: atomicBridge.routeId
|
||||
})
|
||||
);
|
||||
if (atomicBridge.submitCommitment) {
|
||||
IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).submitCommitment(
|
||||
obligationId, atomicBridge.settlementMode
|
||||
);
|
||||
}
|
||||
emit AtomicBridgeTriggered(
|
||||
obligationId,
|
||||
atomicBridge.coordinator,
|
||||
atomicBridge.destinationRecipient,
|
||||
atomicBridge.bridgeAmount,
|
||||
atomicBridge.minDestinationAmount
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {ICrossChainFlashBridge} from "./interfaces/ICrossChainFlashBridge.sol";
|
||||
|
||||
/**
|
||||
* @title CrossChainFlashBorrower
|
||||
* @notice ERC-3156 borrower: flash `token` on **this** chain → bridge `bridgeAmount` out → repay `amount + fee` to the flash vault from **on-hand** balance.
|
||||
* @dev **Atomicity is single-chain only.** CCIP / bridge finality is asynchronous; destination delivery cannot complete inside this callback.
|
||||
* **Prefunding:** before `flashLoan`, this contract should hold at least `bridgeAmount + fee` of `token`
|
||||
* (or `amount + fee` if `bridgeAmount == amount`) so repayment succeeds after the bridge pulls `bridgeAmount`.
|
||||
* Encode `CrossChainFlashParams` in `data`.
|
||||
*/
|
||||
contract CrossChainFlashBorrower is IERC3156FlashBorrower {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||||
|
||||
address public immutable trustedLender;
|
||||
|
||||
struct CrossChainFlashParams {
|
||||
address bridge;
|
||||
uint256 bridgeAmount;
|
||||
uint64 destinationChainSelector;
|
||||
address recipientOnDestination;
|
||||
bytes bridgeExtraData;
|
||||
uint256 nativeBridgeFee;
|
||||
}
|
||||
|
||||
error UntrustedLender();
|
||||
error BadParams();
|
||||
error InsufficientToRepay();
|
||||
|
||||
constructor(address trustedLender_) {
|
||||
trustedLender = trustedLender_;
|
||||
}
|
||||
|
||||
function onFlashLoan(
|
||||
address,
|
||||
address token,
|
||||
uint256 amount,
|
||||
uint256 fee,
|
||||
bytes calldata data
|
||||
) external override returns (bytes32) {
|
||||
if (msg.sender != trustedLender) revert UntrustedLender();
|
||||
_run(abi.decode(data, (CrossChainFlashParams)), token, amount, fee);
|
||||
return _RETURN_VALUE;
|
||||
}
|
||||
|
||||
function _run(CrossChainFlashParams memory p, address token, uint256 amount, uint256 fee) internal {
|
||||
if (p.bridge == address(0) || p.recipientOnDestination == address(0)) revert BadParams();
|
||||
if (p.bridgeAmount == 0 || p.bridgeAmount > amount) revert BadParams();
|
||||
|
||||
IERC20 t = IERC20(token);
|
||||
t.forceApprove(p.bridge, p.bridgeAmount);
|
||||
ICrossChainFlashBridge(p.bridge).bridgeTokensFrom{value: p.nativeBridgeFee}(
|
||||
token,
|
||||
p.bridgeAmount,
|
||||
p.destinationChainSelector,
|
||||
p.recipientOnDestination,
|
||||
p.bridgeExtraData
|
||||
);
|
||||
|
||||
uint256 need = amount + fee;
|
||||
if (t.balanceOf(address(this)) < need) revert InsufficientToRepay();
|
||||
t.safeTransfer(msg.sender, need);
|
||||
}
|
||||
|
||||
receive() external payable {}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {IRouterClient} from "../ccip/IRouterClient.sol";
|
||||
|
||||
/**
|
||||
* @title CrossChainFlashRepayReceiver
|
||||
* @notice **Destination-chain** CCIP receiver: forwards received ERC-20s to a recipient encoded in `message.data`.
|
||||
* @dev Intended pairing with `CrossChainFlashBorrower` / bridge senders. `msg.sender` must be the CCIP router.
|
||||
* `data` encoding: `abi.encode(address recipient, bytes32 obligationId)` where `obligationId` is opaque (indexing / ops).
|
||||
* **Repaying the source-chain flash vault** requires a **separate** flow (second tx / bridge back); this contract does not pull debt on source.
|
||||
*/
|
||||
contract CrossChainFlashRepayReceiver {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
IRouterClient public immutable ccipRouter;
|
||||
|
||||
event CrossChainDelivered(
|
||||
bytes32 indexed messageId,
|
||||
uint64 indexed sourceChainSelector,
|
||||
address indexed recipient,
|
||||
address token,
|
||||
uint256 amount,
|
||||
bytes32 obligationId
|
||||
);
|
||||
|
||||
error OnlyRouter();
|
||||
error NoTokens();
|
||||
error BadData();
|
||||
|
||||
constructor(address ccipRouter_) {
|
||||
require(ccipRouter_ != address(0), "CrossChainFlashRepayReceiver: zero router");
|
||||
ccipRouter = IRouterClient(ccipRouter_);
|
||||
}
|
||||
|
||||
modifier onlyRouter() {
|
||||
if (msg.sender != address(ccipRouter)) revert OnlyRouter();
|
||||
_;
|
||||
}
|
||||
|
||||
function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyRouter {
|
||||
_deliver(message);
|
||||
}
|
||||
|
||||
function _deliver(IRouterClient.Any2EVMMessage calldata m) private {
|
||||
if (m.tokenAmounts.length == 0) revert NoTokens();
|
||||
IRouterClient.TokenAmount calldata ta = m.tokenAmounts[0];
|
||||
if (ta.token == address(0) || ta.amount == 0) revert NoTokens();
|
||||
(address recipient, bytes32 obligationId) = abi.decode(m.data, (address, bytes32));
|
||||
if (recipient == address(0)) revert BadData();
|
||||
IERC20(ta.token).safeTransfer(recipient, ta.amount);
|
||||
emit CrossChainDelivered(m.messageId, m.sourceChainSelector, recipient, ta.token, ta.amount, obligationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {IRouterClient} from "../ccip/IRouterClient.sol";
|
||||
|
||||
/**
|
||||
* @title CrossChainFlashVaultCreditReceiver
|
||||
* @notice **Source-chain** (or same-chain) CCIP receiver: forwards the first ERC-20 leg to a vault address encoded in `message.data`.
|
||||
* @dev Use to **restore flash vault balance** after off-chain or destination-chain settlement. This is not an ERC-3156 repayment
|
||||
* (that must still happen in the flash callback on the borrow chain). `data` = `abi.encode(address vault)`.
|
||||
*/
|
||||
contract CrossChainFlashVaultCreditReceiver {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
IRouterClient public immutable ccipRouter;
|
||||
|
||||
event LiquidityCredited(
|
||||
bytes32 indexed messageId,
|
||||
uint64 indexed sourceChainSelector,
|
||||
address indexed vault,
|
||||
address token,
|
||||
uint256 amount
|
||||
);
|
||||
|
||||
error OnlyRouter();
|
||||
error NoTokens();
|
||||
error BadData();
|
||||
|
||||
constructor(address ccipRouter_) {
|
||||
require(ccipRouter_ != address(0), "CrossChainFlashVaultCreditReceiver: zero router");
|
||||
ccipRouter = IRouterClient(ccipRouter_);
|
||||
}
|
||||
|
||||
modifier onlyRouter() {
|
||||
if (msg.sender != address(ccipRouter)) revert OnlyRouter();
|
||||
_;
|
||||
}
|
||||
|
||||
function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyRouter {
|
||||
_credit(message);
|
||||
}
|
||||
|
||||
function _credit(IRouterClient.Any2EVMMessage calldata m) private {
|
||||
if (m.tokenAmounts.length == 0) revert NoTokens();
|
||||
IRouterClient.TokenAmount calldata ta = m.tokenAmounts[0];
|
||||
if (ta.token == address(0) || ta.amount == 0) revert NoTokens();
|
||||
address vault = abi.decode(m.data, (address));
|
||||
if (vault == address(0)) revert BadData();
|
||||
IERC20(ta.token).safeTransfer(vault, ta.amount);
|
||||
emit LiquidityCredited(m.messageId, m.sourceChainSelector, vault, ta.token, ta.amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
interface IDODOIntegrationSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title DODOIntegrationExternalUnwinder
|
||||
* @notice Unwinds base -> quote through a DODO PMM integration.
|
||||
* @dev `data` must be `abi.encode(address pool)` selecting the registered pool to use.
|
||||
*/
|
||||
contract DODOIntegrationExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable integration;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address integration_) {
|
||||
integration = integration_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams();
|
||||
if (data.length != 32) revert BadParams();
|
||||
|
||||
address pool = abi.decode(data, (address));
|
||||
if (pool == address(0)) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(integration, amountIn);
|
||||
amountOut = IDODOIntegrationSwapExactIn(integration).swapExactIn(pool, tokenIn, amountIn, minAmountOut);
|
||||
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {ISwapRouter} from "../bridge/trustless/interfaces/ISwapRouter.sol";
|
||||
|
||||
interface IDODOMultiHopSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title DODOToUniswapV3MultiHopExternalUnwinder
|
||||
* @notice Unwinds through a DODO PMM first hop and a Uniswap V3 final hop.
|
||||
* @dev `data` must be abi.encode(address dodoPool, address intermediateToken, uint256 minIntermediateOut, bytes uniswapPath).
|
||||
* `uniswapPath` is the standard exactInput path beginning with `intermediateToken` and ending with `tokenOut`.
|
||||
*/
|
||||
contract DODOToUniswapV3MultiHopExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable integration;
|
||||
address public immutable router;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address integration_, address router_) {
|
||||
integration = integration_;
|
||||
router = router_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams();
|
||||
|
||||
(address dodoPool, address intermediateToken, uint256 minIntermediateOut, bytes memory uniswapPath) =
|
||||
abi.decode(data, (address, address, uint256, bytes));
|
||||
if (dodoPool == address(0) || intermediateToken == address(0) || intermediateToken == tokenIn || intermediateToken == tokenOut) {
|
||||
revert BadParams();
|
||||
}
|
||||
if (uniswapPath.length < 43) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(integration, amountIn);
|
||||
uint256 intermediateOut =
|
||||
IDODOMultiHopSwapExactIn(integration).swapExactIn(dodoPool, tokenIn, amountIn, minIntermediateOut);
|
||||
|
||||
IERC20(intermediateToken).forceApprove(router, intermediateOut);
|
||||
amountOut = ISwapRouter(router).exactInput(
|
||||
ISwapRouter.ExactInputParams({
|
||||
path: uniswapPath,
|
||||
recipient: msg.sender,
|
||||
deadline: block.timestamp + 300,
|
||||
amountIn: intermediateOut,
|
||||
amountOutMinimum: minAmountOut
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
/**
|
||||
* @title MinimalERC3156FlashBorrower
|
||||
* @notice Repays vault via ERC-20 transfer in `onFlashLoan`. Pre-fund this contract with at least `flashFee` of the loan token before calling `flashLoan` (vault sends `amount` only before the callback).
|
||||
*/
|
||||
contract MinimalERC3156FlashBorrower is IERC3156FlashBorrower {
|
||||
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||||
|
||||
address public immutable trustedLender;
|
||||
|
||||
error UntrustedLender();
|
||||
|
||||
constructor(address trustedLender_) {
|
||||
trustedLender = trustedLender_;
|
||||
}
|
||||
|
||||
function onFlashLoan(
|
||||
address,
|
||||
address token,
|
||||
uint256 amount,
|
||||
uint256 fee,
|
||||
bytes calldata
|
||||
) external override returns (bytes32) {
|
||||
if (msg.sender != trustedLender) revert UntrustedLender();
|
||||
IERC20(token).transfer(msg.sender, amount + fee);
|
||||
return _RETURN_VALUE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
/// @dev Matches `DODOPMMIntegration.swapExactIn` surface (any registered pool).
|
||||
interface IDODOQuotePushSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
/// @dev Minimal external unwind interface for converting PMM base back into flash-borrowed quote.
|
||||
interface IExternalUnwinder {
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title QuotePushFlashWorkflowBorrower
|
||||
* @notice ERC-3156 borrower for a quote-push loop:
|
||||
* flash `quoteToken` -> buy `baseToken` from a DODO-style PMM -> unwind externally back into `quoteToken`
|
||||
* -> repay `amount + fee`, leaving any quote surplus on this contract.
|
||||
* @dev `data` must be `abi.encode(QuotePushParams)`. The caller is responsible for choosing trusted integrations,
|
||||
* setting conservative minimums, and sweeping any retained surplus from this contract after execution.
|
||||
*/
|
||||
contract QuotePushFlashWorkflowBorrower is IERC3156FlashBorrower {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||||
|
||||
address public immutable trustedLender;
|
||||
|
||||
struct QuotePushParams {
|
||||
address integration;
|
||||
address pool;
|
||||
address baseToken;
|
||||
address externalUnwinder;
|
||||
uint256 minOutPmm;
|
||||
uint256 minOutUnwind;
|
||||
bytes unwindData;
|
||||
}
|
||||
|
||||
error UntrustedLender();
|
||||
error BadParams();
|
||||
error InsufficientToRepay();
|
||||
|
||||
event QuotePushExecuted(
|
||||
address indexed quoteToken,
|
||||
address indexed baseToken,
|
||||
uint256 borrowedAmount,
|
||||
uint256 fee,
|
||||
uint256 baseOut,
|
||||
uint256 unwindOut,
|
||||
uint256 surplus
|
||||
);
|
||||
|
||||
constructor(address trustedLender_) {
|
||||
trustedLender = trustedLender_;
|
||||
}
|
||||
|
||||
function onFlashLoan(
|
||||
address,
|
||||
address quoteToken,
|
||||
uint256 amount,
|
||||
uint256 fee,
|
||||
bytes calldata data
|
||||
) external override returns (bytes32) {
|
||||
if (msg.sender != trustedLender) revert UntrustedLender();
|
||||
QuotePushParams memory p = abi.decode(data, (QuotePushParams));
|
||||
if (
|
||||
p.integration == address(0) || p.pool == address(0) || p.baseToken == address(0)
|
||||
|| p.externalUnwinder == address(0)
|
||||
) revert BadParams();
|
||||
if (p.baseToken == quoteToken) revert BadParams();
|
||||
|
||||
IERC20 borrowed = IERC20(quoteToken);
|
||||
IERC20 base = IERC20(p.baseToken);
|
||||
|
||||
borrowed.forceApprove(p.integration, amount);
|
||||
uint256 baseOut = IDODOQuotePushSwapExactIn(p.integration).swapExactIn(p.pool, quoteToken, amount, p.minOutPmm);
|
||||
|
||||
uint256 baseBal = base.balanceOf(address(this));
|
||||
base.forceApprove(p.externalUnwinder, baseBal);
|
||||
uint256 unwindOut =
|
||||
IExternalUnwinder(p.externalUnwinder).unwind(p.baseToken, quoteToken, baseBal, p.minOutUnwind, p.unwindData);
|
||||
|
||||
uint256 need = amount + fee;
|
||||
uint256 quoteBal = borrowed.balanceOf(address(this));
|
||||
if (quoteBal < need) revert InsufficientToRepay();
|
||||
|
||||
uint256 surplus = quoteBal - need;
|
||||
borrowed.safeTransfer(msg.sender, need);
|
||||
emit QuotePushExecuted(quoteToken, p.baseToken, amount, fee, baseOut, unwindOut, surplus);
|
||||
|
||||
return _RETURN_VALUE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
|
||||
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
|
||||
/**
|
||||
* @title SimpleERC3156FlashVault
|
||||
* @notice Minimal ERC-3156 flash lender: whitelist per token, flat bps fee, same-block repayment invariant.
|
||||
* @dev Intended for Chain 138 / internal atomic workflows when no external flash source exists.
|
||||
*/
|
||||
contract SimpleERC3156FlashVault is IERC3156FlashLender, Ownable, ReentrancyGuard {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||||
|
||||
/// @notice Maximum owner-configurable fee (10%). Prevents griefing at 100% fee.
|
||||
uint256 public constant MAX_FEE_BPS = 1000;
|
||||
|
||||
/// @notice Fee on borrowed amount, basis points (10_000 = 100%).
|
||||
uint256 public feeBps;
|
||||
|
||||
mapping(address token => bool) public supportedToken;
|
||||
|
||||
/// @notice When true, only `approvedBorrower[receiver]` may be used as the flash callback contract.
|
||||
bool public borrowerAllowlistEnabled;
|
||||
|
||||
mapping(address borrower => bool) public approvedBorrower;
|
||||
|
||||
/// @notice Cumulative fees retained by the vault per token (for ops / accounting).
|
||||
mapping(address token => uint256) public totalFeesCollected;
|
||||
|
||||
/// @notice initiator = flashLoan caller; receiver = IERC3156FlashBorrower callback target.
|
||||
event FlashLoan(address indexed initiator, IERC3156FlashBorrower indexed receiver, address indexed token, uint256 amount, uint256 fee);
|
||||
event FeeBpsUpdated(uint256 feeBps);
|
||||
event TokenSupportUpdated(address indexed token, bool supported);
|
||||
event TokensRescued(address indexed token, address indexed to, uint256 amount);
|
||||
event BorrowerAllowlistEnabledUpdated(bool enabled);
|
||||
event BorrowerApprovalUpdated(address indexed borrower, bool approved);
|
||||
|
||||
error UnsupportedToken();
|
||||
error ZeroAmount();
|
||||
error FeeTooHigh();
|
||||
error InvalidCallback();
|
||||
error RepaymentFailed();
|
||||
error ZeroRescue();
|
||||
error ZeroRecipient();
|
||||
error BorrowerNotApproved();
|
||||
|
||||
constructor(address initialOwner, uint256 initialFeeBps) Ownable(initialOwner) {
|
||||
_setFeeBps(initialFeeBps);
|
||||
}
|
||||
|
||||
function setFeeBps(uint256 newFeeBps) external onlyOwner {
|
||||
_setFeeBps(newFeeBps);
|
||||
}
|
||||
|
||||
function setTokenSupported(address token, bool supported) external onlyOwner {
|
||||
supportedToken[token] = supported;
|
||||
emit TokenSupportUpdated(token, supported);
|
||||
}
|
||||
|
||||
function setBorrowerAllowlistEnabled(bool enabled) external onlyOwner {
|
||||
borrowerAllowlistEnabled = enabled;
|
||||
emit BorrowerAllowlistEnabledUpdated(enabled);
|
||||
}
|
||||
|
||||
function setBorrowerApproved(address borrower, bool approved) external onlyOwner {
|
||||
approvedBorrower[borrower] = approved;
|
||||
emit BorrowerApprovalUpdated(borrower, approved);
|
||||
}
|
||||
|
||||
/// @notice Operator alias for `flashFee` (same revert rules).
|
||||
function previewFlashFee(address token, uint256 amount) external view returns (uint256) {
|
||||
return flashFee(token, amount);
|
||||
}
|
||||
|
||||
/// @notice Owner-only recovery (mis-seeded asset, deprecated token, migration). Does not bypass flash invariants on active loans in the same tx.
|
||||
function rescueTokens(address token, uint256 amount, address to) external onlyOwner {
|
||||
if (amount == 0) revert ZeroRescue();
|
||||
if (to == address(0)) revert ZeroRecipient();
|
||||
IERC20(token).safeTransfer(to, amount);
|
||||
emit TokensRescued(token, to, amount);
|
||||
}
|
||||
|
||||
function maxFlashLoan(address token) external view override returns (uint256) {
|
||||
if (!supportedToken[token]) {
|
||||
return 0;
|
||||
}
|
||||
return IERC20(token).balanceOf(address(this));
|
||||
}
|
||||
|
||||
function flashFee(address token, uint256 amount) public view override returns (uint256) {
|
||||
if (!supportedToken[token]) {
|
||||
revert UnsupportedToken();
|
||||
}
|
||||
return (amount * feeBps) / 10_000;
|
||||
}
|
||||
|
||||
function flashLoan(
|
||||
IERC3156FlashBorrower receiver,
|
||||
address token,
|
||||
uint256 amount,
|
||||
bytes calldata data
|
||||
) external override nonReentrant returns (bool) {
|
||||
if (!supportedToken[token]) revert UnsupportedToken();
|
||||
if (amount == 0) revert ZeroAmount();
|
||||
if (borrowerAllowlistEnabled && !approvedBorrower[address(receiver)]) {
|
||||
revert BorrowerNotApproved();
|
||||
}
|
||||
|
||||
uint256 fee = flashFee(token, amount);
|
||||
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
|
||||
if (balanceBefore < amount) revert RepaymentFailed();
|
||||
|
||||
IERC20(token).safeTransfer(address(receiver), amount);
|
||||
|
||||
bytes32 retval = receiver.onFlashLoan(msg.sender, token, amount, fee, data);
|
||||
if (retval != _RETURN_VALUE) revert InvalidCallback();
|
||||
|
||||
if (IERC20(token).balanceOf(address(this)) < balanceBefore + fee) {
|
||||
revert RepaymentFailed();
|
||||
}
|
||||
|
||||
totalFeesCollected[token] += fee;
|
||||
|
||||
emit FlashLoan(msg.sender, receiver, token, amount, fee);
|
||||
return true;
|
||||
}
|
||||
|
||||
function _setFeeBps(uint256 newFeeBps) internal {
|
||||
if (newFeeBps > MAX_FEE_BPS) revert FeeTooHigh();
|
||||
feeBps = newFeeBps;
|
||||
emit FeeBpsUpdated(newFeeBps);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
/// @dev Matches `DODOPMMIntegration.swapExactIn` surface (any registered pool).
|
||||
interface IDODOStyleSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title SwapFlashWorkflowBorrower
|
||||
* @notice ERC-3156 borrower: flash `token` → swap to `midToken` → swap back to `token` → repay `amount + fee`.
|
||||
* @dev `data` must be `abi.encode(SwapFlashParams)`. The caller chooses `integration` — use only trusted DODO/PMM routers.
|
||||
* `pool` is typically the same for both legs (e.g. cUSDT/USDT round-trip). Set mins from off-chain quotes.
|
||||
*/
|
||||
contract SwapFlashWorkflowBorrower is IERC3156FlashBorrower {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||||
|
||||
address public immutable trustedLender;
|
||||
|
||||
struct SwapFlashParams {
|
||||
address integration;
|
||||
address pool;
|
||||
address midToken;
|
||||
uint256 minOutFirst;
|
||||
uint256 minOutSecond;
|
||||
}
|
||||
|
||||
error UntrustedLender();
|
||||
error BadParams();
|
||||
error InsufficientToRepay();
|
||||
|
||||
constructor(address trustedLender_) {
|
||||
trustedLender = trustedLender_;
|
||||
}
|
||||
|
||||
function onFlashLoan(
|
||||
address,
|
||||
address token,
|
||||
uint256 amount,
|
||||
uint256 fee,
|
||||
bytes calldata data
|
||||
) external override returns (bytes32) {
|
||||
if (msg.sender != trustedLender) revert UntrustedLender();
|
||||
SwapFlashParams memory p = abi.decode(data, (SwapFlashParams));
|
||||
if (p.integration == address(0) || p.pool == address(0) || p.midToken == address(0)) revert BadParams();
|
||||
if (p.midToken == token) revert BadParams();
|
||||
|
||||
IERC20 borrowed = IERC20(token);
|
||||
IERC20 mid = IERC20(p.midToken);
|
||||
|
||||
borrowed.forceApprove(p.integration, amount);
|
||||
IDODOStyleSwapExactIn(p.integration).swapExactIn(p.pool, token, amount, p.minOutFirst);
|
||||
|
||||
uint256 midBal = mid.balanceOf(address(this));
|
||||
mid.forceApprove(p.integration, midBal);
|
||||
IDODOStyleSwapExactIn(p.integration).swapExactIn(p.pool, p.midToken, midBal, p.minOutSecond);
|
||||
|
||||
uint256 need = amount + fee;
|
||||
if (borrowed.balanceOf(address(this)) < need) revert InsufficientToRepay();
|
||||
borrowed.safeTransfer(msg.sender, need);
|
||||
|
||||
return _RETURN_VALUE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {ISwapRouter} from "../bridge/trustless/interfaces/ISwapRouter.sol";
|
||||
|
||||
/**
|
||||
* @title UniswapV3ExternalUnwinder
|
||||
* @notice External unwind adapter for quote-push workflows using the Uniswap V3 SwapRouter.
|
||||
* @dev `data` is optional:
|
||||
* - `abi.encode(uint24 fee)` for exactInputSingle
|
||||
* - `abi.encode(bytes path)` for exactInput multi-hop path
|
||||
*/
|
||||
contract UniswapV3ExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable router;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address router_) {
|
||||
router = router_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut) revert BadParams();
|
||||
if (amountIn == 0) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(router, amountIn);
|
||||
|
||||
if (data.length == 32) {
|
||||
uint24 fee = abi.decode(data, (uint24));
|
||||
amountOut = ISwapRouter(router).exactInputSingle(
|
||||
ISwapRouter.ExactInputSingleParams({
|
||||
tokenIn: tokenIn,
|
||||
tokenOut: tokenOut,
|
||||
fee: fee,
|
||||
recipient: msg.sender,
|
||||
deadline: block.timestamp + 300,
|
||||
amountIn: amountIn,
|
||||
amountOutMinimum: minAmountOut,
|
||||
sqrtPriceLimitX96: 0
|
||||
})
|
||||
);
|
||||
return amountOut;
|
||||
}
|
||||
|
||||
bytes memory path = abi.decode(data, (bytes));
|
||||
amountOut = ISwapRouter(router).exactInput(
|
||||
ISwapRouter.ExactInputParams({
|
||||
path: path,
|
||||
recipient: msg.sender,
|
||||
deadline: block.timestamp + 300,
|
||||
amountIn: amountIn,
|
||||
amountOutMinimum: minAmountOut
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {UniversalCCIPBridge} from "../bridge/UniversalCCIPBridge.sol";
|
||||
import {ICrossChainFlashBridge} from "./interfaces/ICrossChainFlashBridge.sol";
|
||||
|
||||
/**
|
||||
* @title UniversalCCIPFlashBridgeAdapter
|
||||
* @notice Pulls `token` from the caller (e.g. `CrossChainFlashBorrower`) and forwards a `UniversalCCIPBridge.bridge` call.
|
||||
* @dev `extraData` (see `ICrossChainFlashBridge`): if empty, uses `assetType = 0`, `usePMM/useVault = false`, empty proofs.
|
||||
* If non-empty: `abi.encode(bytes32 assetType, bool usePMM, bool useVault, bytes complianceProof, bytes vaultInstructions)`.
|
||||
* Native value is forwarded for CCIP fees on the underlying bridge.
|
||||
*/
|
||||
contract UniversalCCIPFlashBridgeAdapter is ICrossChainFlashBridge {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
UniversalCCIPBridge public immutable universalBridge;
|
||||
|
||||
constructor(address universalBridge_) {
|
||||
require(universalBridge_ != address(0), "UniversalCCIPFlashBridgeAdapter: zero bridge");
|
||||
universalBridge = UniversalCCIPBridge(payable(universalBridge_));
|
||||
}
|
||||
|
||||
function bridgeTokensFrom(
|
||||
address token,
|
||||
uint256 amount,
|
||||
uint64 destinationChainSelector,
|
||||
address recipientOnDestination,
|
||||
bytes calldata extraData
|
||||
) external payable override returns (bytes32 messageId) {
|
||||
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
|
||||
IERC20(token).forceApprove(address(universalBridge), amount);
|
||||
|
||||
bytes32 assetType;
|
||||
bool usePMM;
|
||||
bool useVault;
|
||||
bytes memory complianceProof;
|
||||
bytes memory vaultInstructions;
|
||||
|
||||
if (extraData.length == 0) {
|
||||
assetType = bytes32(0);
|
||||
usePMM = false;
|
||||
useVault = false;
|
||||
complianceProof = "";
|
||||
vaultInstructions = "";
|
||||
} else {
|
||||
(assetType, usePMM, useVault, complianceProof, vaultInstructions) =
|
||||
abi.decode(extraData, (bytes32, bool, bool, bytes, bytes));
|
||||
}
|
||||
|
||||
UniversalCCIPBridge.BridgeOperation memory op = UniversalCCIPBridge.BridgeOperation({
|
||||
token: token,
|
||||
amount: amount,
|
||||
destinationChain: destinationChainSelector,
|
||||
recipient: recipientOnDestination,
|
||||
assetType: assetType,
|
||||
usePMM: usePMM,
|
||||
useVault: useVault,
|
||||
complianceProof: complianceProof,
|
||||
vaultInstructions: vaultInstructions
|
||||
});
|
||||
|
||||
messageId = universalBridge.bridge{value: msg.value}(op);
|
||||
IERC20(token).forceApprove(address(universalBridge), 0);
|
||||
}
|
||||
|
||||
receive() external payable {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @title ICrossChainFlashBridge
|
||||
* @notice Minimal surface for “pull `token` from msg.sender then initiate CCIP / bridge”.
|
||||
* @dev Wire to `UniversalCCIPFlashBridgeAdapter` (→ `UniversalCCIPBridge.bridge`), a dedicated adapter, or a mock in tests.
|
||||
* Implementations MUST pull from `msg.sender` (e.g. `transferFrom`) up to `amount` after allowance.
|
||||
* For `UniversalCCIPFlashBridgeAdapter`, optional `extraData` encodes
|
||||
* `(bytes32 assetType, bool usePMM, bool useVault, bytes complianceProof, bytes vaultInstructions)`; empty uses zeros/false.
|
||||
*/
|
||||
interface ICrossChainFlashBridge {
|
||||
function bridgeTokensFrom(
|
||||
address token,
|
||||
uint256 amount,
|
||||
uint64 destinationChainSelector,
|
||||
address recipientOnDestination,
|
||||
bytes calldata extraData
|
||||
) external payable returns (bytes32 messageId);
|
||||
}
|
||||
@@ -3,9 +3,9 @@ pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "../vendor/openzeppelin/ReentrancyGuardUpgradeable.sol";
|
||||
import "./interfaces/IISO4217WToken.sol";
|
||||
import "./libraries/ISO4217WCompliance.sol";
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../vendor/openzeppelin/ReentrancyGuardUpgradeable.sol";
|
||||
import "./interfaces/ILiquidityProvider.sol";
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "../registry/UniversalAssetRegistry.sol";
|
||||
|
||||
|
||||
@@ -44,15 +44,62 @@ contract DODOPMMProvider is ILiquidityProvider, AccessControl {
|
||||
uint256 amountIn
|
||||
) external view override returns (uint256 amountOut, uint256 slippageBps) {
|
||||
address pool = pools[tokenIn][tokenOut];
|
||||
|
||||
|
||||
if (pool == address(0)) {
|
||||
return (0, 10000); // No pool, 100% slippage
|
||||
}
|
||||
|
||||
try dodoIntegration.getPoolPriceOrOracle(pool) returns (uint256 price) {
|
||||
// Simple calculation (in production, would use actual DODO math)
|
||||
amountOut = (amountIn * price) / 1e18;
|
||||
slippageBps = 30; // 0.3% typical for stablecoin pools
|
||||
|
||||
try dodoIntegration.getPoolConfig(pool) returns (DODOPMMIntegration.PoolConfig memory config) {
|
||||
if (tokenIn == config.baseToken && tokenOut == config.quoteToken) {
|
||||
try IDODOPMMPool(pool).querySellBase(address(this), amountIn) returns (uint256 quoteAmount, uint256) {
|
||||
return (quoteAmount, 30);
|
||||
} catch {
|
||||
return _fallbackReserveQuote(pool, config, true, amountIn);
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenIn == config.quoteToken && tokenOut == config.baseToken) {
|
||||
try IDODOPMMPool(pool).querySellQuote(address(this), amountIn) returns (uint256 baseAmount, uint256) {
|
||||
return (baseAmount, 30);
|
||||
} catch {
|
||||
return _fallbackReserveQuote(pool, config, false, amountIn);
|
||||
}
|
||||
}
|
||||
|
||||
return (0, 10000);
|
||||
} catch {
|
||||
return (0, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
function _fallbackReserveQuote(
|
||||
address pool,
|
||||
DODOPMMIntegration.PoolConfig memory config,
|
||||
bool sellBase,
|
||||
uint256 amountIn
|
||||
) internal view returns (uint256 amountOut, uint256 slippageBps) {
|
||||
try IDODOPMMPool(pool).getVaultReserve() returns (uint256 baseReserve, uint256 quoteReserve) {
|
||||
if (baseReserve == 0 || quoteReserve == 0 || amountIn == 0) {
|
||||
return (0, 10000);
|
||||
}
|
||||
|
||||
uint256 netAmountIn = amountIn;
|
||||
if (config.lpFeeRate < 10_000) {
|
||||
netAmountIn = (amountIn * (10_000 - config.lpFeeRate)) / 10_000;
|
||||
}
|
||||
|
||||
if (netAmountIn == 0) {
|
||||
return (0, 10000);
|
||||
}
|
||||
|
||||
if (sellBase) {
|
||||
amountOut = (netAmountIn * quoteReserve) / (baseReserve + netAmountIn);
|
||||
} else {
|
||||
amountOut = (netAmountIn * baseReserve) / (quoteReserve + netAmountIn);
|
||||
}
|
||||
|
||||
// Treat fallback quotes as conservative / read-only approximations.
|
||||
slippageBps = 100;
|
||||
return (amountOut, slippageBps);
|
||||
} catch {
|
||||
return (0, 10000);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,7 @@ contract CCIPRelayRouter is AccessControl {
|
||||
|
||||
event BridgeAuthorized(address indexed bridge);
|
||||
event BridgeRevoked(address indexed bridge);
|
||||
|
||||
|
||||
constructor() {
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
}
|
||||
@@ -74,17 +74,14 @@ contract CCIPRelayRouter is AccessControl {
|
||||
// Call bridge's ccipReceive function using low-level call
|
||||
// This ensures proper ABI encoding for the struct parameter
|
||||
// The call will revert with the actual error if it fails
|
||||
(bool success, bytes memory returnData) = bridge.call(
|
||||
(bool success, ) = bridge.call(
|
||||
abi.encodeWithSignature("ccipReceive((bytes32,uint64,bytes,bytes,(address,uint256,uint8)[]))", message)
|
||||
);
|
||||
require(success, "CCIPRelayRouter: ccipReceive failed");
|
||||
|
||||
// If we get here, the call succeeded
|
||||
// Decode recipient and amount from message data
|
||||
(address recipient, uint256 amount, , ) = abi.decode(
|
||||
message.data,
|
||||
(address, uint256, address, uint256)
|
||||
);
|
||||
// If we get here, the call succeeded. Decode common payload shapes without
|
||||
// reverting the full relay transaction on non-WETH bridge payloads.
|
||||
(address recipient, uint256 amount) = _decodeRecipientAndAmount(message.data);
|
||||
|
||||
emit MessageRelayed(
|
||||
message.messageId,
|
||||
@@ -94,5 +91,26 @@ contract CCIPRelayRouter is AccessControl {
|
||||
amount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _decodeRecipientAndAmount(bytes calldata data)
|
||||
internal
|
||||
pure
|
||||
returns (address recipient, uint256 amount)
|
||||
{
|
||||
if (data.length == 64) {
|
||||
return abi.decode(data, (address, uint256));
|
||||
}
|
||||
|
||||
if (data.length == 96) {
|
||||
(, recipient, amount) = abi.decode(data, (address, address, uint256));
|
||||
return (recipient, amount);
|
||||
}
|
||||
|
||||
if (data.length == 128) {
|
||||
(recipient, amount, , ) = abi.decode(data, (address, uint256, address, uint256));
|
||||
return (recipient, amount);
|
||||
}
|
||||
|
||||
return (address(0), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,16 @@ 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 "../tokens/CompliantUSDT.sol";
|
||||
import "../tokens/CompliantUSDC.sol";
|
||||
|
||||
interface ICompliantTokenOwnerControl {
|
||||
function owner() external view returns (address);
|
||||
function pause() external;
|
||||
function unpause() external;
|
||||
function transferOwnership(address newOwner) external;
|
||||
function mint(address to, uint256 amount) external;
|
||||
function burn(uint256 amount) external;
|
||||
function totalSupply() external view returns (uint256);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title StablecoinReserveVault
|
||||
@@ -29,8 +37,8 @@ contract StablecoinReserveVault is AccessControl, ReentrancyGuard {
|
||||
address public immutable officialUSDC;
|
||||
|
||||
// Compliant token contracts (on Chain 138 or same network)
|
||||
CompliantUSDT public immutable compliantUSDT;
|
||||
CompliantUSDC public immutable compliantUSDC;
|
||||
ICompliantTokenOwnerControl public immutable compliantUSDT;
|
||||
ICompliantTokenOwnerControl public immutable compliantUSDC;
|
||||
|
||||
// Reserve tracking
|
||||
uint256 public usdtReserveBalance;
|
||||
@@ -47,6 +55,9 @@ contract StablecoinReserveVault is AccessControl, ReentrancyGuard {
|
||||
event ReserveWithdrawn(address indexed token, uint256 amount, address indexed recipient);
|
||||
event CompliantTokensMinted(address indexed token, uint256 amount, address indexed recipient);
|
||||
event CompliantTokensBurned(address indexed token, uint256 amount, address indexed redeemer);
|
||||
event CompliantTokenPaused(address indexed token, address indexed operator);
|
||||
event CompliantTokenUnpaused(address indexed token, address indexed operator);
|
||||
event CompliantTokenOwnershipTransferred(address indexed token, address indexed newOwner);
|
||||
event Paused(address indexed account);
|
||||
event Unpaused(address indexed account);
|
||||
|
||||
@@ -82,8 +93,8 @@ contract StablecoinReserveVault is AccessControl, ReentrancyGuard {
|
||||
|
||||
officialUSDT = officialUSDT_;
|
||||
officialUSDC = officialUSDC_;
|
||||
compliantUSDT = CompliantUSDT(compliantUSDT_);
|
||||
compliantUSDC = CompliantUSDC(compliantUSDC_);
|
||||
compliantUSDT = ICompliantTokenOwnerControl(compliantUSDT_);
|
||||
compliantUSDC = ICompliantTokenOwnerControl(compliantUSDC_);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,6 +119,19 @@ contract StablecoinReserveVault is AccessControl, ReentrancyGuard {
|
||||
emit CompliantTokensMinted(address(compliantUSDT), amount, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Seed official USDT reserves without minting new cUSDT
|
||||
* @dev Used to retrofit backing for pre-existing canonical supply
|
||||
*/
|
||||
function seedUSDTReserve(uint256 amount) external onlyRole(RESERVE_OPERATOR_ROLE) nonReentrant {
|
||||
require(amount > 0, "StablecoinReserveVault: zero amount");
|
||||
|
||||
IERC20(officialUSDT).safeTransferFrom(msg.sender, address(this), amount);
|
||||
usdtReserveBalance += amount;
|
||||
|
||||
emit ReserveDeposited(officialUSDT, amount, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Deposit official USDC and mint cUSDC 1:1
|
||||
* @dev Transfers USDC from caller, mints cUSDC to caller
|
||||
@@ -130,6 +154,19 @@ contract StablecoinReserveVault is AccessControl, ReentrancyGuard {
|
||||
emit CompliantTokensMinted(address(compliantUSDC), amount, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Seed official USDC reserves without minting new cUSDC
|
||||
* @dev Used to retrofit backing for pre-existing canonical supply
|
||||
*/
|
||||
function seedUSDCReserve(uint256 amount) external onlyRole(RESERVE_OPERATOR_ROLE) nonReentrant {
|
||||
require(amount > 0, "StablecoinReserveVault: zero amount");
|
||||
|
||||
IERC20(officialUSDC).safeTransferFrom(msg.sender, address(this), amount);
|
||||
usdcReserveBalance += amount;
|
||||
|
||||
emit ReserveDeposited(officialUSDC, amount, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Redeem cUSDT for official USDT 1:1
|
||||
* @dev Burns cUSDT from caller, transfers USDT to caller
|
||||
@@ -139,7 +176,8 @@ contract StablecoinReserveVault is AccessControl, ReentrancyGuard {
|
||||
require(amount > 0, "StablecoinReserveVault: zero amount");
|
||||
require(usdtReserveBalance >= amount, "StablecoinReserveVault: insufficient reserve");
|
||||
|
||||
// Burn cUSDT from caller
|
||||
// Pull cUSDT from the redeemer, then burn from the vault balance as token owner.
|
||||
IERC20(address(compliantUSDT)).safeTransferFrom(msg.sender, address(this), amount);
|
||||
compliantUSDT.burn(amount);
|
||||
|
||||
// Update reserve
|
||||
@@ -162,7 +200,8 @@ contract StablecoinReserveVault is AccessControl, ReentrancyGuard {
|
||||
require(amount > 0, "StablecoinReserveVault: zero amount");
|
||||
require(usdcReserveBalance >= amount, "StablecoinReserveVault: insufficient reserve");
|
||||
|
||||
// Burn cUSDC from caller
|
||||
// Pull cUSDC from the redeemer, then burn from the vault balance as token owner.
|
||||
IERC20(address(compliantUSDC)).safeTransferFrom(msg.sender, address(this), amount);
|
||||
compliantUSDC.burn(amount);
|
||||
|
||||
// Update reserve
|
||||
@@ -232,6 +271,37 @@ contract StablecoinReserveVault is AccessControl, ReentrancyGuard {
|
||||
emit Unpaused(msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Pause one of the compliant tokens currently owned by the vault
|
||||
*/
|
||||
function pauseCompliantToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
_requireSupportedCompliantToken(token);
|
||||
ICompliantTokenOwnerControl(token).pause();
|
||||
emit CompliantTokenPaused(token, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unpause one of the compliant tokens currently owned by the vault
|
||||
*/
|
||||
function unpauseCompliantToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
_requireSupportedCompliantToken(token);
|
||||
ICompliantTokenOwnerControl(token).unpause();
|
||||
emit CompliantTokenUnpaused(token, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Move compliant token ownership away from the vault if governance needs to recover control
|
||||
*/
|
||||
function transferCompliantTokenOwnership(
|
||||
address token,
|
||||
address newOwner
|
||||
) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
require(newOwner != address(0), "StablecoinReserveVault: zero new owner");
|
||||
_requireSupportedCompliantToken(token);
|
||||
ICompliantTokenOwnerControl(token).transferOwnership(newOwner);
|
||||
emit CompliantTokenOwnershipTransferred(token, newOwner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Emergency withdrawal (admin only, after pause)
|
||||
* @dev Can be used to recover funds in emergency situations
|
||||
@@ -249,5 +319,11 @@ contract StablecoinReserveVault is AccessControl, ReentrancyGuard {
|
||||
require(paused, "StablecoinReserveVault: not paused");
|
||||
_;
|
||||
}
|
||||
}
|
||||
|
||||
function _requireSupportedCompliantToken(address token) internal view {
|
||||
require(
|
||||
token == address(compliantUSDT) || token == address(compliantUSDC),
|
||||
"StablecoinReserveVault: unsupported compliant token"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "../registry/UniversalAssetRegistry.sol";
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "./CompliantMonetaryUnitToken.sol";
|
||||
|
||||
/**
|
||||
* @title CompliantBTC
|
||||
* @notice Canonical Chain 138 Bitcoin monetary-unit token.
|
||||
*/
|
||||
contract CompliantBTC is CompliantMonetaryUnitToken {
|
||||
uint8 public constant SATOSHI_DECIMALS = 8;
|
||||
|
||||
constructor(
|
||||
address initialOwner,
|
||||
address admin,
|
||||
uint256 initialSupply
|
||||
)
|
||||
CompliantMonetaryUnitToken(
|
||||
"Bitcoin (Compliant)",
|
||||
"cBTC",
|
||||
SATOSHI_DECIMALS,
|
||||
"BTC",
|
||||
initialOwner,
|
||||
admin,
|
||||
initialSupply
|
||||
)
|
||||
{}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/Pausable.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../compliance/LegallyCompliantBase.sol";
|
||||
|
||||
/**
|
||||
* @title CompliantMonetaryUnitToken
|
||||
* @notice Generic GRU monetary-unit token for non-ISO units such as BTC.
|
||||
* @dev Mirrors the compliant fiat token controls while keeping monetary-unit metadata separate
|
||||
* from ISO-4217 and commodity classifications.
|
||||
*/
|
||||
contract CompliantMonetaryUnitToken is ERC20, Pausable, Ownable, LegallyCompliantBase {
|
||||
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
||||
|
||||
uint8 private immutable _decimalsStorage;
|
||||
string private _unitCode;
|
||||
|
||||
constructor(
|
||||
string memory name_,
|
||||
string memory symbol_,
|
||||
uint8 decimals_,
|
||||
string memory unitCode_,
|
||||
address initialOwner,
|
||||
address admin,
|
||||
uint256 initialSupply
|
||||
)
|
||||
ERC20(name_, symbol_)
|
||||
Ownable(initialOwner)
|
||||
LegallyCompliantBase(admin)
|
||||
{
|
||||
_decimalsStorage = decimals_;
|
||||
_unitCode = unitCode_;
|
||||
_grantRole(MINTER_ROLE, initialOwner);
|
||||
if (initialSupply > 0) {
|
||||
_mint(msg.sender, initialSupply);
|
||||
}
|
||||
}
|
||||
|
||||
function decimals() public view override returns (uint8) {
|
||||
return _decimalsStorage;
|
||||
}
|
||||
|
||||
function unitCode() external view returns (string memory) {
|
||||
return _unitCode;
|
||||
}
|
||||
|
||||
function isMonetaryUnit() external pure returns (bool) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function _update(
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount
|
||||
) internal override whenNotPaused {
|
||||
super._update(from, to, amount);
|
||||
if (from != address(0) && to != address(0)) {
|
||||
bytes32 legalRefHash = _generateLegalReferenceHash(
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
abi.encodePacked(symbol(), " Transfer")
|
||||
);
|
||||
emit ValueTransferDeclared(from, to, amount, legalRefHash);
|
||||
}
|
||||
}
|
||||
|
||||
function pause() public onlyOwner {
|
||||
_pause();
|
||||
}
|
||||
|
||||
function unpause() public onlyOwner {
|
||||
_unpause();
|
||||
}
|
||||
|
||||
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
|
||||
_mint(to, amount);
|
||||
}
|
||||
|
||||
function burn(uint256 amount) public {
|
||||
_burn(msg.sender, amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "./CompliantFiatTokenV2.sol";
|
||||
|
||||
/**
|
||||
* @title CompliantUSDCTokenV2
|
||||
* @notice Thin cUSDC V2 specialization over the shared compliant fiat token base.
|
||||
*/
|
||||
contract CompliantUSDCTokenV2 is CompliantFiatTokenV2 {
|
||||
constructor(
|
||||
address initialOperator,
|
||||
address admin,
|
||||
uint256 initialSupply,
|
||||
bool forwardCanonical_
|
||||
)
|
||||
CompliantFiatTokenV2(
|
||||
"USD Coin (Compliant V2)",
|
||||
"cUSDC",
|
||||
6,
|
||||
"USD",
|
||||
"2",
|
||||
initialOperator,
|
||||
admin,
|
||||
initialSupply,
|
||||
forwardCanonical_
|
||||
)
|
||||
{}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "./CompliantFiatTokenV2.sol";
|
||||
|
||||
/**
|
||||
* @title CompliantUSDTTokenV2
|
||||
* @notice Thin cUSDT V2 specialization over the shared compliant fiat token base.
|
||||
*/
|
||||
contract CompliantUSDTTokenV2 is CompliantFiatTokenV2 {
|
||||
constructor(
|
||||
address initialOperator,
|
||||
address admin,
|
||||
uint256 initialSupply,
|
||||
bool forwardCanonical_
|
||||
)
|
||||
CompliantFiatTokenV2(
|
||||
"Tether USD (Compliant V2)",
|
||||
"cUSDT",
|
||||
6,
|
||||
"USD",
|
||||
"2",
|
||||
initialOperator,
|
||||
admin,
|
||||
initialSupply,
|
||||
forwardCanonical_
|
||||
)
|
||||
{}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "../../emoney/interfaces/IeMoneyToken.sol";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../../vendor/openzeppelin/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "../../emoney/interfaces/IeMoneyToken.sol";
|
||||
|
||||
|
||||
@@ -112,6 +112,8 @@ After deployment, add the AddressMapper address to `.env`:
|
||||
ADDRESS_MAPPER=0x...
|
||||
```
|
||||
|
||||
On **Chain 138**, the canonical mapper is documented in the parent repo at `docs/11-references/ADDRESS_MATRIX_AND_STATUS.md` (`0x439Fcb2d2ab2f890DCcAE50461Fa7d978F9Ffe1A`); inventory also lists `ADDRESS_MAPPER_LEGACY_DUPLICATE` in `config/address-inventory.chain138.json`. Prefer the canonical address in `.env`.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use deployed addresses** for contract interactions
|
||||
|
||||
@@ -156,7 +156,7 @@ forge script script/dex/DeployPrivatePoolRegistryAndPools.s.sol:DeployPrivatePoo
|
||||
|
||||
### PMM (Chain 138) completion checklist
|
||||
|
||||
1. Set **DODO_VENDING_MACHINE_ADDRESS** in `.env` to the DODO DVM factory address on Chain 138. **Chain 138 is not in DODO’s official list** — see [DVM_DEPLOYMENT_CHECK.md](DVM_DEPLOYMENT_CHECK.md). Deploy the factory yourself from [DODOEX](https://github.com/DODOEX) or use an existing deployment if available.
|
||||
1. Set **DODO_VENDING_MACHINE_ADDRESS** in `.env` to the DODO DVM factory adapter address on Chain 138. **Chain 138 is not in DODO’s official list** — see [DVM_DEPLOYMENT_CHECK.md](DVM_DEPLOYMENT_CHECK.md). Use `scripts/deployment/deploy-official-dvm-chain138.sh` to deploy the official DVM `0.6.9` stack from the vendored DODO tree and then deploy the local adapter.
|
||||
2. Set **OFFICIAL_USDT_ADDRESS** and **OFFICIAL_USDC_ADDRESS** to the live Chain 138 quote-side mirror stables:
|
||||
- `OFFICIAL_USDT_ADDRESS=0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1`
|
||||
- `OFFICIAL_USDC_ADDRESS=0x71D6687F38b93CCad569Fa6352c876eea967201b`
|
||||
@@ -169,7 +169,7 @@ forge script script/dex/DeployPrivatePoolRegistryAndPools.s.sol:DeployPrivatePoo
|
||||
|
||||
| Chain ID | Network | Mapper (AddressMapperEmpty) |
|
||||
|----------|-----------|----------------------------------|
|
||||
| 138 | Chain 138 | 0xe48E3f248698610e18Db865457fcd935Bb3da856 (AddressMapper) |
|
||||
| 138 | Chain 138 | 0x439Fcb2d2ab2f890DCcAE50461Fa7d978F9Ffe1A (AddressMapper; legacy dup `0xe48E3f248698610e18Db865457fcd935Bb3da856`) |
|
||||
| 1 | Ethereum | 0x0ea68F5B5A8427bB58e54ECcee941F543Dc538c5 |
|
||||
| 56 | BSC | 0x6e94e53F73893b2a6784Df663920D31043A6dE07 |
|
||||
| 137 | Polygon | 0xb689c1C69DAa08DEb5D8feA2aBF0F64bFD409727 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Deployed Smart Contracts — Cross-Network Overview
|
||||
|
||||
> Historical note (2026-03-26): this overview includes superseded PMM stack references from earlier deployment phases. The current canonical Chain 138 PMM stack is `DODOPMMIntegration=0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d` and `DODOPMMProvider=0x5CAe6Ce155b7f08D3a956F5Dc82fC9945f29B381`.
|
||||
> Historical note (2026-04-01): this overview preserves earlier deployment phases, but the live canonical Chain 138 PMM stable stack is now `DODOPMMIntegration=0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895` and `DODOPMMProvider=0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e`.
|
||||
|
||||
**Last updated:** 2026-02-20
|
||||
**Source:** `smom-dbis-138/.env` and deployment runbooks.
|
||||
@@ -64,8 +64,9 @@ flowchart TB
|
||||
| | CCIPWETH10 Bridge | `0xe0E93247376aa097dB308B92e6Ba36bA015535D0` | WETH10 cross-chain |
|
||||
| | CCIP Router | `0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817` | CCIP router (on 138) |
|
||||
| | CCIPTxReporter | `0x3F88b662F04d9B1413BA8d65bFC229e830D7d077` | Report txs to mainnet logger |
|
||||
| **PMM** | Mock DVM Factory | `0xB16c3D48A111714B1795E58341FeFDd643Ab01ab` | Create mock pools |
|
||||
| | DODOPMMIntegration | `0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d` | Canonical corrected PMM integration |
|
||||
| **PMM** | DODO V2 DVMFactory | `0xc93870594C7f83A0aE076c2e30b494Efc526b68E` | Create canonical DVM-backed stable pools |
|
||||
| | DODOPMMIntegration | `0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895` | Canonical official-DVM-backed PMM integration |
|
||||
| | DODOPMMProvider | `0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e` | Canonical stable-pool routing provider |
|
||||
| **Core eMoney** | Compliance Registry, Token Factory, Bridge Vault, Debt Registry, Policy Manager, Token Impl | (see .env) | Core system |
|
||||
| **Channels** | Payment Channel Manager, Address Mapper, Mirror Manager | (see .env) | State channels |
|
||||
| **Oracle** | Oracle Aggregator, Oracle Proxy | (see .env) | Price feeds |
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
# DVM (DODO Vending Machine) Deployment Check — Chain 138
|
||||
|
||||
**Date:** 2026-02-20
|
||||
**Date:** 2026-04-02
|
||||
|
||||
## Summary
|
||||
|
||||
**There is no official DODO DVM factory deployment on Chain 138.**
|
||||
**Chain 138 now has a self-deployed official DODO V2 DVM stack, but it is not DODO-listed.**
|
||||
|
||||
- DODO’s official contract list ([api.dodoex.io/dodo-contract/list?version=v1,v2](https://api.dodoex.io/dodo-contract/list?version=v1,v2)) includes chains such as **1** (Ethereum), **5** (Goerli), **10** (Optimism), **56** (BSC), **133** (Syscoin), **137** (Polygon), **8453** (Base), etc.
|
||||
- **Chain ID 138 is not in that list**, so there is no canonical DVM factory address for Chain 138 from DODO.
|
||||
- DODO’s official contract list ([api.dodoex.io/dodo-contract/list?version=v1,v2](https://api.dodoex.io/dodo-contract/list?version=v1,v2)) includes chains such as **1** (Ethereum), **5** (Goerli), **10** (Optimism), **56** (BSC), **133** (Syscoin), **137** (Polygon), **8453** (Base), etc.
|
||||
- **Chain ID 138 is still not in that list**, so there is no DODO-published canonical DVM factory address for Chain 138.
|
||||
- This repo now operates a **self-deployed official DODO V2 DVM stack** on Chain 138:
|
||||
- **DVMFactory:** `0xc93870594C7f83A0aE076c2e30b494Efc526b68E`
|
||||
- **DVMFactoryAdapter / DODO_VENDING_MACHINE_ADDRESS:** `0xb6D9EF3575bc48De3f011C310DC24d87bEC6087C`
|
||||
- **DODOPMMIntegration:** `0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895`
|
||||
- **DODOPMMProvider:** `0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e`
|
||||
- **Stable pools (canonical corrected stack):**
|
||||
- `cUSDT/cUSDC`: `0x9e89bAe009adf128782E19e8341996c596ac40dC`
|
||||
- `cUSDT/USDT`: `0x866Cb44b59303d8dc5f4F9E3E7A8e8b0bf238d66`
|
||||
- `cUSDC/USDC`: `0xc39B7D0F40838cbFb54649d327f49a6DAC964062`
|
||||
|
||||
## Implications
|
||||
|
||||
- **`DODO_VENDING_MACHINE_ADDRESS`** in `.env` must point to a DVM factory **you deploy or obtain** on Chain 138; it will not be an “official” DODO-listed address.
|
||||
- Until that address is set, **PMM (run-pmm-and-pools.sh)** and **DODOPMMIntegration** cannot be used on Chain 138.
|
||||
- **`DODO_VENDING_MACHINE_ADDRESS`** in `.env` must point to a Chain 138 factory adapter **you deploy or obtain**; it is not a DODO-listed public address.
|
||||
- The canonical Chain 138 PMM path is now the **official DODO V2 DVMFactoryAdapter** above, not the earlier mock DVM path.
|
||||
|
||||
## Options
|
||||
|
||||
0. **Use mock DVM (this repo)**
|
||||
0. **Use mock DVM (this repo, fallback only)**
|
||||
- A minimal `MockDVMFactory` (and `MockDVMPool`) is provided so PMM can run on Chain 138 without official DODO. Deploy with:
|
||||
- `forge script script/dex/DeployMockDVMFactory.s.sol:DeployMockDVMFactory --rpc-url "$RPC_URL_138" --broadcast --private-key "$PRIVATE_KEY" --legacy`
|
||||
- Set `DODO_VENDING_MACHINE_ADDRESS` in `.env` to the deployed address, then run `scripts/deployment/run-pmm-and-pools.sh`. Pools created are mocks (no real AMM); swap views return stubs.
|
||||
- **Do not treat this as canonical anymore** unless you are intentionally testing without the official DVM stack.
|
||||
|
||||
1. **Deploy official DODO DVM on Chain 138 (this repo)**
|
||||
- Submodule **lib/dodo-contractV2** (DODOEX/contractV2) is used to deploy the official DVM stack via Truffle. Our **DVMFactoryAdapter** wraps `createDODOVendingMachine` as `createDVM` so `DODOPMMIntegration` works unchanged.
|
||||
- **Blocker:** DODO contractV2 mixes Solidity 0.6.9 (DVM, Factory) and 0.8.16 (DODOGasSavingPool, SmartRoute). Truffle compiles the whole repo with one solc version, so `truffle compile` fails. Until DODO adds multi-compiler support or we vendor only the DVM 0.6.9 tree, use the **mock DVM** (option 0) or deploy from a DODO fork that compiles.
|
||||
- **One-shot (when Truffle compiles):** `scripts/deployment/deploy-official-dvm-chain138.sh`
|
||||
- The repo deployment helper now runs the vendored DODO tree in a **DVM-only compile mode** by temporarily hiding unrelated Solidity `0.8.x` contracts during `truffle compile` / `truffle migrate`, then restoring them on exit. That allows the official DVM `0.6.9` stack to compile without forking the upstream repo.
|
||||
- **One-shot:** `scripts/deployment/deploy-official-dvm-chain138.sh`
|
||||
- To verify compile before broadcast: `scripts/deployment/deploy-official-dvm-chain138.sh --compile-only`
|
||||
- Requires `lib/dodo-contractV2` deps: `cd lib/dodo-contractV2 && npm install` (if npm hits a registry/cert error, use `npm install --registry https://registry.npmjs.org/ --no-package-lock`).
|
||||
- **Manual:**
|
||||
1) In `lib/dodo-contractV2`: set `privKey` and `RPC_URL_138` (e.g. from parent `.env`), then `npx truffle migrate -f 1 --to 1 --network chain138` and `npx truffle migrate -f 9 --to 9 --network chain138`.
|
||||
@@ -32,6 +43,8 @@
|
||||
`forge script script/dex/DeployDVMFactoryAdapter.s.sol:DeployDVMFactoryAdapter --rpc-url "$RPC_URL_138" --broadcast --private-key "$PRIVATE_KEY" --legacy`
|
||||
3) Set `DODO_VENDING_MACHINE_ADDRESS` in `.env` to the **DVMFactoryAdapter** address (not the DVMFactory address).
|
||||
- Then run `scripts/deployment/run-pmm-and-pools.sh` (you may need to re-deploy DODOPMMIntegration if it was previously deployed with the mock DVM).
|
||||
- **Current live Chain 138 result:** the repo has already deployed this path and currently uses the addresses listed in the Summary section above.
|
||||
- **Stable-pair calibration note:** for equal-reserve stable pairs on the official DODO V2 DVM stack, the current canonical Chain 138 pools use `i = 1e18`, `k = 0`, `lpFeeRate = 10 bps`, `isOpenTWAP = false`. Earlier `k = 0.5e18` pools were removed because they priced above parity.
|
||||
|
||||
2. **Deploy DVM factory yourself from DODO source (outside this repo)**
|
||||
- DODO’s contracts are open source: [DODOEX GitHub](https://github.com/DODOEX) (contractV2).
|
||||
|
||||
@@ -197,17 +197,17 @@ chainRegistry.registerEVMChain(
|
||||
|
||||
---
|
||||
|
||||
### **Hyperledger Firefly**
|
||||
### **Hyperledger FireFly**
|
||||
|
||||
**Status**: ✅ Adapter + Service Client Created
|
||||
|
||||
**Infrastructure**: ✅ VMIDs 6202, 6203 deployed
|
||||
**Infrastructure**: `6200` is the only validated FireFly runtime, `6201` is retired / standby, and `6202` / `6203` are not deployed in the current cluster.
|
||||
|
||||
**Deployment Steps**:
|
||||
|
||||
1. Initialize Firefly on VMID 6202:
|
||||
1. Initialize FireFly only after confirming `6200` is healthy and intentionally in scope:
|
||||
```bash
|
||||
ssh root@192.168.11.175
|
||||
ssh root@192.168.11.35
|
||||
ff init alltra-bridge --multiparty
|
||||
```
|
||||
|
||||
@@ -237,21 +237,26 @@ namespaces:
|
||||
- erc20_erc721
|
||||
```
|
||||
|
||||
For current runtime truth, prefer [docs/03-deployment/DBIS_HYPERLEDGER_RUNTIME_STATUS.md](../../../docs/03-deployment/DBIS_HYPERLEDGER_RUNTIME_STATUS.md).
|
||||
|
||||
---
|
||||
|
||||
## 🔧 **Hyperledger Integration**
|
||||
|
||||
### **Cacti (VMID 5201)**
|
||||
### **Cacti (VMIDs 5200 / 5201 / 5202)**
|
||||
|
||||
**Status**: ⚠️ Needs Adapter
|
||||
|
||||
**Infrastructure**: `5200` is the primary Cacti / Besu connector node. `5201` and `5202` are the live Alltra/HYBX public Cacti surfaces and each also exposes a local Hyperledger Cacti API on `:4000`.
|
||||
|
||||
**Deployment Steps**:
|
||||
|
||||
1. Install Cacti API server on VMID 5201
|
||||
2. Configure Besu connector plugin
|
||||
3. Configure Fabric connector plugin (if needed)
|
||||
4. Deploy CactiAdapter contract
|
||||
5. Create Cacti client service
|
||||
1. Maintain the core connector deployment on VMID `5200`.
|
||||
2. Keep VMIDs `5201` and `5202` healthy for the public Alltra/HYBX Cacti surfaces and local `:4000` APIs.
|
||||
3. Configure the Besu connector plugin.
|
||||
4. Configure the Fabric connector plugin if needed.
|
||||
5. Deploy the `CactiAdapter` contract.
|
||||
6. Create the Cacti client service.
|
||||
|
||||
**Cacti Configuration**:
|
||||
```json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Next Steps Completion Summary
|
||||
|
||||
> Historical note (2026-03-26): this completion summary preserves an earlier PMM deployment phase. The current canonical Chain 138 PMM stack is `DODOPMMIntegration=0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d` and `DODOPMMProvider=0x5CAe6Ce155b7f08D3a956F5Dc82fC9945f29B381`.
|
||||
> Historical note (2026-04-01): this completion summary preserves an earlier PMM deployment phase. The live canonical Chain 138 PMM stable stack is now `DODOPMMIntegration=0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895` and `DODOPMMProvider=0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e`.
|
||||
|
||||
**Date:** 2026-02-20
|
||||
|
||||
@@ -21,7 +21,7 @@ This document summarizes what was completed in the "next steps" pass and what re
|
||||
| **Verify script** | Optional checks (CCIPTxReporter, genesis) are warnings; log_* fallbacks; safe unset-var handling. |
|
||||
| **Hardhat / Forge** | @emoney/interfaces replaced with relative imports; Hardhat viaIR for 0.8.22; Forge build available. |
|
||||
| **Docs** | WARNINGS_AND_OPTIONAL_TASKS.md, DVM_DEPLOYMENT_CHECK.md, ALL_MAINNETS_DEPLOYMENT_RUNBOOK (PMM step → DVM doc), TODO_TASK_LIST_MASTER §10b updated. |
|
||||
| **PMM (Chain 138)** | Earlier Mock DVM phase completed; current canonical PMM integration is `0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d` and supersedes the initial stack. `DODO_VENDING_MACHINE_ADDRESS` and `DODO_PMM_INTEGRATION` were set in .env during the first deployment phase. |
|
||||
| **PMM (Chain 138)** | Earlier mock-DVM phase completed; the live canonical stable stack is now `0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895` with provider `0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e`. `DODO_VENDING_MACHINE_ADDRESS` and `DODO_PMM_INTEGRATION` should follow the current DVM-backed deployment in `.env`. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -47,6 +47,17 @@ The Master Plan specifies four **public** liquidity pool types for user routing
|
||||
|
||||
For cUSDT/XAU, cUSDC/XAU, and cEURT/XAU: use the deployed Chain 138 XAU anchor token (`cXAUC` at `0x290E52a8819A4fbD0714E517225429aA2B70EC6b` or `cXAUT` at `0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E`), then call `createPool(baseToken, quoteToken, lpFeeRate, initialPrice, k, isOpenTWAP)` with `POOL_MANAGER_ROLE`. **Unit:** 1 full XAU token = **1 troy ounce** of gold (`10^6` base units with 6 decimals). Public pools serve user routing, price discovery, and flash loan access; they do not serve as the primary stabilization engine (see Master Plan private mesh).
|
||||
|
||||
### Planned `cUSDW / cUSDC_V2` hub pool
|
||||
|
||||
For the repo-native D-WIN stabilization path on Chain 138, the generic `createPool` flow is also the correct path for `cUSDW / cUSDC_V2`.
|
||||
|
||||
- `baseToken`: `cUSDW` (`0xcA6BFa614935f1AB71c9aB106bAA6FBB6057095e`)
|
||||
- `quoteToken`: `cUSDC_V2` (`0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d`)
|
||||
- decimals: both `6`
|
||||
- script support: `script/dex/CreateCUSDWCUSDCV2Pool.s.sol` and `script/dex/AddLiquidityCUSDWCUSDCV2PoolChain138.s.sol`
|
||||
|
||||
See [docs/03-deployment/CUSDW_CUSDC_V2_HUB_POOL_RUNBOOK.md](../../../docs/03-deployment/CUSDW_CUSDC_V2_HUB_POOL_RUNBOOK.md) for the full operator sequence.
|
||||
|
||||
---
|
||||
|
||||
## DODO PMM Overview
|
||||
|
||||
@@ -57,11 +57,17 @@ References: [DEPLOYMENT_COMPLETE_GUIDE.md](../deployment/DEPLOYMENT_COMPLETE_GUI
|
||||
| `KEEPER_PRIVATE_KEY` | Wallet with **KEEPER_ROLE** on `PriceFeedKeeper` for `performUpkeep` txs. |
|
||||
| `MESH_WETH_WRAP_WEI` | Optional tiny `WETH.deposit{value}` on a throttled cadence (`MESH_WETH_WRAP_EVERY_N`); costs gas—default **0** (off). |
|
||||
|
||||
**Dedicated keeper signer:** generate and store a separate keeper key with
|
||||
`scripts/deployment/generate-chain138-keeper-key.sh`.
|
||||
By default it writes the secret to `~/.secure-secrets/chain138-keeper.env` and a local helper
|
||||
to `.env.keeper.local` (gitignored) that sources the secret file.
|
||||
|
||||
**systemd:** `config/systemd/chain138-pmm-mesh-automation.service.example` in the Proxmox repo—copy, fix paths, `enable --now`.
|
||||
|
||||
**Bring-up checklist (operator):**
|
||||
1. `PRICE_FEED_KEEPER_ADDRESS` in `smom-dbis-138/.env` (Chain 138: `0xD3AD6831aacB5386B8A25BB8D8176a6C8a026f04` per explorer docs—verify live).
|
||||
2. `KEEPER_PRIVATE_KEY` optional if same as `PRIVATE_KEY` (must have `KEEPER_ROLE` on the keeper).
|
||||
2. `KEEPER_PRIVATE_KEY` should be a dedicated keeper signer with `KEEPER_ROLE` on the keeper.
|
||||
The mesh now skips keeper writes cleanly when no dedicated keeper key is present.
|
||||
3. `./scripts/reserve/set-price-feed-keeper-interval.sh 6` once if the keeper still used a 30s interval.
|
||||
4. Oracle: `scripts/update-oracle-price.sh` needs Besu-safe paths (eth_call + explicit `--gas-limit`); set `AGGREGATOR_ADDRESS` / transmitter as deployed.
|
||||
5. If the keeper uses a **WETH MockPriceFeed** (`CHAIN138_WETH_MOCK_PRICE_FEED`), run `scripts/reserve/sync-weth-mock-price.sh` on a schedule (or from the same cron as the mesh) so ReserveSystem WETH tracks the market.
|
||||
|
||||
+40
-9
@@ -5,24 +5,55 @@
|
||||
# Or copy the needed lines into .env manually. Do NOT commit .env.
|
||||
|
||||
# DODO PMM Provider (deployed); required for liquidity/quoting scripts
|
||||
DODO_PMM_PROVIDER_ADDRESS=0x5CAe6Ce155b7f08D3a956F5Dc82fC9945f29B381
|
||||
DODO_PMM_PROVIDER_ADDRESS=0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e
|
||||
|
||||
# DODO PMM Integration (Chain 138)
|
||||
DODO_PMM_INTEGRATION_ADDRESS=0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d
|
||||
# Official DODO V2 DVM stack (Chain 138)
|
||||
DODO_DVM_FACTORY=0xc93870594C7f83A0aE076c2e30b494Efc526b68E
|
||||
DODO_VENDING_MACHINE_ADDRESS=0xb6D9EF3575bc48De3f011C310DC24d87bEC6087C
|
||||
DODO_PMM_INTEGRATION_ADDRESS=0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895
|
||||
|
||||
# Local quote-side mirror stables (Chain 138); optional — see deploy runbooks
|
||||
OFFICIAL_USDT_ADDRESS=0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1
|
||||
OFFICIAL_USDC_ADDRESS=0x71D6687F38b93CCad569Fa6352c876eea967201b
|
||||
|
||||
# PMM pool addresses (optional; RegisterDODOPools reads from integration if unset)
|
||||
# Canonical current cUSDT/USDT pool is the funded pool returned by the live
|
||||
# Chain 138 integration/provider mapping.
|
||||
POOL_CUSDTCUSDC=0x9fcB06Aa1FD5215DC0E91Fd098aeff4B62fEa5C8
|
||||
POOL_CUSDTUSDT=0x6fc60DEDc92a2047062294488539992710b99D71
|
||||
POOL_CUSDCUSDC=0x90bd9Bf18Daa26Af3e814ea224032d015db58Ea5
|
||||
# Canonical current stable pools were recalibrated on 2026-04-02 with i=1e18 and k=0
|
||||
# so equal-reserve stable pairs quote at true 1:1 on the official DODO V2 DVM stack.
|
||||
POOL_CUSDTCUSDC=0x9e89bAe009adf128782E19e8341996c596ac40dC
|
||||
POOL_CUSDTUSDT=0x866Cb44b59303d8dc5f4F9E3E7A8e8b0bf238d66
|
||||
POOL_CUSDCUSDC=0xc39B7D0F40838cbFb54649d327f49a6DAC964062
|
||||
|
||||
# Canonical stable-pool creation parameters for Chain 138 official DVM
|
||||
DODO_LP_FEE_BPS=10
|
||||
DODO_INITIAL_PRICE_1E18=1000000000000000000
|
||||
DODO_K_FACTOR_1E18=0
|
||||
DODO_ENABLE_TWAP=false
|
||||
|
||||
# Token-aggregation (quote API): same integration address so indexer can index DODO pools
|
||||
CHAIN_138_DODO_PMM_INTEGRATION=0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d
|
||||
CHAIN_138_DODO_PMM_INTEGRATION=0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895
|
||||
|
||||
# Optional USDW public-wrap surfaces (BSC / Polygon) for cWUSDW rollout
|
||||
CWUSDW_ADDRESS_56=0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55
|
||||
# CWUSDW_ADDRESS_137=0x...
|
||||
USDW_NATIVE_ADDRESS_56=0xed75ad08f416d4e53e4d45dd5140a4c8b84f39fb
|
||||
USDW_NATIVE_ADDRESS_137=0x3deb0c60f0be9d9b99da83a2b6b2ee790f5af37a
|
||||
# USDW_WRAP_VAULT_56=0x...
|
||||
# USDW_WRAP_VAULT_137=0x...
|
||||
|
||||
# Planned ALL Mainnet AUSDT -> cWAUSDT -> cAUSDT corridor
|
||||
AUSDT_ADDRESS_651940=0x015B1897Ed5279930bC2Be46F661894d219292A6
|
||||
CAUSDT_ADDRESS_138=0x5fdDF65733e3d590463F68f93Cf16E8c04081271
|
||||
CWAUSDT_ADDRESS_56=0xe1a51Bc037a79AB36767561B147eb41780124934
|
||||
CWAUSDT_ADDRESS_137=0xf12e262F85107df26741726b074606CaFa24AAe7
|
||||
CWAUSDT_ADDRESS_43114=0xff3084410A732231472Ee9f93F5855dA89CC5254
|
||||
CWAUSDT_ADDRESS_42220=0xC158b6cD3A3088C52F797D41f5Aa02825361629e
|
||||
|
||||
# Planned ALL Mainnet gold corridor
|
||||
# 138 cXAUC/cXAUT -> source-leg cWXAUC/cWXAUT -> 651940 cWAXAUC/cWAXAUT -> 651940 cAXAUC/cAXAUT
|
||||
# CAXAUC_ADDRESS_651940=0x...
|
||||
# CAXAUT_ADDRESS_651940=0x...
|
||||
# CWAXAUC_ADDRESS_651940=0x...
|
||||
# CWAXAUT_ADDRESS_651940=0x...
|
||||
|
||||
# Add liquidity (run-pmm-full-parity or AddLiquidityPMMPoolsChain138.s.sol); amounts in token units (6 decimals, e.g. 1000000e6 = 1M)
|
||||
# ADD_LIQUIDITY_BASE_AMOUNT=1000000e6
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[profile.default]
|
||||
src = "src"
|
||||
test = "test"
|
||||
out = "out"
|
||||
libs = ["../lib"]
|
||||
solc = "0.8.20"
|
||||
optimizer = true
|
||||
optimizer_runs = 1
|
||||
via_ir = true
|
||||
evm_version = "cancun"
|
||||
allow_paths = [".."]
|
||||
fs_permissions = [
|
||||
{ access = "read", path = "../config" }
|
||||
]
|
||||
remappings = [
|
||||
"@openzeppelin/contracts/=../lib/openzeppelin-contracts/contracts/",
|
||||
"forge-std/=../lib/forge-std/src/",
|
||||
"atomic/=../contracts/bridge/atomic/",
|
||||
"repo-test/=../test/"
|
||||
]
|
||||
@@ -0,0 +1,218 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
interface IAavePoolLike {
|
||||
function flashLoanSimple(
|
||||
address receiverAddress,
|
||||
address asset,
|
||||
uint256 amount,
|
||||
bytes calldata params,
|
||||
uint16 referralCode
|
||||
) external;
|
||||
}
|
||||
|
||||
interface IAaveFlashLoanSimpleReceiver {
|
||||
function executeOperation(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 premium,
|
||||
address initiator,
|
||||
bytes calldata params
|
||||
) external returns (bool);
|
||||
}
|
||||
|
||||
interface IAaveDODOQuotePushSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
interface IAaveExternalUnwinder {
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
interface IAaveAtomicBridgeCoordinator {
|
||||
struct CreateIntentParams {
|
||||
uint64 sourceChain;
|
||||
uint64 destinationChain;
|
||||
address assetIn;
|
||||
address assetOut;
|
||||
uint256 amountIn;
|
||||
uint256 minAmountOut;
|
||||
address recipient;
|
||||
uint256 deadline;
|
||||
bytes32 routeId;
|
||||
}
|
||||
|
||||
function createIntent(CreateIntentParams calldata p) external returns (bytes32 obligationId);
|
||||
|
||||
function submitCommitment(bytes32 obligationId, bytes32 settlementMode) external;
|
||||
|
||||
function obligationEscrow() external view returns (address);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title AaveQuotePushFlashReceiver
|
||||
* @notice Aave V3 flashLoanSimple receiver for the quote-push workflow:
|
||||
* flash borrow quote -> buy PMM base -> unwind base externally -> repay lender, retaining any surplus.
|
||||
*/
|
||||
contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable pool;
|
||||
|
||||
struct QuotePushParams {
|
||||
address integration;
|
||||
address pmmPool;
|
||||
address baseToken;
|
||||
address externalUnwinder;
|
||||
uint256 minOutPmm;
|
||||
uint256 minOutUnwind;
|
||||
bytes unwindData;
|
||||
AtomicBridgeParams atomicBridge;
|
||||
}
|
||||
|
||||
struct AtomicBridgeParams {
|
||||
address coordinator;
|
||||
uint64 sourceChain;
|
||||
uint64 destinationChain;
|
||||
address destinationAsset;
|
||||
uint256 bridgeAmount;
|
||||
uint256 minDestinationAmount;
|
||||
address destinationRecipient;
|
||||
uint256 destinationDeadline;
|
||||
bytes32 routeId;
|
||||
bytes32 settlementMode;
|
||||
bool submitCommitment;
|
||||
}
|
||||
|
||||
error UntrustedPool();
|
||||
error UntrustedInitiator();
|
||||
error BadParams();
|
||||
error InsufficientToRepay();
|
||||
error InvalidAtomicBridge();
|
||||
|
||||
event QuotePushExecuted(
|
||||
address indexed quoteToken,
|
||||
address indexed baseToken,
|
||||
uint256 borrowedAmount,
|
||||
uint256 premium,
|
||||
uint256 baseOut,
|
||||
uint256 unwindOut,
|
||||
uint256 surplus
|
||||
);
|
||||
event AtomicBridgeTriggered(
|
||||
bytes32 indexed obligationId,
|
||||
address indexed coordinator,
|
||||
address indexed destinationRecipient,
|
||||
uint256 bridgeAmount,
|
||||
uint256 minDestinationAmount
|
||||
);
|
||||
|
||||
constructor(address pool_) {
|
||||
pool = pool_;
|
||||
}
|
||||
|
||||
function flashQuotePush(address asset, uint256 amount, QuotePushParams calldata params) external {
|
||||
IAavePoolLike(pool).flashLoanSimple(address(this), asset, amount, abi.encode(address(this), params), 0);
|
||||
}
|
||||
|
||||
function executeOperation(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 premium,
|
||||
address initiator,
|
||||
bytes calldata params
|
||||
) external returns (bool) {
|
||||
if (msg.sender != pool) revert UntrustedPool();
|
||||
(address expectedInitiator, QuotePushParams memory p) = abi.decode(params, (address, QuotePushParams));
|
||||
if (initiator != expectedInitiator) revert UntrustedInitiator();
|
||||
if (
|
||||
p.integration == address(0) || p.pmmPool == address(0) || p.baseToken == address(0)
|
||||
|| p.externalUnwinder == address(0)
|
||||
) revert BadParams();
|
||||
if (p.baseToken == asset) revert BadParams();
|
||||
|
||||
uint256 baseOut = _swapQuoteForBase(asset, amount, p.integration, p.pmmPool, p.minOutPmm);
|
||||
|
||||
uint256 baseBal = IERC20(p.baseToken).balanceOf(address(this));
|
||||
if (p.atomicBridge.coordinator != address(0)) {
|
||||
_triggerAtomicBridge(p.baseToken, baseBal, p.atomicBridge);
|
||||
}
|
||||
|
||||
uint256 unwindOut = _unwindBaseIntoQuote(p.baseToken, asset, p.externalUnwinder, p.minOutUnwind, p.unwindData);
|
||||
uint256 surplus = _approveRepayment(asset, amount + premium);
|
||||
emit QuotePushExecuted(asset, p.baseToken, amount, premium, baseOut, unwindOut, surplus);
|
||||
return true;
|
||||
}
|
||||
|
||||
function _swapQuoteForBase(address asset, uint256 amount, address integration, address pmmPool, uint256 minOutPmm)
|
||||
internal
|
||||
returns (uint256 baseOut)
|
||||
{
|
||||
IERC20(asset).forceApprove(integration, amount);
|
||||
baseOut = IAaveDODOQuotePushSwapExactIn(integration).swapExactIn(pmmPool, asset, amount, minOutPmm);
|
||||
}
|
||||
|
||||
function _unwindBaseIntoQuote(
|
||||
address baseToken,
|
||||
address quoteToken,
|
||||
address externalUnwinder,
|
||||
uint256 minOutUnwind,
|
||||
bytes memory unwindData
|
||||
) internal returns (uint256 unwindOut) {
|
||||
uint256 remainingBase = IERC20(baseToken).balanceOf(address(this));
|
||||
IERC20(baseToken).forceApprove(externalUnwinder, remainingBase);
|
||||
unwindOut =
|
||||
IAaveExternalUnwinder(externalUnwinder).unwind(baseToken, quoteToken, remainingBase, minOutUnwind, unwindData);
|
||||
}
|
||||
|
||||
function _approveRepayment(address quoteToken, uint256 need) internal returns (uint256 surplus) {
|
||||
IERC20 quote = IERC20(quoteToken);
|
||||
uint256 quoteBal = quote.balanceOf(address(this));
|
||||
if (quoteBal < need) revert InsufficientToRepay();
|
||||
surplus = quoteBal - need;
|
||||
quote.forceApprove(pool, need);
|
||||
}
|
||||
|
||||
function _triggerAtomicBridge(address baseToken, uint256 baseBal, AtomicBridgeParams memory atomicBridge) internal {
|
||||
if (
|
||||
atomicBridge.destinationAsset == address(0) || atomicBridge.destinationRecipient == address(0)
|
||||
|| atomicBridge.bridgeAmount == 0 || atomicBridge.bridgeAmount > baseBal
|
||||
|| atomicBridge.destinationDeadline <= block.timestamp
|
||||
) revert InvalidAtomicBridge();
|
||||
|
||||
address escrowAddress = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).obligationEscrow();
|
||||
IERC20(baseToken).forceApprove(escrowAddress, atomicBridge.bridgeAmount);
|
||||
bytes32 obligationId = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).createIntent(
|
||||
IAaveAtomicBridgeCoordinator.CreateIntentParams({
|
||||
sourceChain: atomicBridge.sourceChain,
|
||||
destinationChain: atomicBridge.destinationChain,
|
||||
assetIn: baseToken,
|
||||
assetOut: atomicBridge.destinationAsset,
|
||||
amountIn: atomicBridge.bridgeAmount,
|
||||
minAmountOut: atomicBridge.minDestinationAmount,
|
||||
recipient: atomicBridge.destinationRecipient,
|
||||
deadline: atomicBridge.destinationDeadline,
|
||||
routeId: atomicBridge.routeId
|
||||
})
|
||||
);
|
||||
if (atomicBridge.submitCommitment) {
|
||||
IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).submitCommitment(
|
||||
obligationId, atomicBridge.settlementMode
|
||||
);
|
||||
}
|
||||
emit AtomicBridgeTriggered(
|
||||
obligationId,
|
||||
atomicBridge.coordinator,
|
||||
atomicBridge.destinationRecipient,
|
||||
atomicBridge.bridgeAmount,
|
||||
atomicBridge.minDestinationAmount
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
interface IDODOIntegrationSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title DODOIntegrationExternalUnwinder
|
||||
* @notice Unwinds base -> quote through a DODO PMM integration.
|
||||
* @dev `data` must be `abi.encode(address pool)` selecting the registered pool to use.
|
||||
*/
|
||||
contract DODOIntegrationExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable integration;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address integration_) {
|
||||
integration = integration_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams();
|
||||
if (data.length != 32) revert BadParams();
|
||||
|
||||
address pool = abi.decode(data, (address));
|
||||
if (pool == address(0)) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(integration, amountIn);
|
||||
amountOut = IDODOIntegrationSwapExactIn(integration).swapExactIn(pool, tokenIn, amountIn, minAmountOut);
|
||||
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
interface IDODOMultiHopSwapExactInLike {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
interface ISwapRouterLikeMultiHop {
|
||||
struct ExactInputParams {
|
||||
bytes path;
|
||||
address recipient;
|
||||
uint256 amountIn;
|
||||
uint256 amountOutMinimum;
|
||||
}
|
||||
|
||||
function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
contract DODOToUniswapV3MultiHopExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable integration;
|
||||
address public immutable router;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address integration_, address router_) {
|
||||
integration = integration_;
|
||||
router = router_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams();
|
||||
|
||||
(address dodoPool, address intermediateToken, uint256 minIntermediateOut, bytes memory uniswapPath) =
|
||||
abi.decode(data, (address, address, uint256, bytes));
|
||||
if (dodoPool == address(0) || intermediateToken == address(0) || intermediateToken == tokenIn || intermediateToken == tokenOut) {
|
||||
revert BadParams();
|
||||
}
|
||||
if (uniswapPath.length < 43) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(integration, amountIn);
|
||||
uint256 intermediateOut =
|
||||
IDODOMultiHopSwapExactInLike(integration).swapExactIn(dodoPool, tokenIn, amountIn, minIntermediateOut);
|
||||
|
||||
IERC20(intermediateToken).forceApprove(router, intermediateOut);
|
||||
amountOut = ISwapRouterLikeMultiHop(router).exactInput(
|
||||
ISwapRouterLikeMultiHop.ExactInputParams({
|
||||
path: uniswapPath,
|
||||
recipient: msg.sender,
|
||||
amountIn: intermediateOut,
|
||||
amountOutMinimum: minAmountOut
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
interface ISwapRouterLike {
|
||||
struct ExactInputSingleParams {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
uint24 fee;
|
||||
address recipient;
|
||||
uint256 amountIn;
|
||||
uint256 amountOutMinimum;
|
||||
uint160 sqrtPriceLimitX96;
|
||||
}
|
||||
|
||||
struct ExactInputParams {
|
||||
bytes path;
|
||||
address recipient;
|
||||
uint256 amountIn;
|
||||
uint256 amountOutMinimum;
|
||||
}
|
||||
|
||||
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
|
||||
function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
contract UniswapV3ExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable router;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address router_) {
|
||||
router = router_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(router, amountIn);
|
||||
|
||||
if (data.length == 32) {
|
||||
uint24 fee = abi.decode(data, (uint24));
|
||||
return ISwapRouterLike(router).exactInputSingle(
|
||||
ISwapRouterLike.ExactInputSingleParams({
|
||||
tokenIn: tokenIn,
|
||||
tokenOut: tokenOut,
|
||||
fee: fee,
|
||||
recipient: msg.sender,
|
||||
amountIn: amountIn,
|
||||
amountOutMinimum: minAmountOut,
|
||||
sqrtPriceLimitX96: 0
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
bytes memory path = abi.decode(data, (bytes));
|
||||
return ISwapRouterLike(router).exactInput(
|
||||
ISwapRouterLike.ExactInputParams({
|
||||
path: path,
|
||||
recipient: msg.sender,
|
||||
amountIn: amountIn,
|
||||
amountOutMinimum: minAmountOut
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {
|
||||
AaveQuotePushFlashReceiver,
|
||||
IAaveExternalUnwinder
|
||||
} from "../src/AaveQuotePushFlashReceiver.sol";
|
||||
import {AtomicBridgeCoordinator} from "atomic/AtomicBridgeCoordinator.sol";
|
||||
import {AtomicFeePolicy} from "atomic/AtomicFeePolicy.sol";
|
||||
import {AtomicFulfillerRegistry} from "atomic/AtomicFulfillerRegistry.sol";
|
||||
import {AtomicLiquidityVault} from "atomic/AtomicLiquidityVault.sol";
|
||||
import {AtomicObligationEscrow} from "atomic/AtomicObligationEscrow.sol";
|
||||
import {AtomicSettlementRouter} from "atomic/AtomicSettlementRouter.sol";
|
||||
import {AtomicSlashingManager} from "atomic/AtomicSlashingManager.sol";
|
||||
import {AtomicTypes} from "atomic/AtomicTypes.sol";
|
||||
import {IAtomicSettlementAdapter} from "atomic/interfaces/IAtomicSettlementAdapter.sol";
|
||||
import {MockMintableToken} from "repo-test/dbis/MockMintableToken.sol";
|
||||
|
||||
contract AaveForkMockExternalUnwinder is IAaveExternalUnwinder {
|
||||
IERC20 public immutable base;
|
||||
IERC20 public immutable quote;
|
||||
uint256 public immutable numerator;
|
||||
uint256 public immutable denominator;
|
||||
|
||||
constructor(IERC20 base_, IERC20 quote_, uint256 numerator_, uint256 denominator_) {
|
||||
base = base_;
|
||||
quote = quote_;
|
||||
numerator = numerator_;
|
||||
denominator = denominator_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata)
|
||||
external
|
||||
override
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
require(tokenIn == address(base), "base only");
|
||||
require(tokenOut == address(quote), "quote only");
|
||||
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
|
||||
amountOut = amountIn * numerator / denominator;
|
||||
require(amountOut >= minAmountOut, "min unwind");
|
||||
IERC20(address(quote)).transfer(msg.sender, amountOut);
|
||||
}
|
||||
}
|
||||
|
||||
contract MockAtomicSettlementAdapter is IAtomicSettlementAdapter {
|
||||
address public lastToken;
|
||||
uint256 public lastAmount;
|
||||
address public lastRecipient;
|
||||
bytes32 public lastObligationId;
|
||||
|
||||
function executeSettlement(
|
||||
bytes32 obligationId,
|
||||
address token,
|
||||
uint256 amount,
|
||||
address recipient,
|
||||
bytes calldata
|
||||
) external payable returns (bytes32 settlementId) {
|
||||
lastObligationId = obligationId;
|
||||
lastToken = token;
|
||||
lastAmount = amount;
|
||||
lastRecipient = recipient;
|
||||
settlementId = keccak256(abi.encode(obligationId, token, amount, recipient, block.timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
contract AaveQuotePushFlashReceiverMainnetForkTest is Test {
|
||||
address constant AAVE_POOL_MAINNET = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
|
||||
address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84;
|
||||
address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E;
|
||||
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
||||
bytes32 constant MOCK_SETTLEMENT_MODE = keccak256("MOCK_SETTLEMENT_MODE");
|
||||
|
||||
AaveQuotePushFlashReceiver internal receiver;
|
||||
AaveForkMockExternalUnwinder internal unwinder;
|
||||
MockMintableToken internal cusdc138;
|
||||
MockMintableToken internal bondToken;
|
||||
AtomicLiquidityVault internal atomicVault;
|
||||
AtomicFulfillerRegistry internal atomicRegistry;
|
||||
AtomicFeePolicy internal atomicFeePolicy;
|
||||
AtomicObligationEscrow internal atomicEscrow;
|
||||
AtomicSettlementRouter internal atomicRouter;
|
||||
AtomicSlashingManager internal atomicSlashingManager;
|
||||
AtomicBridgeCoordinator internal atomicCoordinator;
|
||||
MockAtomicSettlementAdapter internal mockSettlementAdapter;
|
||||
bytes32 internal atomicCorridorId;
|
||||
address internal destinationRecipient = address(0x138138);
|
||||
|
||||
function setUp() public {
|
||||
string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
|
||||
vm.createSelectFork(rpcUrl);
|
||||
|
||||
receiver = new AaveQuotePushFlashReceiver(AAVE_POOL_MAINNET);
|
||||
unwinder = new AaveForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 130, 100);
|
||||
deal(USDC, address(unwinder), 100_000_000);
|
||||
|
||||
cusdc138 = new MockMintableToken("Chain 138 USDC", "cUSDC", 6, address(this));
|
||||
bondToken = new MockMintableToken("Atomic Bond", "aBOND", 6, address(this));
|
||||
atomicVault = new AtomicLiquidityVault(address(this));
|
||||
atomicRegistry = new AtomicFulfillerRegistry(address(bondToken), address(this));
|
||||
atomicFeePolicy = new AtomicFeePolicy(address(this));
|
||||
atomicEscrow = new AtomicObligationEscrow(address(this));
|
||||
atomicRouter = new AtomicSettlementRouter(address(this));
|
||||
atomicSlashingManager = new AtomicSlashingManager(address(atomicRegistry), address(this));
|
||||
mockSettlementAdapter = new MockAtomicSettlementAdapter();
|
||||
atomicCoordinator = new AtomicBridgeCoordinator(
|
||||
address(atomicVault),
|
||||
address(atomicRegistry),
|
||||
address(atomicEscrow),
|
||||
address(atomicRouter),
|
||||
address(atomicFeePolicy),
|
||||
address(atomicSlashingManager),
|
||||
address(this),
|
||||
address(this)
|
||||
);
|
||||
|
||||
atomicVault.grantRole(atomicVault.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicVault.grantRole(atomicVault.RECONCILER_ROLE(), address(atomicCoordinator));
|
||||
atomicRegistry.grantRole(atomicRegistry.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicRegistry.grantRole(atomicRegistry.SLASHER_ROLE(), address(atomicSlashingManager));
|
||||
atomicEscrow.grantRole(atomicEscrow.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicRouter.grantRole(atomicRouter.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicSlashingManager.grantRole(atomicSlashingManager.COORDINATOR_ROLE(), address(atomicCoordinator));
|
||||
atomicRouter.setAdapter(MOCK_SETTLEMENT_MODE, address(mockSettlementAdapter));
|
||||
|
||||
atomicCorridorId = atomicCoordinator.getCorridorId(1, 138, CWUSDC, address(cusdc138));
|
||||
atomicCoordinator.configureCorridor(
|
||||
AtomicTypes.CorridorConfig({
|
||||
enabled: true,
|
||||
degraded: false,
|
||||
sourceChain: 1,
|
||||
destinationChain: 138,
|
||||
assetIn: CWUSDC,
|
||||
assetOut: address(cusdc138),
|
||||
maxNotional: 10_000_000,
|
||||
maxReservedBps: 8_000,
|
||||
targetBuffer: 100_000,
|
||||
maxSettlementBacklog: 5_000_000,
|
||||
maxOracleDriftBps: 500,
|
||||
fulfilmentTimeout: 1 days,
|
||||
settlementTimeout: 2 days,
|
||||
defaultSettlementMode: MOCK_SETTLEMENT_MODE
|
||||
})
|
||||
);
|
||||
atomicFeePolicy.setCorridorPolicy(atomicCorridorId, 25, 10, 12_000, 500, 1 days, 2 days);
|
||||
atomicVault.setTargetBuffer(atomicCorridorId, address(cusdc138), 100_000);
|
||||
cusdc138.mint(address(this), 5_000_000);
|
||||
cusdc138.approve(address(atomicVault), type(uint256).max);
|
||||
atomicVault.fundCorridor(atomicCorridorId, address(cusdc138), 5_000_000);
|
||||
|
||||
bondToken.mint(address(receiver), 5_000_000);
|
||||
vm.startPrank(address(receiver));
|
||||
bondToken.approve(address(atomicRegistry), type(uint256).max);
|
||||
atomicRegistry.depositBond(3_000_000);
|
||||
vm.stopPrank();
|
||||
atomicRegistry.setFulfillerActive(address(receiver), true);
|
||||
atomicRegistry.setCorridorAuthorization(address(receiver), atomicCorridorId, true);
|
||||
}
|
||||
|
||||
function testFork_aaveQuotePush_usesRealAaveAndRealMainnetPmm() public {
|
||||
uint256 amount = 2_964_298;
|
||||
uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver));
|
||||
uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
|
||||
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
||||
integration: DODO_PMM_INTEGRATION_MAINNET,
|
||||
pmmPool: POOL_CWUSDC_USDC,
|
||||
baseToken: CWUSDC,
|
||||
externalUnwinder: address(unwinder),
|
||||
minOutPmm: 2_800_000,
|
||||
minOutUnwind: amount + 1_483,
|
||||
unwindData: bytes(""),
|
||||
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
||||
coordinator: address(0),
|
||||
sourceChain: 0,
|
||||
destinationChain: 0,
|
||||
destinationAsset: address(0),
|
||||
bridgeAmount: 0,
|
||||
minDestinationAmount: 0,
|
||||
destinationRecipient: address(0),
|
||||
destinationDeadline: 0,
|
||||
routeId: bytes32(0),
|
||||
settlementMode: bytes32(0),
|
||||
submitCommitment: false
|
||||
})
|
||||
});
|
||||
|
||||
receiver.flashQuotePush(USDC, amount, p);
|
||||
|
||||
uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver));
|
||||
uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 actualBaseOut = poolBaseBefore - poolBaseAfter;
|
||||
uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore;
|
||||
uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore;
|
||||
uint256 premium = _aavePremium(amount);
|
||||
(uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) =
|
||||
_predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, 0, premium);
|
||||
uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3);
|
||||
|
||||
assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver retains surplus");
|
||||
assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "base fully unwound");
|
||||
assertLt(poolBaseAfter, poolBaseBefore, "pool base decreased");
|
||||
assertGt(poolQuoteAfter, poolQuoteBefore, "pool quote increased");
|
||||
_assertWithinOnePercent(actualBaseOut, predictedBaseOut, "baseOut");
|
||||
_assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "netQuoteIn");
|
||||
_assertWithinOnePercent(actualSurplus, predictedSurplus, "surplus");
|
||||
assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "unwindOut");
|
||||
}
|
||||
|
||||
function testFork_aaveQuotePush_atomicCorridorFulfillment_1_to_138_cwusdc_to_cusdc() public {
|
||||
uint256 amount = 2_964_298;
|
||||
uint256 bridgeAmount = 500_000;
|
||||
uint256 destinationRecipientBefore = cusdc138.balanceOf(destinationRecipient);
|
||||
uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
|
||||
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
||||
integration: DODO_PMM_INTEGRATION_MAINNET,
|
||||
pmmPool: POOL_CWUSDC_USDC,
|
||||
baseToken: CWUSDC,
|
||||
externalUnwinder: address(unwinder),
|
||||
minOutPmm: 2_800_000,
|
||||
minOutUnwind: amount + 1_483,
|
||||
unwindData: bytes(""),
|
||||
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
||||
coordinator: address(atomicCoordinator),
|
||||
sourceChain: 1,
|
||||
destinationChain: 138,
|
||||
destinationAsset: address(cusdc138),
|
||||
bridgeAmount: bridgeAmount,
|
||||
minDestinationAmount: bridgeAmount,
|
||||
destinationRecipient: destinationRecipient,
|
||||
destinationDeadline: block.timestamp + 1 hours,
|
||||
routeId: atomicCorridorId,
|
||||
settlementMode: bytes32(0),
|
||||
submitCommitment: true
|
||||
})
|
||||
});
|
||||
|
||||
uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver));
|
||||
receiver.flashQuotePush(USDC, amount, p);
|
||||
|
||||
uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver));
|
||||
uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
||||
uint256 actualBaseOut = poolBaseBefore - poolBaseAfter;
|
||||
uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore;
|
||||
uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore;
|
||||
uint256 premium = _aavePremium(amount);
|
||||
(uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) =
|
||||
_predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, bridgeAmount, premium);
|
||||
uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3);
|
||||
assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver still retains quote surplus");
|
||||
assertEq(cusdc138.balanceOf(destinationRecipient), destinationRecipientBefore + bridgeAmount, "destination funded");
|
||||
assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "remaining base fully unwound");
|
||||
_assertWithinOnePercent(actualBaseOut, predictedBaseOut, "atomic baseOut");
|
||||
_assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "atomic netQuoteIn");
|
||||
_assertWithinOnePercent(actualSurplus, predictedSurplus, "atomic surplus");
|
||||
assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "atomic unwindOut");
|
||||
|
||||
AtomicTypes.CorridorLiquidityState memory state =
|
||||
atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138));
|
||||
assertEq(state.settlementBacklog, bridgeAmount, "backlog increased by delivered amount");
|
||||
assertEq(state.totalLiquidity, 5_000_000 - bridgeAmount, "vault liquidity debited on immediate fulfillment");
|
||||
}
|
||||
|
||||
function testFork_aaveQuotePush_atomicCorridorSettlementConfirmation_1_to_138() public {
|
||||
uint256 amount = 2_964_298;
|
||||
uint256 bridgeAmount = 500_000;
|
||||
uint256 deadline = block.timestamp + 1 hours;
|
||||
uint256 receiverBondBefore = atomicRegistry.availableBond(address(receiver));
|
||||
uint256 treasuryBaseBefore = IERC20(CWUSDC).balanceOf(address(this));
|
||||
uint256 receiverBaseBefore = IERC20(CWUSDC).balanceOf(address(receiver));
|
||||
|
||||
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
||||
integration: DODO_PMM_INTEGRATION_MAINNET,
|
||||
pmmPool: POOL_CWUSDC_USDC,
|
||||
baseToken: CWUSDC,
|
||||
externalUnwinder: address(unwinder),
|
||||
minOutPmm: 2_800_000,
|
||||
minOutUnwind: amount + 1_483,
|
||||
unwindData: bytes(""),
|
||||
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
||||
coordinator: address(atomicCoordinator),
|
||||
sourceChain: 1,
|
||||
destinationChain: 138,
|
||||
destinationAsset: address(cusdc138),
|
||||
bridgeAmount: bridgeAmount,
|
||||
minDestinationAmount: bridgeAmount,
|
||||
destinationRecipient: destinationRecipient,
|
||||
destinationDeadline: deadline,
|
||||
routeId: atomicCorridorId,
|
||||
settlementMode: bytes32(0),
|
||||
submitCommitment: true
|
||||
})
|
||||
});
|
||||
|
||||
receiver.flashQuotePush(USDC, amount, p);
|
||||
bytes32 obligationId = _deriveObligationId(bridgeAmount, deadline);
|
||||
|
||||
AtomicTypes.AtomicObligation memory fulfilled = atomicCoordinator.getObligation(obligationId);
|
||||
assertEq(uint8(fulfilled.status), uint8(AtomicTypes.ObligationStatus.Fulfilled), "obligation fulfilled");
|
||||
|
||||
atomicCoordinator.initiateSettlement(obligationId, abi.encodePacked(bytes32(uint256(138))));
|
||||
|
||||
AtomicTypes.AtomicObligation memory pending = atomicCoordinator.getObligation(obligationId);
|
||||
assertEq(uint8(pending.status), uint8(AtomicTypes.ObligationStatus.SettlementPending), "obligation pending");
|
||||
assertEq(mockSettlementAdapter.lastObligationId(), obligationId, "adapter saw obligation");
|
||||
assertEq(mockSettlementAdapter.lastToken(), CWUSDC, "adapter token");
|
||||
assertEq(mockSettlementAdapter.lastRecipient(), destinationRecipient, "adapter recipient");
|
||||
|
||||
uint256 expectedFulfillerFee = (bridgeAmount * 25) / 10_000;
|
||||
uint256 expectedProtocolFee = (bridgeAmount * 10) / 10_000;
|
||||
uint256 expectedSettlementAmount = bridgeAmount - expectedFulfillerFee - expectedProtocolFee;
|
||||
assertEq(mockSettlementAdapter.lastAmount(), expectedSettlementAmount, "adapter amount");
|
||||
assertEq(IERC20(CWUSDC).balanceOf(address(this)), treasuryBaseBefore + expectedProtocolFee, "protocol fee received");
|
||||
assertEq(
|
||||
IERC20(CWUSDC).balanceOf(address(receiver)),
|
||||
receiverBaseBefore + expectedFulfillerFee,
|
||||
"fulfiller fee received"
|
||||
);
|
||||
|
||||
cusdc138.mint(address(this), bridgeAmount);
|
||||
cusdc138.approve(address(atomicVault), bridgeAmount);
|
||||
atomicCoordinator.confirmSettlement(obligationId, bridgeAmount);
|
||||
|
||||
AtomicTypes.AtomicObligation memory settled = atomicCoordinator.getObligation(obligationId);
|
||||
assertEq(uint8(settled.status), uint8(AtomicTypes.ObligationStatus.Settled), "obligation settled");
|
||||
|
||||
AtomicTypes.CorridorLiquidityState memory state =
|
||||
atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138));
|
||||
assertEq(state.settlementBacklog, 0, "backlog cleared");
|
||||
assertEq(state.totalLiquidity, 5_000_000, "vault replenished");
|
||||
assertEq(atomicRegistry.availableBond(address(receiver)), receiverBondBefore, "bond released");
|
||||
}
|
||||
|
||||
function _deriveObligationId(uint256 bridgeAmount, uint256 deadline) internal view returns (bytes32) {
|
||||
bytes32 intentId = keccak256(
|
||||
abi.encode(
|
||||
block.chainid,
|
||||
address(receiver),
|
||||
uint256(1),
|
||||
atomicCorridorId,
|
||||
bridgeAmount,
|
||||
bridgeAmount,
|
||||
deadline,
|
||||
atomicCorridorId
|
||||
)
|
||||
);
|
||||
return keccak256(abi.encode(intentId, destinationRecipient));
|
||||
}
|
||||
|
||||
function _predictQuotePush(
|
||||
uint256 baseReserve,
|
||||
uint256 quoteReserve,
|
||||
uint256 grossQuoteIn,
|
||||
uint256 lpFeeBps,
|
||||
uint256 unwindNumerator,
|
||||
uint256 unwindDenominator,
|
||||
uint256 bridgeAmount,
|
||||
uint256 premium
|
||||
) internal pure returns (uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) {
|
||||
uint256 netQuoteIn = (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000;
|
||||
predictedBaseOut = (netQuoteIn * baseReserve) / (quoteReserve + netQuoteIn);
|
||||
uint256 remainingBase = predictedBaseOut - bridgeAmount;
|
||||
predictedUnwindOut = (remainingBase * unwindNumerator) / unwindDenominator;
|
||||
predictedSurplus = predictedUnwindOut - grossQuoteIn - premium;
|
||||
}
|
||||
|
||||
function _netQuoteIn(uint256 grossQuoteIn, uint256 lpFeeBps) internal pure returns (uint256) {
|
||||
return (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000;
|
||||
}
|
||||
|
||||
function _assertWithinOnePercent(uint256 actual, uint256 expected, string memory label) internal pure {
|
||||
if (expected == 0) {
|
||||
require(actual == 0, label);
|
||||
return;
|
||||
}
|
||||
uint256 diff = actual > expected ? actual - expected : expected - actual;
|
||||
require(diff * 10_000 <= expected * 100, label);
|
||||
}
|
||||
|
||||
function _aavePremium(uint256 amount) internal pure returns (uint256) {
|
||||
return (amount * 5) / 10_000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {DODOIntegrationExternalUnwinder} from "../src/DODOIntegrationExternalUnwinder.sol";
|
||||
|
||||
contract DODOIntegrationExternalUnwinderMainnetForkTest is Test {
|
||||
address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84;
|
||||
address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E;
|
||||
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
||||
|
||||
DODOIntegrationExternalUnwinder internal unwinder;
|
||||
|
||||
function setUp() public {
|
||||
string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
|
||||
vm.createSelectFork(rpcUrl);
|
||||
unwinder = new DODOIntegrationExternalUnwinder(DODO_PMM_INTEGRATION_MAINNET);
|
||||
}
|
||||
|
||||
function testFork_cWUSDCToUSDC_unwindsThroughMainnetDodoIntegration() public {
|
||||
uint256 amountIn = 1_000_000;
|
||||
deal(CWUSDC, address(this), amountIn);
|
||||
IERC20(CWUSDC).approve(address(unwinder), amountIn);
|
||||
|
||||
uint256 before = IERC20(USDC).balanceOf(address(this));
|
||||
uint256 amountOut = unwinder.unwind(CWUSDC, USDC, amountIn, 1, abi.encode(POOL_CWUSDC_USDC));
|
||||
uint256 afterBal = IERC20(USDC).balanceOf(address(this));
|
||||
|
||||
assertGt(amountOut, 0, "amountOut > 0");
|
||||
assertEq(afterBal - before, amountOut, "USDC received");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {DODOToUniswapV3MultiHopExternalUnwinder} from "../src/DODOToUniswapV3MultiHopExternalUnwinder.sol";
|
||||
|
||||
contract DODOToUniswapV3MultiHopExternalUnwinderMainnetForkTest is Test {
|
||||
address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84;
|
||||
address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
|
||||
address constant POOL_CWUSDC_USDT = 0xCC0fd27A40775c9AfcD2BBd3f7c902b0192c247A;
|
||||
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
|
||||
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
||||
|
||||
DODOToUniswapV3MultiHopExternalUnwinder internal unwinder;
|
||||
|
||||
function setUp() public {
|
||||
string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
|
||||
vm.createSelectFork(rpcUrl);
|
||||
unwinder = new DODOToUniswapV3MultiHopExternalUnwinder(DODO_PMM_INTEGRATION_MAINNET, UNISWAP_V3_ROUTER);
|
||||
}
|
||||
|
||||
function testFork_cWUSDCToUSDC_multihopViaUSDT_works() public {
|
||||
uint256 amountIn = 1_000_000;
|
||||
deal(CWUSDC, address(this), amountIn);
|
||||
IERC20(CWUSDC).approve(address(unwinder), amountIn);
|
||||
|
||||
bytes memory path = abi.encodePacked(USDT, uint24(100), USDC);
|
||||
bytes memory data = abi.encode(POOL_CWUSDC_USDT, USDT, uint256(1), path);
|
||||
|
||||
uint256 before = IERC20(USDC).balanceOf(address(this));
|
||||
uint256 amountOut = unwinder.unwind(CWUSDC, USDC, amountIn, 1, data);
|
||||
uint256 afterBal = IERC20(USDC).balanceOf(address(this));
|
||||
|
||||
assertGt(amountOut, 0, "amountOut > 0");
|
||||
assertEq(afterBal - before, amountOut, "USDC received");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {UniswapV3ExternalUnwinder} from "../src/UniswapV3ExternalUnwinder.sol";
|
||||
|
||||
interface IWETHFork {
|
||||
function deposit() external payable;
|
||||
function transfer(address to, uint256 value) external returns (bool);
|
||||
}
|
||||
|
||||
contract UniswapV3ExternalUnwinderMainnetForkTest is Test {
|
||||
address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
|
||||
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
||||
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
||||
|
||||
UniswapV3ExternalUnwinder internal unwinder;
|
||||
|
||||
function setUp() public {
|
||||
string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
|
||||
vm.createSelectFork(rpcUrl);
|
||||
unwinder = new UniswapV3ExternalUnwinder(UNISWAP_V3_ROUTER);
|
||||
}
|
||||
|
||||
function testFork_knownRoute_WETHToUSDC_singleHopWorks() public {
|
||||
vm.deal(address(this), 1 ether);
|
||||
IWETHFork(WETH).deposit{value: 1 ether}();
|
||||
IERC20(WETH).approve(address(unwinder), 1 ether);
|
||||
uint256 before = IERC20(USDC).balanceOf(address(this));
|
||||
|
||||
uint256 amountOut = unwinder.unwind(WETH, USDC, 1 ether, 1, abi.encode(uint24(3000)));
|
||||
|
||||
uint256 afterBal = IERC20(USDC).balanceOf(address(this));
|
||||
assertGt(amountOut, 0, "amountOut > 0");
|
||||
assertEq(afterBal - before, amountOut, "USDC received");
|
||||
}
|
||||
|
||||
function testFork_cWUSDCToUSDC_routeUnavailableOnUniswapV3() public {
|
||||
deal(CWUSDC, address(this), 1_000_000);
|
||||
IERC20(CWUSDC).approve(address(unwinder), 1_000_000);
|
||||
|
||||
vm.expectRevert();
|
||||
unwinder.unwind(CWUSDC, USDC, 1_000_000, 1, abi.encode(uint24(3000)));
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,12 @@ optimizer = true
|
||||
optimizer_runs = 1
|
||||
via_ir = false
|
||||
|
||||
[profile.fork]
|
||||
optimizer = true
|
||||
optimizer_runs = 1
|
||||
via_ir = true
|
||||
evm_version = "cancun"
|
||||
|
||||
[profile.deploy]
|
||||
optimizer = true
|
||||
optimizer_runs = 100
|
||||
|
||||
+34
-11
@@ -1,6 +1,29 @@
|
||||
require("@nomicfoundation/hardhat-toolbox");
|
||||
require("dotenv").config();
|
||||
|
||||
function resolveAccounts() {
|
||||
const rawPrivateKey = (process.env.PRIVATE_KEY || "").trim();
|
||||
|
||||
if (!rawPrivateKey) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedPrivateKey = rawPrivateKey.startsWith("0x")
|
||||
? rawPrivateKey
|
||||
: `0x${rawPrivateKey}`;
|
||||
|
||||
if (/^0x[0-9a-fA-F]{64}$/.test(normalizedPrivateKey)) {
|
||||
return [normalizedPrivateKey];
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"Ignoring PRIVATE_KEY in Hardhat config because it is not a valid 32-byte hex key."
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const sharedAccounts = resolveAccounts();
|
||||
|
||||
/** @type import('hardhat/config').HardhatUserConfig */
|
||||
module.exports = {
|
||||
solidity: {
|
||||
@@ -15,57 +38,57 @@ module.exports = {
|
||||
},
|
||||
mainnet: {
|
||||
url: process.env.ETHEREUM_MAINNET_RPC || "https://eth.llamarpc.com",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 1,
|
||||
},
|
||||
chain138: {
|
||||
url: process.env.CHAIN138_RPC_URL || "https://rpc.d-bis.org",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 138,
|
||||
},
|
||||
sepolia: {
|
||||
url: process.env.SEPOLIA_RPC_URL || "https://rpc.sepolia.org",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 11155111,
|
||||
},
|
||||
bsc: {
|
||||
url: process.env.BSC_MAINNET_RPC || process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 56,
|
||||
},
|
||||
polygon: {
|
||||
url: process.env.POLYGON_MAINNET_RPC || "https://polygon-rpc.com",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 137,
|
||||
},
|
||||
gnosis: {
|
||||
url: process.env.GNOSIS_RPC_URL || "https://rpc.gnosischain.com",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 100,
|
||||
},
|
||||
cronos: {
|
||||
url: process.env.CRONOS_RPC_URL || "https://evm.cronos.org",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 25,
|
||||
},
|
||||
optimism: {
|
||||
url: process.env.OPTIMISM_MAINNET_RPC || "https://mainnet.optimism.io",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 10,
|
||||
},
|
||||
base: {
|
||||
url: process.env.BASE_MAINNET_RPC || "https://mainnet.base.org",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 8453,
|
||||
},
|
||||
arbitrum: {
|
||||
url: process.env.ARBITRUM_MAINNET_RPC || "https://arb1.arbitrum.io/rpc",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 42161,
|
||||
},
|
||||
avalanche: {
|
||||
url: process.env.AVALANCHE_RPC_URL || process.env.AVALANCHE_MAINNET_RPC || "https://api.avax.network/ext/bc/C/rpc",
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||
accounts: sharedAccounts,
|
||||
chainId: 43114,
|
||||
},
|
||||
},
|
||||
|
||||
+10
-1
@@ -4,7 +4,16 @@
|
||||
"description": "Production-grade CCIP integration for Chain-138 to Ethereum Mainnet",
|
||||
"scripts": {
|
||||
"compile": "hardhat compile",
|
||||
"test": "hardhat test",
|
||||
"test": "pnpm run test:ci",
|
||||
"test:ci": "pnpm run test:contracts:ci && pnpm run test:services:ci",
|
||||
"test:contracts:ci": "pnpm run test:contracts:transport && pnpm run test:contracts:ccip-smoke",
|
||||
"test:contracts:transport": "bash ../scripts/verify/check-cstar-v2-transport-stack.sh",
|
||||
"test:contracts:ccip-smoke": "npx hardhat test --no-compile test/ccip-integration/CCIPIntegration.test.js",
|
||||
"test:services:ci": "pnpm run test:services:token-aggregation && pnpm run test:services:emoney-api",
|
||||
"test:services:token-aggregation": "pnpm --dir services/token-aggregation run test:ci",
|
||||
"test:services:emoney-api": "pnpm --dir test/emoney/api run test:ci",
|
||||
"test:hardhat:full": "hardhat test",
|
||||
"test:forge:full": "forge test",
|
||||
"forge:build": "forge build",
|
||||
"forge:test": "forge test",
|
||||
"forge:test:vault": "forge test --match-path 'test/vault/Ledger.t.sol'",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Script, console} from "forge-std/Script.sol";
|
||||
import {CCIPRelayRouter} from "../contracts/relay/CCIPRelayRouter.sol";
|
||||
|
||||
contract DeployCCIPRelayRouterOnly is Script {
|
||||
function run() external {
|
||||
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
|
||||
address deployer = vm.addr(deployerPrivateKey);
|
||||
address relayer = vm.envAddress("RELAYER_ADDRESS");
|
||||
|
||||
console.log("Deploying CCIPRelayRouter with deployer:", deployer);
|
||||
console.log("Relayer:", relayer);
|
||||
|
||||
vm.startBroadcast(deployerPrivateKey);
|
||||
|
||||
CCIPRelayRouter relayRouter = new CCIPRelayRouter();
|
||||
if (relayer != address(0)) {
|
||||
relayRouter.grantRelayerRole(relayer);
|
||||
}
|
||||
|
||||
vm.stopBroadcast();
|
||||
|
||||
console.log("CCIPRelayRouter deployed at:", address(relayRouter));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user