Complete archive manual merge follow-ups and secure relay env handling
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m4s
CI/CD Pipeline / Security Scanning (push) Successful in 2m13s
CI/CD Pipeline / Lint and Format (push) Failing after 34s
CI/CD Pipeline / Terraform Validation (push) Failing after 21s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 22s
Validation / validate-genesis (push) Successful in 26s
Validation / validate-terraform (push) Failing after 24s
Validation / validate-kubernetes (push) Failing after 8s
Validation / validate-smart-contracts (push) Failing after 8s
Validation / validate-security (push) Failing after 1m10s
Validation / validate-documentation (push) Failing after 15s

Apply clean archive patches (WETH CREATE2 doc, DeployKeeper script),
restore emoney unit tests from .bak, add keeper npm scripts without
replacing the Hardhat package.json, untrack relay lane secret env files,
and document superseded patch hunks in SIBLING_WIP_IMPORT.md.
This commit is contained in:
defiQUG
2026-06-02 06:40:12 -07:00
parent 1f9fe09c5a
commit e254f81a83
15 changed files with 1042 additions and 200 deletions

11
.gitignore vendored
View File

@@ -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

View File

@@ -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`.

View File

@@ -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 <deployer-address>`
### 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 <deployer-address>
# For WETH10
node scripts/deployment/calculate-create2-salt.js WETH10 <deployer-address>
```
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 <address> --rpc-url $RPC_URL`
2. ✅ Test contracts: `cast call <address> "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

View File

@@ -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",

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}