diff --git a/.gitignore b/.gitignore index 7b7c08c..2dc842f 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,14 @@ assets/azure-icons/*.zip assets/**/*.tmp assets/**/.*.swp + +# Relay lane secrets (examples stay tracked) +services/relay/.env.* +!services/relay/.env.*.example +!services/relay/.env.local.example + +# Local DODO vendor trees (use lib/dodo-contractV2 submodule) +lib/dodo-gassaving-pool/ +lib/dodo-limit-order/ +lib/dodo-v3/ +lib/deploy-starter.txt diff --git a/docs/00-meta/SIBLING_WIP_IMPORT.md b/docs/00-meta/SIBLING_WIP_IMPORT.md index 85e498d..3bc4959 100644 --- a/docs/00-meta/SIBLING_WIP_IMPORT.md +++ b/docs/00-meta/SIBLING_WIP_IMPORT.md @@ -68,3 +68,18 @@ Imported CCIP and keeper documentation was consolidated into [docs/ccip-integrat - Chain 138 guides → `docs/ccip-integration/chain138/` - Reference material → `docs/ccip-integration/reference/` - Ops supplements → `docs/ccip-integration/operations/` + +## Manual merge completion (2026-06-02) + +| Path | Result | +|------|--------| +| `docs/WETH_CREATE2_DEPLOYMENT.md` | Applied from archive patch | +| `script/reserve/DeployKeeper.s.sol` | Applied from archive patch | +| `test/emoney/unit/*.t.sol` | Restored from `.bak` (canonical had archived tests) | +| `contracts/ccip/CCIPSender.sol` | **Skipped** — canonical uses `safeIncreaseAllowance` (newer than archive `approve`) | +| `contracts/governance/{MultiSig,Voting}.sol` | **Skipped** — `Ownable(msg.sender)` already in canonical | +| `contracts/reserve/{MockPriceFeed,OraclePriceFeed,PriceFeedKeeper,ReserveTokenIntegration}.sol` | **Skipped** — canonical already includes archive fixes | +| `package.json` | **Merged** — keeper npm scripts added; full Hardhat/CCIP scripts preserved (archive would have replaced entire file) | +| Config TOML / genesis / static-nodes | **Skipped** — policy: no config drift | + +Archive patch reference remains at `~/projects/archives/smom-dbis-138-sibling-wip-20260602/git-diff.patch`. diff --git a/docs/WETH_CREATE2_DEPLOYMENT.md b/docs/WETH_CREATE2_DEPLOYMENT.md index 38dd68c..70ea7a2 100644 --- a/docs/WETH_CREATE2_DEPLOYMENT.md +++ b/docs/WETH_CREATE2_DEPLOYMENT.md @@ -1,16 +1,37 @@ # WETH9 and WETH10 CREATE2 Deployment Guide +**Last Updated**: 2025-01-27 +**Network**: ChainID 138 (DeFi Oracle Meta Mainnet) + ## Overview -This guide explains how to deploy WETH9 and WETH10 contracts to the exact addresses specified in `genesis.json` using CREATE2. +This guide explains how to deploy WETH9 and WETH10 contracts to the exact addresses specified in `genesis.json` using CREATE2. **Important**: These contracts are already pre-deployed in the genesis block with bytecode, so CREATE2 deployment may not be necessary unless you need to update or redeploy them. ## Target Addresses -From `genesis.json`: +From `config/genesis.json`: - **WETH9**: `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` - **WETH10**: `0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f` -These addresses are pre-allocated in the genesis block with balance `0x0` and no code. +## ⚠️ Important: Genesis Pre-deployment Status + +**Both contracts are already pre-deployed in the genesis block with bytecode:** + +- **WETH9** (`0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2`): Has bytecode in `genesis.json` (field: `"code"`) +- **WETH10** (`0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f`): Has bytecode in `genesis.json` (field: `"code"`) + +This means: +- ✅ Contracts are **already available** at these addresses when the chain starts +- ✅ **No CREATE2 deployment needed** unless you want to update/redeploy +- ✅ Contracts can be used immediately after genesis block + +### When CREATE2 Deployment is Needed + +CREATE2 deployment is only necessary if: +1. You need to update the contract bytecode +2. The genesis pre-deployment failed or was removed +3. You're deploying to a different network that doesn't have genesis pre-deployment +4. You want to verify the contracts match your compiled bytecode ## CREATE2 Address Calculation @@ -27,25 +48,64 @@ To deploy to the exact target addresses, we need: ## Deployment Scripts ### 1. `script/DeployWETH9ToExactAddress.s.sol` -- Attempts to find the salt that produces the WETH9 target address -- Uses CREATE2Factory to deploy -- Tries common salts first, then brute forces if needed +- **Purpose**: Deploy WETH9 to exact address using CREATE2 +- **Features**: + - Attempts to find the salt that produces the WETH9 target address + - Uses CREATE2Factory to deploy + - Tries common salts first, then brute forces if needed (up to 10,000 iterations) + - Checks if contract already exists before deploying + - Tries multiple deployer addresses (current deployer, CREATE2Factory, standard CREATE2 deployer, genesis addresses) +- **Status**: ✅ Available +- **Location**: `script/DeployWETH9ToExactAddress.s.sol` ### 2. `script/DeployWETH10ToExactAddress.s.sol` -- Attempts to find the salt that produces the WETH10 target address -- Uses CREATE2Factory to deploy -- Tries common salts first, then brute forces if needed +- **Purpose**: Deploy WETH10 to exact address using CREATE2 +- **Features**: + - Attempts to find the salt that produces the WETH10 target address + - Uses CREATE2Factory to deploy + - Tries common salts first, then brute forces if needed (up to 10,000 iterations) + - Checks if contract already exists before deploying + - Tries multiple deployer addresses +- **Status**: ✅ Available +- **Location**: `script/DeployWETH10ToExactAddress.s.sol` ### 3. `scripts/deployment/calculate-create2-salt.js` -- Node.js utility to calculate CREATE2 salt -- Can be used to find the salt that produces a target address -- Supports brute-force search +- **Purpose**: Node.js utility to calculate CREATE2 salt +- **Features**: + - Can be used to find the salt that produces a target address + - Supports brute-force search (default: 1,000,000 iterations) + - Tries common salts first (zero, one, chain ID, contract name hashes) + - Loads bytecode from Foundry artifacts +- **Status**: ✅ Available +- **Location**: `scripts/deployment/calculate-create2-salt.js` +- **Usage**: `node scripts/deployment/calculate-create2-salt.js WETH ` ### 4. `scripts/deployment/deploy-weth-create2.sh` -- Main deployment script -- Compiles contracts -- Checks if contracts already exist -- Deploys WETH9 and WETH10 sequentially +- **Purpose**: Main automated deployment script +- **Features**: + - Compiles contracts using `forge build` + - Checks if contracts already exist at target addresses + - Deploys WETH9 and WETH10 sequentially + - Verifies deployments after completion + - Skips deployment if contracts already exist +- **Status**: ✅ Available +- **Location**: `scripts/deployment/deploy-weth-create2.sh` + +### 5. `script/DeployWETH.s.sol` +- **Purpose**: Standard WETH deployment (not CREATE2) +- **Features**: Deploys WETH to a new address (not to genesis addresses) +- **Status**: ✅ Available +- **Location**: `script/DeployWETH.s.sol` +- **Note**: This deploys to a new address, not the genesis addresses + +### 6. `contracts/utils/CREATE2Factory.sol` +- **Purpose**: Factory contract for CREATE2 deployments +- **Features**: + - `deploy(bytes memory bytecode, uint256 salt)`: Deploy contract using CREATE2 + - `computeAddress(bytes memory bytecode, uint256 salt)`: Compute CREATE2 address + - `computeAddressWithDeployer(address deployer, bytes memory bytecode, uint256 salt)`: Compute with custom deployer +- **Status**: ✅ Available +- **Location**: `contracts/utils/CREATE2Factory.sol` ## Deployment Process @@ -54,7 +114,7 @@ To deploy to the exact target addresses, we need: 1. **Environment Variables** (in `.env`): ```bash PRIVATE_KEY=0x... - RPC_URL=http://localhost:8545 + RPC_URL=http://localhost:8545 # ChainID 138 RPC endpoint ``` 2. **Compiled Contracts**: @@ -62,24 +122,60 @@ To deploy to the exact target addresses, we need: forge build ``` -### Step 1: Calculate Salt (Optional) +3. **Verify Genesis Status** (Recommended): + ```bash + # Check if contracts already exist (they should from genesis) + cast code 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --rpc-url $RPC_URL + cast code 0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f --rpc-url $RPC_URL + ``` -If you know the deployer address used when creating genesis.json: +### Step 1: Verify Current Status + +**Before deploying, check if contracts already exist:** ```bash +# Check WETH9 +cast code 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --rpc-url $RPC_URL + +# Check WETH10 +cast code 0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f --rpc-url $RPC_URL + +# If contracts exist, verify they work +cast call 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 "name()" --rpc-url $RPC_URL +cast call 0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f "name()" --rpc-url $RPC_URL +``` + +**If contracts already exist and work correctly, CREATE2 deployment is NOT needed.** + +### Step 2: Calculate Salt (Optional - Only if Redeploying) + +If you need to redeploy and know the deployer address used when creating genesis.json: + +```bash +# For WETH9 node scripts/deployment/calculate-create2-salt.js WETH + +# For WETH10 node scripts/deployment/calculate-create2-salt.js WETH10 ``` -This will find the salt that produces the target addresses. +**Common deployer addresses to try:** +- `0x4e59b44847b379578588920cA78FbF26c0B4956C` (Standard CREATE2 deployer) +- `0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb` (Genesis address) +- `0xa55A4B57A91561e9df5a883D4883Bd4b1a7C4882` (Genesis address with high balance) -### Step 2: Deploy Contracts +### Step 3: Deploy Contracts (Only if Needed) **Option A: Use the automated script** ```bash ./scripts/deployment/deploy-weth-create2.sh ``` +The script will: +1. Check if contracts already exist +2. Skip deployment if they exist +3. Deploy only if contracts are missing + **Option B: Deploy manually using Foundry** ```bash # Deploy WETH9 @@ -97,15 +193,53 @@ forge script script/DeployWETH10ToExactAddress.s.sol:DeployWETH10ToExactAddress --legacy ``` +**Option C: Standard Deployment (New Address)** + +If you don't need the exact genesis addresses, use standard deployment: + +```bash +forge script script/DeployWETH.s.sol:DeployWETH \ + --rpc-url $RPC_URL \ + --broadcast \ + --private-key $PRIVATE_KEY +``` + +This will deploy to a new address (not the genesis addresses). + ## Troubleshooting +### Issue: Contracts Already Exist (Expected) + +**If contracts already exist at target addresses:** + +✅ **This is expected and correct!** The contracts are pre-deployed in genesis.json. + +**Verification:** +```bash +# Check if contracts exist +cast code 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --rpc-url $RPC_URL +cast code 0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f --rpc-url $RPC_URL + +# Verify functionality +cast call 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 "name()" --rpc-url $RPC_URL +# Should return: "Wrapped Ether" +``` + +**Action**: No deployment needed. Contracts are ready to use. + ### Issue: Salt not found If the scripts cannot find a salt that produces the target address: 1. **Check Deployer Address**: The deployer address must match the one used when calculating the genesis addresses 2. **Verify Bytecode**: Ensure the compiled bytecode matches what was used in genesis.json + ```bash + # Compare bytecode + cast code 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --rpc-url $RPC_URL > deployed-bytecode.txt + # Check compiled bytecode in out/WETH/WETH.sol/WETH.json + ``` 3. **Try Different Deployer**: If genesis.json used a different deployer, you may need to use that address +4. **Increase Search Range**: Modify the brute-force limit in the scripts (currently 10,000 iterations) ### Issue: Address mismatch @@ -114,28 +248,73 @@ If the deployed address doesn't match the target: 1. **Verify Salt**: Double-check the salt calculation 2. **Check Factory Address**: If using CREATE2Factory, ensure the factory address matches 3. **Review Genesis**: Confirm the target addresses in genesis.json are correct +4. **Check Bytecode Hash**: Ensure the bytecode hash matches what was used in genesis -### Issue: Contract already exists +### Issue: Contract deployment fails -If the contract already exists at the target address: +If CREATE2 deployment fails: -1. **Verify Deployment**: Check if the contract is already deployed -2. **Check Code**: Verify the existing code matches what you expect -3. **Skip Deployment**: The scripts will skip deployment if contracts already exist +1. **Check Factory Deployment**: Ensure CREATE2Factory is deployed first +2. **Verify Gas**: Ensure deployer has sufficient balance for gas +3. **Check Network**: Verify you're on ChainID 138 +4. **Review Logs**: Check deployment logs in `/tmp/weth9-deploy.log` and `/tmp/weth10-deploy.log` + +### Issue: Contract exists but bytecode differs + +If contract exists but bytecode doesn't match your compiled version: + +1. **This is normal** if genesis.json used different bytecode +2. **Verify functionality**: Test the contract to ensure it works +3. **If update needed**: You'll need to deploy a new version to a different address, or update genesis.json ## Alternative Approaches -### Approach 1: Direct Deployment to Pre-allocated Addresses +### Approach 1: Use Genesis Pre-deployment (Current Status) ✅ -If genesis.json pre-allocates these addresses, you might be able to deploy directly to them without CREATE2, depending on the blockchain client's implementation. +**Status**: ✅ **Already Implemented** -### Approach 2: Use Known CREATE2 Deployer +The contracts are already pre-deployed in `config/genesis.json` with bytecode in the `"code"` field. This is the recommended approach for ChainID 138. + +**Advantages**: +- ✅ Contracts available immediately at genesis +- ✅ No deployment transaction needed +- ✅ No gas costs +- ✅ Deterministic addresses + +**Disadvantages**: +- ⚠️ Bytecode is fixed in genesis (harder to update) +- ⚠️ Requires genesis file modification for updates + +### Approach 2: Standard Deployment (New Address) + +Deploy contracts to new addresses (not the genesis addresses): + +```bash +forge script script/DeployWETH.s.sol:DeployWETH \ + --rpc-url $RPC_URL \ + --broadcast \ + --private-key $PRIVATE_KEY +``` + +**Use when**: +- You don't need the exact genesis addresses +- You want to deploy updated bytecode +- You're testing on a different network + +### Approach 3: CREATE2 Deployment (For Updates) + +Use CREATE2 to deploy updated contracts to the same addresses: + +**Use when**: +- Genesis pre-deployment failed +- You need to update contract bytecode +- You want to verify contracts match your compiled version + +### Approach 4: Use Known CREATE2 Deployer Use a well-known CREATE2 deployer (like `0x4e59b44847b379578588920cA78FbF26c0B4956C`) and calculate the appropriate salt. -### Approach 3: Genesis Pre-deployment - -If the blockchain client supports it, you can include the contract bytecode directly in genesis.json instead of deploying via CREATE2. +**Standard CREATE2 Deployer**: `0x4e59b44847b379578588920cA78FbF26c0B4956C` ## Verification @@ -155,14 +334,59 @@ cast call 0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f "name()" --rpc-url $RPC_URL ## Important Notes -1. **Gas Costs**: CREATE2 deployment with salt finding can be gas-intensive, especially if brute-force is needed -2. **Deployer Balance**: Ensure the deployer account has sufficient balance for deployment -3. **Network**: Make sure you're deploying to the correct network (ChainID 138) -4. **Genesis Alignment**: The addresses in genesis.json must match the CREATE2 calculation for deployment to work +1. **Genesis Pre-deployment**: Contracts are already pre-deployed in genesis.json. CREATE2 deployment is only needed if you want to update/redeploy. + +2. **Gas Costs**: CREATE2 deployment with salt finding can be gas-intensive, especially if brute-force is needed (up to 10,000 iterations in scripts). + +3. **Deployer Balance**: Ensure the deployer account has sufficient balance for deployment (recommended: at least 0.1 ETH). + +4. **Network**: Make sure you're deploying to the correct network (ChainID 138 - DeFi Oracle Meta Mainnet). + +5. **Genesis Alignment**: The addresses in genesis.json must match the CREATE2 calculation for deployment to work. + +6. **Bytecode Matching**: The compiled bytecode must match exactly what was used in genesis.json for CREATE2 to work. + +7. **Contract Verification**: Always verify contracts exist and work before attempting CREATE2 deployment. + +8. **Salt Finding**: Finding the correct salt can be time-consuming. The scripts try common salts first, then brute-force up to 10,000 iterations. + +## Current Deployment Status + +| Contract | Address | Genesis Status | CREATE2 Needed | +|----------|---------|----------------|----------------| +| **WETH9** | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | ✅ Pre-deployed with bytecode | ❌ Not needed | +| **WETH10** | `0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f` | ✅ Pre-deployed with bytecode | ❌ Not needed | + +**Recommendation**: Verify contracts exist and work correctly. CREATE2 deployment is only needed if contracts are missing or need updates. + +## Related Files + +- **Genesis Configuration**: `config/genesis.json` +- **WETH9 Contract**: `contracts/tokens/WETH.sol` +- **WETH10 Contract**: `contracts/tokens/WETH10.sol` +- **CREATE2Factory**: `contracts/utils/CREATE2Factory.sol` +- **Deployment Scripts**: `script/DeployWETH*.s.sol` +- **Deployment Scripts**: `scripts/deployment/deploy-weth-create2.sh` +- **Salt Calculator**: `scripts/deployment/calculate-create2-salt.js` ## References - [EIP-1014: CREATE2](https://eips.ethereum.org/EIPS/eip-1014) - [Foundry CREATE2 Guide](https://getfoundry.sh/guides/deterministic-deployments-using-create2) - [Alchemy CREATE2 Guide](https://www.alchemy.com/docs/create2-an-alternative-to-deriving-contract-addresses) +- [Genesis File Documentation](https://besu.hyperledger.org/en/stable/private-networks/how-to/configure/genesis-file/) + +## Summary + +**Key Takeaway**: WETH9 and WETH10 are already pre-deployed in the genesis block with bytecode. CREATE2 deployment is **not required** unless you need to update or redeploy the contracts. Always verify contracts exist before attempting deployment. + +**Quick Start**: +1. ✅ Verify contracts exist: `cast code
--rpc-url $RPC_URL` +2. ✅ Test contracts: `cast call
"name()" --rpc-url $RPC_URL` +3. ✅ If contracts work, no deployment needed +4. ⚠️ Only deploy if contracts are missing or need updates + +--- + +**Last Updated**: 2025-01-27 diff --git a/package.json b/package.json index c4f3439..2fdd4cc 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,13 @@ "deploy:logger:cronos": "hardhat run scripts/ccip-deployment/deploy-ccip-logger-multichain.js --network cronos", "deploy:reporter:chain138": "node scripts/ccip-deployment/deploy-ccip-reporter.js", "verify:logger": "hardhat verify --network mainnet", - "verify:reporter": "hardhat verify --network chain138" + "verify:reporter": "hardhat verify --network chain138", + "keeper": "node scripts/reserve/keeper-service.js", + "keeper:monitor": "node scripts/reserve/monitor-keeper.js", + "keeper:deploy": "bash scripts/reserve/deploy-all.sh", + "keeper:deploy:dry-run": "DRY_RUN=1 bash scripts/reserve/deploy-all.sh", + "keeper:chainlink": "node scripts/reserve/chainlink-keeper-setup.js", + "keeper:gelato": "node scripts/reserve/gelato-keeper-setup.js" }, "devDependencies": { "@chainlink/contracts-ccip": "^1.6.3", diff --git a/script/reserve/DeployKeeper.s.sol b/script/reserve/DeployKeeper.s.sol index a681cac..060bd2c 100644 --- a/script/reserve/DeployKeeper.s.sol +++ b/script/reserve/DeployKeeper.s.sol @@ -40,33 +40,32 @@ contract DeployKeeper is Script { address usdcAsset = vm.envOr("USDC_ASSET", address(0)); address ethAsset = vm.envOr("ETH_ASSET", address(0)); + // Note: Asset tracking and role granting should be done after deployment + // via separate transactions to avoid prank issues in broadcast mode + address keeperAddress = vm.envOr("KEEPER_ADDRESS", deployer); + + // Grant keeper role (admin is deployer, so we can call directly) + if (admin == deployer) { + keeper.grantRole(keeper.KEEPER_ROLE(), keeperAddress); + console.log("Keeper role granted to:", keeperAddress); + } else { + console.log("Note: Grant keeper role manually to:", keeperAddress); + } + + // Note: Track assets after deployment via separate script or manual calls if (xauAsset != address(0) || usdcAsset != address(0) || ethAsset != address(0)) { console.log(""); - console.log("Tracking assets..."); - + console.log("Note: Track assets after deployment:"); if (xauAsset != address(0)) { - vm.prank(admin); - keeper.trackAsset(xauAsset); - console.log("XAU tracked:", xauAsset); + console.log(" keeper.trackAsset(", xauAsset, ")"); } - if (usdcAsset != address(0)) { - vm.prank(admin); - keeper.trackAsset(usdcAsset); - console.log("USDC tracked:", usdcAsset); + console.log(" keeper.trackAsset(", usdcAsset, ")"); } - if (ethAsset != address(0)) { - vm.prank(admin); - keeper.trackAsset(ethAsset); - console.log("ETH tracked:", ethAsset); + console.log(" keeper.trackAsset(", ethAsset, ")"); } } - - // Grant keeper role to deployer (or specified keeper address) - address keeperAddress = vm.envOr("KEEPER_ADDRESS", deployer); - vm.prank(admin); - keeper.grantRole(keeper.KEEPER_ROLE(), keeperAddress); console.log(""); console.log("Keeper role granted to:", keeperAddress); diff --git a/services/relay/.env.avax b/services/relay/.env.avax deleted file mode 100644 index 3328653..0000000 --- a/services/relay/.env.avax +++ /dev/null @@ -1,26 +0,0 @@ -# Copy to .env.avax and adjust if you need a different AVAX RPC. -# start-relay.sh avax loads this profile before .env.local / .env. -# Use the public 138 RPC for relay polling so a Core deploy-RPC restart does not strand this lane. -RPC_URL_138=https://rpc-http-pub.d-bis.org -CCIP_ROUTER_CHAIN138=0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817 -CCIPWETH9_BRIDGE_CHAIN138=0xcacfd227A040002e49e2e01626363071324f820a -SOURCE_CHAIN_SELECTOR=138 - -DEST_CHAIN_NAME=Avalanche -DEST_CHAIN_ID=43114 -DEST_RPC_URL=https://avalanche-c-chain.publicnode.com -DEST_CHAIN_SELECTOR=6433500567565415381 -DEST_RELAY_ROUTER=0x2a0023Ad5ce1Ac6072B454575996DfFb1BB11b16 -DEST_RELAY_BRIDGE=0x3f8C409C6072a2B6a4Ff17071927bA70F80c725F -DEST_WETH9_ADDRESS=0xa4B9DD039565AeD9641D45b57061f99d9cA6Df08 - -RELAYER_PRIVATE_KEY=${PRIVATE_KEY} -RELAYER_ADDRESS=0x4A666F96fC8764181194447A7dFdb7d471b301C8 - -START_BLOCK=latest -POLL_INTERVAL=5000 -CONFIRMATION_BLOCKS=1 -MAX_RETRIES=3 -RETRY_DELAY=5000 -LOG_LEVEL=info -DEST_RELAY_BRIDGE_ALLOWLIST=0x3f8C409C6072a2B6a4Ff17071927bA70F80c725F diff --git a/services/relay/.env.avax-cw b/services/relay/.env.avax-cw deleted file mode 100644 index 766c0f5..0000000 --- a/services/relay/.env.avax-cw +++ /dev/null @@ -1,25 +0,0 @@ -# Forward relay profile for non-prefunded AVAX cW minting. -# Use the public 138 RPC for relay polling so a Core deploy-RPC restart does not strand this lane. -RPC_URL_138=https://rpc-http-pub.d-bis.org -CCIP_ROUTER_CHAIN138=0x42dab7b888dd382bd5adcf9e038dbf1fd03b4817 -SOURCE_BRIDGE_ADDRESS=0x152ed3e9912161b76bdfd368d0c84b7c31c10de7 -SOURCE_CHAIN_SELECTOR=138 - -DEST_CHAIN_NAME=Avalanche -DEST_CHAIN_ID=43114 -DEST_RPC_URL=https://avalanche-c-chain.publicnode.com -DEST_CHAIN_SELECTOR=6433500567565415381 -DEST_RELAY_ROUTER=0xc9158759a7e3621f6bb191bf5d77605d6e25b410 -DEST_RELAY_BRIDGE=0x635002c5fb227160cd2eac926d1baa61847f3c75 -DEST_WETH9_ADDRESS=0xa4b9dd039565aed9641d45b57061f99d9ca6df08 - -RELAYER_PRIVATE_KEY=${PRIVATE_KEY} -RELAYER_ADDRESS=0x4A666F96fC8764181194447A7dFdb7d471b301C8 - -START_BLOCK=3411398 -POLL_INTERVAL=5000 -CONFIRMATION_BLOCKS=1 -MAX_RETRIES=3 -RETRY_DELAY=5000 -LOG_LEVEL=info -DEST_RELAY_BRIDGE_ALLOWLIST=0x635002c5Fb227160Cd2eAC926d1BaA61847f3C75 diff --git a/services/relay/.env.avax-to-138 b/services/relay/.env.avax-to-138 deleted file mode 100644 index ba465ad..0000000 --- a/services/relay/.env.avax-to-138 +++ /dev/null @@ -1,29 +0,0 @@ -# Reverse relay profile for AVAX cW burns back to Chain 138. -SOURCE_CHAIN_NAME=Avalanche -SOURCE_CHAIN_ID=43114 -SOURCE_CHAIN_SELECTOR=6433500567565415381 -SOURCE_RPC_URL=https://api.avax.network/ext/bc/C/rpc -SOURCE_ROUTER_ADDRESS=0x1773125b280d296354f4f4b958a7cfc4e5975b60 -SOURCE_BRIDGE_ADDRESS=0x635002c5fb227160cd2eac926d1baa61847f3c75 -SOURCE_LOGS_MAX_BLOCK_RANGE=2048 - -DEST_CHAIN_NAME=Chain 138 -DEST_CHAIN_ID=138 -DEST_CHAIN_SELECTOR=138 -DEST_RPC_URL=http://192.168.11.211:8545 -DEST_RELAY_ROUTER=0xe75d26bc558a28442f30750c6d97bffb46f39abc -DEST_RELAY_BRIDGE=0x152ed3e9912161b76bdfd368d0c84b7c31c10de7 -DEST_RELAY_BRIDGE_ALLOWLIST=0x152ed3e9912161b76bdfd368d0c84b7c31c10de7 - -RELAYER_PRIVATE_KEY=${PRIVATE_KEY} -RELAYER_ADDRESS=0x4A666F96fC8764181194447A7dFdb7d471b301C8 - -START_BLOCK=latest -POLL_INTERVAL=5000 -CONFIRMATION_BLOCKS=1 -MAX_RETRIES=3 -RETRY_DELAY=5000 -LOG_LEVEL=info -RELAY_DEST_LEGACY_TX=1 -RELAY_DEST_GAS_PRICE_BUMP_PCT=20 -RELAY_DEST_GAS_PRICE_BUMP_WEI=1000000 diff --git a/services/relay/.env.mainnet-cw b/services/relay/.env.mainnet-cw deleted file mode 100644 index ab68576..0000000 --- a/services/relay/.env.mainnet-cw +++ /dev/null @@ -1,28 +0,0 @@ -# Explicit Mainnet cW mint worker. -# This profile watches the dedicated Chain 138 cW sender bridge and only delivers to the Mainnet cW receiver. - -RPC_URL_138=https://rpc-http-pub.d-bis.org -CCIP_ROUTER_CHAIN138=0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817 -SOURCE_BRIDGE_ADDRESS=0x152ed3e9912161b76bdfd368d0c84b7c31c10de7 -SOURCE_CHAIN_SELECTOR=138 - -# Publicnode avoids Infura 429 during mesh replay bursts (operator mesh scripts may share the same Infura key). -RPC_URL_MAINNET=https://ethereum-rpc.publicnode.com -DEST_CHAIN_NAME=Ethereum Mainnet -DEST_CHAIN_ID=1 -DEST_CHAIN_SELECTOR=5009297550715157269 -DEST_RELAY_ROUTER=0x416564Ab73Ad5710855E98dC7bC7Bff7387285BA -DEST_RELAY_BRIDGE=0x2bF74583206A49Be07E0E8A94197C12987AbD7B5 -DEST_RELAY_BRIDGE_ALLOWLIST=0x2bF74583206A49Be07E0E8A94197C12987AbD7B5 - -RELAYER_PRIVATE_KEY=${PRIVATE_KEY} -RELAYER_ADDRESS=0x4A666F96fC8764181194447A7dFdb7d471b301C8 - -# After mesh replay completes, set START_BLOCK=latest for cold starts. -# Mesh batch lockAndSend cluster ~block 0x514ff6 (5320694); use 5320000 for replay. -START_BLOCK=latest -POLL_INTERVAL=5000 -CONFIRMATION_BLOCKS=1 -MAX_RETRIES=3 -RETRY_DELAY=5000 -LOG_LEVEL=info diff --git a/services/relay/.env.mainnet-weth b/services/relay/.env.mainnet-weth deleted file mode 100644 index c4805df..0000000 --- a/services/relay/.env.mainnet-weth +++ /dev/null @@ -1,37 +0,0 @@ -# Explicit Mainnet WETH release worker. -# This profile skips `.env.local` in systemd so WETH behavior does not inherit ad hoc cW overrides. - -RPC_URL_138=https://rpc-http-pub.d-bis.org -CCIP_ROUTER_CHAIN138=0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817 -SOURCE_BRIDGE_ADDRESS=0xcacfd227A040002e49e2e01626363071324f820a -SOURCE_CHAIN_SELECTOR=138 - -RPC_URL_MAINNET=https://mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286 -DEST_CHAIN_NAME=Ethereum Mainnet -DEST_CHAIN_ID=1 -DEST_CHAIN_SELECTOR=5009297550715157269 -DEST_RELAY_ROUTER=0x416564Ab73Ad5710855E98dC7bC7Bff7387285BA -DEST_RELAY_BRIDGE=0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939 -CCIP_RELAY_BRIDGE_LINK_MAINNET=0x2cd963d54a7Af576Fea71292f961Bf2604f3583A -DEST_RELAY_BRIDGE_LINK=0x2cd963d54a7Af576Fea71292f961Bf2604f3583A -DEST_RELAY_BRIDGE_ALLOWLIST=0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939,0x2cd963d54a7Af576Fea71292f961Bf2604f3583A - -RELAYER_PRIVATE_KEY=${PRIVATE_KEY} -RELAYER_ADDRESS=0x4A666F96fC8764181194447A7dFdb7d471b301C8 - -START_BLOCK=latest -POLL_INTERVAL=5000 -CONFIRMATION_BLOCKS=1 -MAX_RETRIES=3 -RETRY_DELAY=5000 - -# Keep the WETH lane observably alive but safe until the Mainnet release bridge is funded again. -RELAY_SHEDDING=0 -RELAY_DELIVERY_ENABLED=1 -RELAY_ENFORCE_BRIDGE_TOKEN_BALANCE=1 - -# Park the known oversized WETH release messages until Mainnet bridge inventory policy changes. -# Keep the worker on forward-only monitoring so restarts do not reload old underfunded backlog. -RELAY_SKIP_MESSAGE_IDS=0xf718c9895c0a5442349996383184d017d2fa041af7aaeb9f0c0675d3ceed756b,0x19656fe758fc0e36ce5ce16ad9101e76c9eae19e5ed6bea08335dfb664215edc,0x249042e74fc322b2a8dc9fabe63b18094df11aaaed86b149287a6feea1b94157,0x263fa601b709c1c71a78004936eb195b43ed9da4dce23cf12dcfd24d40880375,0x300d38035aebd97bfbfa13737dc60ed23dba91991348259fd01ea1bc3109b260,0x3d3e6978c9e796b23fb8709fff4102131648825728ad0dd4197b98c6ba7a46cc,0x42fad60f851a43c6a52a216d211679d6fb786130f34dc5f26e7ddad350e7c83e,0x47b36fc517e7055efbc7408b17fca08f5fa41dbeea24d72e02f4995e22a4601f,0x4a4cab9082800ddb10ac60cae94dc2c5a6509134e6c8f915dc0ff636752b449d,0x523f8a202f069644488747dbd2a221cafdfac3f0a0fc7271685f5b23736fe8eb,0x5571002da2d2202280b11df4c772978decd007c18e0eacc5f80839f4be95dc65,0x63e56db9e3d6f2864e284b32c84ffa7118c65bee0559567cddf3288d812ef3cc,0x6ae76d1ec258666a1e6a95e63d911f4178f34ec312dcb88e3f237ba1288e6f79,0x75881681e8b2c793a8386f471cad44768c4e6f125e3f888978cc4c14d74049cf,0x770e246987c22c32fd2c9627c37e28316ec3390b33fb9cb9e9c3f21670af5ba3,0x779894438af9602eee92bc6c9c02475d6659ab9ed4bdd7250e6d0d331e628366,0x781eb1072c501efc10f92be6dc3355cf95d2f6f0c992468275d69fc5ded52a30,0x7a49b584a1966c9c568036169b227a2293b74132a21bcfbd253b2e8d621f1dde,0xa447bdb1962882920ca8e966d7e8ba0cc016b80252bff5d5741317f0484a74fd,0xb11ca230b35d706eb0a43dc99c8806647aaeef29cdfa14762fa2a397bcbe82ae,0xc076289b0120a9b010e7851c4b00566ecdc8f46f2108d87ebebd042a005fb250,0xcc5ec02070b51ff927e540c62b2aa0c4b4f237efc8b34bbd6a5e8827f57f0a0b,0xd606e745392b1385870bcf5c7a1177833d2872a4e1a0beb33319e5b645be5b12 - -LOG_LEVEL=info diff --git a/test/emoney/unit/AccountWalletRegistryTest.t.sol b/test/emoney/unit/AccountWalletRegistryTest.t.sol new file mode 100644 index 0000000..7772f98 --- /dev/null +++ b/test/emoney/unit/AccountWalletRegistryTest.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {AccountWalletRegistry} from "@emoney/AccountWalletRegistry.sol"; +import {IAccountWalletRegistry} from "@emoney/interfaces/IAccountWalletRegistry.sol"; + +contract AccountWalletRegistryTest is Test { + AccountWalletRegistry public registry; + address public admin; + address public accountManager; + + bytes32 public accountRefId1 = keccak256("account1"); + bytes32 public walletRefId1 = keccak256("wallet1"); + bytes32 public walletRefId2 = keccak256("wallet2"); + bytes32 public provider1 = keccak256("METAMASK"); + bytes32 public provider2 = keccak256("FIREBLOCKS"); + + function setUp() public { + admin = address(0x1); + accountManager = address(0x2); + + registry = new AccountWalletRegistry(admin); + + vm.startPrank(admin); + registry.grantRole(registry.ACCOUNT_MANAGER_ROLE(), accountManager); + vm.stopPrank(); + } + + function test_linkAccountToWallet() public { + vm.expectEmit(true, true, false, true); + emit AccountWalletLinked(accountRefId1, walletRefId1, provider1, uint64(block.timestamp)); + + vm.prank(accountManager); + registry.linkAccountToWallet(accountRefId1, walletRefId1, provider1); + + assertTrue(registry.isLinked(accountRefId1, walletRefId1)); + assertTrue(registry.isActive(accountRefId1, walletRefId1)); + + IAccountWalletRegistry.WalletLink[] memory wallets = registry.getWallets(accountRefId1); + assertEq(wallets.length, 1); + assertEq(wallets[0].walletRefId, walletRefId1); + assertEq(wallets[0].provider, provider1); + assertTrue(wallets[0].active); + } + + function test_linkMultipleWallets() public { + vm.prank(accountManager); + registry.linkAccountToWallet(accountRefId1, walletRefId1, provider1); + + vm.prank(accountManager); + registry.linkAccountToWallet(accountRefId1, walletRefId2, provider2); + + IAccountWalletRegistry.WalletLink[] memory wallets = registry.getWallets(accountRefId1); + assertEq(wallets.length, 2); + assertEq(wallets[0].walletRefId, walletRefId1); + assertEq(wallets[1].walletRefId, walletRefId2); + } + + function test_unlinkAccountFromWallet() public { + vm.prank(accountManager); + registry.linkAccountToWallet(accountRefId1, walletRefId1, provider1); + + assertTrue(registry.isActive(accountRefId1, walletRefId1)); + + vm.expectEmit(true, true, false, false); + emit AccountWalletUnlinked(accountRefId1, walletRefId1); + + vm.prank(accountManager); + registry.unlinkAccountFromWallet(accountRefId1, walletRefId1); + + assertTrue(registry.isLinked(accountRefId1, walletRefId1)); // Still linked + assertFalse(registry.isActive(accountRefId1, walletRefId1)); // But inactive + } + + function test_getAccounts() public { + bytes32 accountRefId2 = keccak256("account2"); + + vm.prank(accountManager); + registry.linkAccountToWallet(accountRefId1, walletRefId1, provider1); + + vm.prank(accountManager); + registry.linkAccountToWallet(accountRefId2, walletRefId1, provider1); + + bytes32[] memory accounts = registry.getAccounts(walletRefId1); + assertEq(accounts.length, 2); + } + + function test_linkAccountToWallet_reactivate() public { + vm.prank(accountManager); + registry.linkAccountToWallet(accountRefId1, walletRefId1, provider1); + + vm.prank(accountManager); + registry.unlinkAccountFromWallet(accountRefId1, walletRefId1); + + assertFalse(registry.isActive(accountRefId1, walletRefId1)); + + // Reactivate + vm.prank(accountManager); + registry.linkAccountToWallet(accountRefId1, walletRefId1, provider1); + + assertTrue(registry.isActive(accountRefId1, walletRefId1)); + } + + // Helper events for testing (match IAccountWalletRegistry events) + event AccountWalletLinked( + bytes32 indexed accountRefId, + bytes32 indexed walletRefId, + bytes32 provider, + uint64 linkedAt + ); + + event AccountWalletUnlinked(bytes32 indexed accountRefId, bytes32 indexed walletRefId); +} + diff --git a/test/emoney/unit/ISO20022RouterTest.t.sol b/test/emoney/unit/ISO20022RouterTest.t.sol new file mode 100644 index 0000000..1444724 --- /dev/null +++ b/test/emoney/unit/ISO20022RouterTest.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ISO20022Router} from "@emoney/ISO20022Router.sol"; +import {IISO20022Router} from "@emoney/interfaces/IISO20022Router.sol"; +import {RailTriggerRegistry} from "@emoney/RailTriggerRegistry.sol"; +import {IRailTriggerRegistry} from "@emoney/interfaces/IRailTriggerRegistry.sol"; +import {RailTypes} from "@emoney/libraries/RailTypes.sol"; +import {ISO20022Types} from "@emoney/libraries/ISO20022Types.sol"; + +contract ISO20022RouterTest is Test { + ISO20022Router public router; + RailTriggerRegistry public triggerRegistry; + address public admin; + address public railOperator; + address public token; + + function setUp() public { + admin = address(0x1); + railOperator = address(0x2); + token = address(0x100); + + triggerRegistry = new RailTriggerRegistry(admin); + router = new ISO20022Router(admin, address(triggerRegistry)); + + vm.startPrank(admin); + triggerRegistry.grantRole(triggerRegistry.RAIL_OPERATOR_ROLE(), address(router)); + router.grantRole(router.RAIL_OPERATOR_ROLE(), railOperator); + vm.stopPrank(); + } + + function test_submitOutbound() public { + IISO20022Router.CanonicalMessage memory m = IISO20022Router.CanonicalMessage({ + msgType: ISO20022Types.PAIN_001, + instructionId: keccak256("instruction1"), + endToEndId: keccak256("e2e1"), + msgId: bytes32(0), + uetr: bytes32(0), + accountRefId: keccak256("account1"), + counterpartyRefId: keccak256("counterparty1"), + debtorId: bytes32(0), + creditorId: bytes32(0), + purpose: bytes32(0), + settlementMethod: bytes32(0), + categoryPurpose: bytes32(0), + token: token, + amount: 1000, + currencyCode: keccak256("USD"), + payloadHash: keccak256("payload1") + }); + + vm.expectEmit(true, true, false, true); + emit OutboundSubmitted(0, ISO20022Types.PAIN_001, keccak256("instruction1"), keccak256("account1")); + + vm.prank(railOperator); + uint256 triggerId = router.submitOutbound(m); + + assertEq(triggerId, 0); + IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); + assertEq(trigger.instructionId, keccak256("instruction1")); + assertEq(trigger.msgType, ISO20022Types.PAIN_001); + } + + function test_submitInbound() public { + IISO20022Router.CanonicalMessage memory m = IISO20022Router.CanonicalMessage({ + msgType: ISO20022Types.CAMT_054, + instructionId: keccak256("instruction2"), + endToEndId: keccak256("e2e2"), + msgId: bytes32(0), + uetr: bytes32(0), + accountRefId: keccak256("account2"), + counterpartyRefId: keccak256("counterparty2"), + debtorId: bytes32(0), + creditorId: bytes32(0), + purpose: bytes32(0), + settlementMethod: bytes32(0), + categoryPurpose: bytes32(0), + token: token, + amount: 2000, + currencyCode: keccak256("EUR"), + payloadHash: keccak256("payload2") + }); + + vm.expectEmit(true, true, false, true); + emit InboundSubmitted(0, ISO20022Types.CAMT_054, keccak256("instruction2"), keccak256("account2")); + + vm.prank(railOperator); + uint256 triggerId = router.submitInbound(m); + + assertEq(triggerId, 0); + IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); + assertEq(trigger.instructionId, keccak256("instruction2")); + assertEq(trigger.msgType, ISO20022Types.CAMT_054); + } + + function test_getTriggerIdByInstructionId() public { + IISO20022Router.CanonicalMessage memory m = IISO20022Router.CanonicalMessage({ + msgType: ISO20022Types.PAIN_001, + instructionId: keccak256("instruction3"), + endToEndId: bytes32(0), + msgId: bytes32(0), + uetr: bytes32(0), + accountRefId: keccak256("account3"), + counterpartyRefId: bytes32(0), + debtorId: bytes32(0), + creditorId: bytes32(0), + purpose: bytes32(0), + settlementMethod: bytes32(0), + categoryPurpose: bytes32(0), + token: token, + amount: 3000, + currencyCode: keccak256("USD"), + payloadHash: bytes32(0) + }); + + vm.prank(railOperator); + uint256 triggerId = router.submitOutbound(m); + + assertEq(router.getTriggerIdByInstructionId(keccak256("instruction3")), triggerId); + } + + // Helper events for testing (match IISO20022Router events) + event InboundSubmitted(uint256 indexed triggerId, bytes32 msgType, bytes32 instructionId, bytes32 accountRefId); + event OutboundSubmitted(uint256 indexed triggerId, bytes32 msgType, bytes32 instructionId, bytes32 accountRefId); +} + diff --git a/test/emoney/unit/RailEscrowVaultTest.t.sol b/test/emoney/unit/RailEscrowVaultTest.t.sol new file mode 100644 index 0000000..7d4b45d --- /dev/null +++ b/test/emoney/unit/RailEscrowVaultTest.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {RailEscrowVault} from "@emoney/RailEscrowVault.sol"; +import {IRailEscrowVault} from "@emoney/interfaces/IRailEscrowVault.sol"; +import {RailTypes} from "@emoney/libraries/RailTypes.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor() ERC20("Mock Token", "MOCK") { + _mint(msg.sender, 1000000 * 10**18); + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +contract RailEscrowVaultTest is Test { + RailEscrowVault public vault; + MockERC20 public token; + address public admin; + address public settlementOperator; + address public user; + + function setUp() public { + admin = address(0x1); + settlementOperator = address(0x2); + user = address(0x10); + + vault = new RailEscrowVault(admin); + token = new MockERC20(); + + vm.startPrank(admin); + vault.grantRole(vault.SETTLEMENT_OPERATOR_ROLE(), settlementOperator); + vm.stopPrank(); + + // Give user some tokens + token.mint(user, 10000 * 10**18); + } + + function test_lock() public { + uint256 amount = 1000 * 10**18; + uint256 triggerId = 1; + + vm.startPrank(user); + token.approve(address(vault), amount); + vm.stopPrank(); + + vm.expectEmit(true, true, false, true); + emit Locked(address(token), user, amount, triggerId, uint8(RailTypes.Rail.SWIFT)); + + vm.prank(settlementOperator); + vault.lock(address(token), user, amount, triggerId, RailTypes.Rail.SWIFT); + + assertEq(vault.getEscrowAmount(address(token), triggerId), amount); + assertEq(vault.getTotalEscrow(address(token)), amount); + assertEq(token.balanceOf(address(vault)), amount); + } + + function test_release() public { + uint256 amount = 1000 * 10**18; + uint256 triggerId = 1; + address recipient = address(0x20); + + vm.startPrank(user); + token.approve(address(vault), amount); + vm.stopPrank(); + + vm.prank(settlementOperator); + vault.lock(address(token), user, amount, triggerId, RailTypes.Rail.SWIFT); + + uint256 recipientBalanceBefore = token.balanceOf(recipient); + + vm.expectEmit(true, true, false, true); + emit Released(address(token), recipient, amount, triggerId); + + vm.prank(settlementOperator); + vault.release(address(token), recipient, amount, triggerId); + + assertEq(vault.getEscrowAmount(address(token), triggerId), 0); + assertEq(vault.getTotalEscrow(address(token)), 0); + assertEq(token.balanceOf(recipient), recipientBalanceBefore + amount); + } + + function test_release_insufficientEscrow() public { + uint256 amount = 1000 * 10**18; + uint256 triggerId = 1; + + vm.startPrank(user); + token.approve(address(vault), amount); + vm.stopPrank(); + + vm.prank(settlementOperator); + vault.lock(address(token), user, amount, triggerId, RailTypes.Rail.SWIFT); + + vm.prank(settlementOperator); + vm.expectRevert("RailEscrowVault: insufficient escrow"); + vault.release(address(token), address(0x20), amount + 1, triggerId); + } + + // Helper events for testing (match IRailEscrowVault events) + event Locked(address indexed token, address indexed account, uint256 amount, uint256 indexed triggerId, uint8 rail); + event Released(address indexed token, address indexed recipient, uint256 amount, uint256 indexed triggerId); +} + diff --git a/test/emoney/unit/RailTriggerRegistryTest.t.sol b/test/emoney/unit/RailTriggerRegistryTest.t.sol new file mode 100644 index 0000000..23f0c2a --- /dev/null +++ b/test/emoney/unit/RailTriggerRegistryTest.t.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {RailTriggerRegistry} from "@emoney/RailTriggerRegistry.sol"; +import {IRailTriggerRegistry} from "@emoney/interfaces/IRailTriggerRegistry.sol"; +import {RailTypes} from "@emoney/libraries/RailTypes.sol"; + +contract RailTriggerRegistryTest is Test { + RailTriggerRegistry public registry; + address public admin; + address public railOperator; + address public railAdapter; + address public token; + + function setUp() public { + admin = address(0x1); + railOperator = address(0x2); + railAdapter = address(0x3); + token = address(0x100); + + registry = new RailTriggerRegistry(admin); + + vm.startPrank(admin); + registry.grantRole(registry.RAIL_OPERATOR_ROLE(), railOperator); + registry.grantRole(registry.RAIL_ADAPTER_ROLE(), railAdapter); + vm.stopPrank(); + } + + function test_createTrigger() public { + IRailTriggerRegistry.Trigger memory t = IRailTriggerRegistry.Trigger({ + id: 0, + rail: RailTypes.Rail.SWIFT, + msgType: keccak256("pacs.008"), + accountRefId: keccak256("account1"), + walletRefId: bytes32(0), + token: token, + amount: 1000, + currencyCode: keccak256("USD"), + instructionId: keccak256("instruction1"), + state: RailTypes.State.CREATED, + createdAt: 0, + updatedAt: 0 + }); + + vm.expectEmit(true, true, false, true); + emit TriggerCreated( + 0, + uint8(RailTypes.Rail.SWIFT), + keccak256("pacs.008"), + keccak256("instruction1"), + keccak256("account1"), + token, + 1000 + ); + + vm.prank(railOperator); + uint256 id = registry.createTrigger(t); + + assertEq(id, 0); + IRailTriggerRegistry.Trigger memory retrieved = registry.getTrigger(id); + assertEq(uint8(retrieved.rail), uint8(RailTypes.Rail.SWIFT)); + assertEq(retrieved.msgType, keccak256("pacs.008")); + assertEq(retrieved.amount, 1000); + assertEq(uint8(retrieved.state), uint8(RailTypes.State.CREATED)); + } + + function test_createTrigger_duplicateInstructionId() public { + IRailTriggerRegistry.Trigger memory t = IRailTriggerRegistry.Trigger({ + id: 0, + rail: RailTypes.Rail.SWIFT, + msgType: keccak256("pacs.008"), + accountRefId: keccak256("account1"), + walletRefId: bytes32(0), + token: token, + amount: 1000, + currencyCode: keccak256("USD"), + instructionId: keccak256("instruction1"), + state: RailTypes.State.CREATED, + createdAt: 0, + updatedAt: 0 + }); + + vm.prank(railOperator); + registry.createTrigger(t); + + vm.prank(railOperator); + vm.expectRevert("RailTriggerRegistry: duplicate instructionId"); + registry.createTrigger(t); + } + + function test_updateState() public { + IRailTriggerRegistry.Trigger memory t = IRailTriggerRegistry.Trigger({ + id: 0, + rail: RailTypes.Rail.SWIFT, + msgType: keccak256("pacs.008"), + accountRefId: keccak256("account1"), + walletRefId: bytes32(0), + token: token, + amount: 1000, + currencyCode: keccak256("USD"), + instructionId: keccak256("instruction1"), + state: RailTypes.State.CREATED, + createdAt: 0, + updatedAt: 0 + }); + + vm.prank(railOperator); + uint256 id = registry.createTrigger(t); + + vm.expectEmit(true, false, false, true); + emit TriggerStateUpdated(id, uint8(RailTypes.State.CREATED), uint8(RailTypes.State.VALIDATED), bytes32(0)); + + vm.prank(railAdapter); + registry.updateState(id, RailTypes.State.VALIDATED, bytes32(0)); + + IRailTriggerRegistry.Trigger memory retrieved = registry.getTrigger(id); + assertEq(uint8(retrieved.state), uint8(RailTypes.State.VALIDATED)); + } + + function test_updateState_invalidTransition() public { + IRailTriggerRegistry.Trigger memory t = IRailTriggerRegistry.Trigger({ + id: 0, + rail: RailTypes.Rail.SWIFT, + msgType: keccak256("pacs.008"), + accountRefId: keccak256("account1"), + walletRefId: bytes32(0), + token: token, + amount: 1000, + currencyCode: keccak256("USD"), + instructionId: keccak256("instruction1"), + state: RailTypes.State.CREATED, + createdAt: 0, + updatedAt: 0 + }); + + vm.prank(railOperator); + uint256 id = registry.createTrigger(t); + + vm.prank(railAdapter); + vm.expectRevert("RailTriggerRegistry: invalid state transition"); + registry.updateState(id, RailTypes.State.SETTLED, bytes32(0)); + } + + function test_instructionIdExists() public { + IRailTriggerRegistry.Trigger memory t = IRailTriggerRegistry.Trigger({ + id: 0, + rail: RailTypes.Rail.SWIFT, + msgType: keccak256("pacs.008"), + accountRefId: keccak256("account1"), + walletRefId: bytes32(0), + token: token, + amount: 1000, + currencyCode: keccak256("USD"), + instructionId: keccak256("instruction1"), + state: RailTypes.State.CREATED, + createdAt: 0, + updatedAt: 0 + }); + + assertFalse(registry.instructionIdExists(keccak256("instruction1"))); + + vm.prank(railOperator); + registry.createTrigger(t); + + assertTrue(registry.instructionIdExists(keccak256("instruction1"))); + } + + // Helper events for testing (match IRailTriggerRegistry events) + event TriggerCreated( + uint256 indexed id, + uint8 rail, + bytes32 msgType, + bytes32 instructionId, + bytes32 accountRefId, + address token, + uint256 amount + ); + + event TriggerStateUpdated(uint256 indexed id, uint8 oldState, uint8 newState, bytes32 reason); +} + diff --git a/test/emoney/unit/SettlementOrchestratorTest.t.sol b/test/emoney/unit/SettlementOrchestratorTest.t.sol new file mode 100644 index 0000000..328c013 --- /dev/null +++ b/test/emoney/unit/SettlementOrchestratorTest.t.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {SettlementOrchestrator} from "@emoney/SettlementOrchestrator.sol"; +import {ISettlementOrchestrator} from "@emoney/interfaces/ISettlementOrchestrator.sol"; +import {RailTriggerRegistry} from "@emoney/RailTriggerRegistry.sol"; +import {IRailTriggerRegistry} from "@emoney/interfaces/IRailTriggerRegistry.sol"; +import {RailEscrowVault} from "@emoney/RailEscrowVault.sol"; +import {AccountWalletRegistry} from "@emoney/AccountWalletRegistry.sol"; +import {PolicyManager} from "@emoney/PolicyManager.sol"; +import {DebtRegistry} from "@emoney/DebtRegistry.sol"; +import {ComplianceRegistry} from "@emoney/ComplianceRegistry.sol"; +import {RailTypes} from "@emoney/libraries/RailTypes.sol"; +import {ReasonCodes} from "@emoney/libraries/ReasonCodes.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor() ERC20("Mock Token", "MOCK") { + _mint(msg.sender, 1000000 * 10**18); + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +contract SettlementOrchestratorTest is Test { + SettlementOrchestrator public orchestrator; + RailTriggerRegistry public triggerRegistry; + RailEscrowVault public escrowVault; + AccountWalletRegistry public accountWalletRegistry; + PolicyManager public policyManager; + DebtRegistry public debtRegistry; + ComplianceRegistry public complianceRegistry; + MockERC20 public token; + + address public admin; + address public settlementOperator; + address public railAdapter; + address public user; + address public issuer; + + bytes32 public accountRefId = keccak256("account1"); + bytes32 public instructionId = keccak256("instruction1"); + + function setUp() public { + admin = address(0x1); + settlementOperator = address(0x2); + railAdapter = address(0x3); + user = address(0x10); + issuer = address(0x20); + + // Deploy core contracts + complianceRegistry = new ComplianceRegistry(admin); + debtRegistry = new DebtRegistry(admin); + policyManager = new PolicyManager(admin, address(complianceRegistry), address(debtRegistry)); + triggerRegistry = new RailTriggerRegistry(admin); + escrowVault = new RailEscrowVault(admin); + accountWalletRegistry = new AccountWalletRegistry(admin); + orchestrator = new SettlementOrchestrator( + admin, + address(triggerRegistry), + address(escrowVault), + address(accountWalletRegistry), + address(policyManager), + address(debtRegistry), + address(complianceRegistry) + ); + + token = new MockERC20(); + token.mint(user, 10000 * 10**18); + + // Set up roles + vm.startPrank(admin); + triggerRegistry.grantRole(triggerRegistry.RAIL_OPERATOR_ROLE(), settlementOperator); + triggerRegistry.grantRole(triggerRegistry.RAIL_ADAPTER_ROLE(), railAdapter); + triggerRegistry.grantRole(triggerRegistry.RAIL_ADAPTER_ROLE(), address(orchestrator)); // Orchestrator needs this to call updateState + escrowVault.grantRole(escrowVault.SETTLEMENT_OPERATOR_ROLE(), address(orchestrator)); + orchestrator.grantRole(orchestrator.SETTLEMENT_OPERATOR_ROLE(), settlementOperator); + orchestrator.grantRole(orchestrator.RAIL_ADAPTER_ROLE(), railAdapter); + debtRegistry.grantRole(debtRegistry.DEBT_AUTHORITY_ROLE(), address(orchestrator)); + complianceRegistry.grantRole(complianceRegistry.COMPLIANCE_ROLE(), admin); + vm.stopPrank(); + + // Set up compliance + vm.prank(admin); + complianceRegistry.setCompliance(user, true, 1, keccak256("US")); + } + + function test_validateAndLock_vaultMode() public { + // Create trigger + IRailTriggerRegistry.Trigger memory t = IRailTriggerRegistry.Trigger({ + id: 0, + rail: RailTypes.Rail.SWIFT, + msgType: keccak256("pacs.008"), + accountRefId: accountRefId, + walletRefId: bytes32(0), + token: address(token), + amount: 1000 * 10**18, + currencyCode: keccak256("USD"), + instructionId: instructionId, + state: RailTypes.State.CREATED, + createdAt: 0, + updatedAt: 0 + }); + + vm.prank(settlementOperator); + uint256 triggerId = triggerRegistry.createTrigger(t); + + // Approve vault to spend tokens + vm.startPrank(user); + token.approve(address(escrowVault), 1000 * 10**18); + vm.stopPrank(); + + // Note: validateAndLock needs account address resolution + // This test demonstrates the flow, but in production you'd need to set up account mapping + // For now, we'll skip the actual validation test and test the state transitions + } + + function test_markSubmitted() public { + // Create and validate trigger + IRailTriggerRegistry.Trigger memory t = IRailTriggerRegistry.Trigger({ + id: 0, + rail: RailTypes.Rail.SWIFT, + msgType: keccak256("pacs.008"), + accountRefId: accountRefId, + walletRefId: bytes32(0), + token: address(token), + amount: 1000 * 10**18, + currencyCode: keccak256("USD"), + instructionId: instructionId, + state: RailTypes.State.CREATED, + createdAt: 0, + updatedAt: 0 + }); + + vm.prank(settlementOperator); + uint256 triggerId = triggerRegistry.createTrigger(t); + + // Update to VALIDATED state + vm.prank(railAdapter); + triggerRegistry.updateState(triggerId, RailTypes.State.VALIDATED, ReasonCodes.OK); + + bytes32 railTxRef = keccak256("railTx1"); + + // markSubmitted emits Submitted event, but also calls updateState twice which emits other events + // We'll check the event was emitted by checking the result instead of using vm.expectEmit + vm.prank(railAdapter); + orchestrator.markSubmitted(triggerId, railTxRef); + + assertEq(orchestrator.getRailTxRef(triggerId), railTxRef); + } + + function test_confirmSettled_inbound() public { + // Note: This test is skipped because _resolveAccountAddress always returns address(0) + // which causes validateAndLock and confirmSettled to fail for inbound flows. + // In production, _resolveAccountAddress needs to be properly implemented to decode + // walletRefId to an address or use AccountWalletRegistry properly. + // This test demonstrates the limitation - inbound flows require account resolution. + // For now, we test outbound flows which don't require account resolution for confirmSettled. + } + + function test_confirmRejected() public { + IRailTriggerRegistry.Trigger memory t = IRailTriggerRegistry.Trigger({ + id: 0, + rail: RailTypes.Rail.SWIFT, + msgType: keccak256("pacs.008"), + accountRefId: accountRefId, + walletRefId: bytes32(0), + token: address(token), + amount: 1000 * 10**18, + currencyCode: keccak256("USD"), + instructionId: instructionId, + state: RailTypes.State.CREATED, + createdAt: 0, + updatedAt: 0 + }); + + vm.prank(settlementOperator); + uint256 triggerId = triggerRegistry.createTrigger(t); + + vm.prank(railAdapter); + triggerRegistry.updateState(triggerId, RailTypes.State.VALIDATED, ReasonCodes.OK); + + bytes32 reason = keccak256("REJECTED"); + + // confirmRejected emits Rejected event, but also calls updateState which emits other events + // We'll check the state was updated correctly instead of using vm.expectEmit + vm.prank(railAdapter); + orchestrator.confirmRejected(triggerId, reason); + + IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); + assertEq(uint8(trigger.state), uint8(RailTypes.State.REJECTED)); + } + + // Helper events for testing (match ISettlementOrchestrator events) + event Submitted(uint256 indexed triggerId, bytes32 railTxRef); + event Rejected(uint256 indexed triggerId, bytes32 reason); +} +