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:
defiQUG
2026-04-07 23:40:52 -07:00
parent 0fb7bba07b
commit 76aa419320
289 changed files with 28367 additions and 824 deletions
+3
View File
@@ -57,6 +57,9 @@ temp/
# Node modules (if any)
node_modules/
# TypeScript service emit (build in-tree)
services/btc-intake/dist/
# Python
__pycache__/
*.pyc
+4 -3
View File
@@ -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
```
+8 -7
View File
@@ -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
+4 -3
View File
@@ -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
```
+21
View File
@@ -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/"
]
+45 -5
View File
@@ -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
+12 -5
View File
@@ -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",
+1 -1
View File
@@ -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";
+358
View File
@@ -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;
}
}
+304
View File
@@ -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 -2
View File
@@ -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);
}
}
+92
View File
@@ -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);
}
}
}
+1 -1
View File
@@ -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";
/**
+88 -15
View File
@@ -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;
}
}
+70 -6
View File
@@ -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;
}
}
+140
View File
@@ -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);
}
+2 -2
View File
@@ -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 -2
View File
@@ -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";
/**
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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";
/**
+1 -1
View File
@@ -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";
/**
+27 -9
View File
@@ -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);
}
}
+85 -9
View File
@@ -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"
);
}
}
+1 -1
View File
@@ -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";
+28
View File
@@ -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);
}
}
+29
View File
@@ -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_
)
{}
}
+29
View File
@@ -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_
)
{}
}
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+2
View File
@@ -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 DODOs 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 DODOs 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 |
+22 -9
View File
@@ -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.**
- DODOs 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.
- DODOs 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)**
- DODOs contracts are open source: [DODOEX GitHub](https://github.com/DODOEX) (contractV2).
+15 -10
View File
@@ -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
+2 -2
View File
@@ -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`. |
---
+11
View File
@@ -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
View File
@@ -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
+20
View File
@@ -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)));
}
}
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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'",
+27
View File
@@ -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