chore: .gitignore and README updates

Made-with: Cursor
This commit is contained in:
defiQUG
2026-04-21 22:00:55 -07:00
parent 843cdbf71c
commit 768168de5e
37 changed files with 505 additions and 118 deletions

1
.gitignore vendored
View File

@@ -29,6 +29,7 @@ datadir/
# Foundry
out/
artifacts/
.omnl-poller-state.json
cache/
broadcast/
.cronos-verify/

View File

@@ -578,6 +578,7 @@ See [ALL Mainnet Master Documentation](../docs/MASTER_INDEX.md) for complete int
- [Financial Tokenization (Archived)](docs/archive/status-reports/operations-legacy/FINANCIAL_TOKENIZATION.md) - Historical ISO-20022 and SWIFT FIN tokenization notes
- [ALL Mainnet Integration](../docs/MASTER_INDEX.md) - Complete ALL Mainnet (651940) integration guide
- [Multi-Chain Deployment](docs/deployment/MULTI_CHAIN_DEPLOYMENT_GUIDE.md) - Multi-chain deployment guide
- [HYBX OMNL (reserves, compliance, CCIP mirror)](docs/hybx-omnl/README.md) - Policy, deployment, IPSAS anchor, token-aggregation routes
### 📊 Operations & Runbooks

View File

@@ -1,8 +1,8 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Desired-state pool spec for Chain 138 DODO PMM. Scripts should create and register only missing pools from this file, not redeploy contracts.",
"version": "1.0.0",
"updated": "2026-04-15",
"version": "1.1.0",
"updated": "2026-04-19",
"chainId": 138,
"defaults": {
"lpFeeRate": 3,
@@ -11,6 +11,7 @@
"enableTwap": false
},
"tokens": {
"cBTC": "0xe94260c555aC1d9D3CC9E1632883452ebDf0082E",
"cUSDT": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22",
"cUSDC": "0xf22258f57794CC8E06237084b353Ab30fFfa640b",
"cEURC": "0x8085961F9cF02b4d800A3c6d386D31da4B34266a",
@@ -55,6 +56,9 @@
}
},
"explicitPairs": [
{ "baseSymbol": "cBTC", "quoteSymbol": "cUSDT" },
{ "baseSymbol": "cBTC", "quoteSymbol": "cUSDC" },
{ "baseSymbol": "cBTC", "quoteSymbol": "cXAUC" },
{ "baseSymbol": "cUSDT", "quoteSymbol": "cUSDC" },
{ "baseSymbol": "cUSDT", "quoteSymbol": "USDT" },
{ "baseSymbol": "cUSDC", "quoteSymbol": "USDC" },
@@ -70,6 +74,12 @@
{ "baseSymbol": "cXAUT", "quoteSymbol": "cUSDC" },
{ "baseSymbol": "cEURC", "quoteSymbol": "cEURT" },
{ "baseSymbol": "cGBPC", "quoteSymbol": "cGBPT" },
{ "baseSymbol": "cXAUC", "quoteSymbol": "cXAUT" }
]
{ "baseSymbol": "cXAUC", "quoteSymbol": "cXAUT" },
{ "baseSymbol": "WETH", "quoteSymbol": "USDT" },
{ "baseSymbol": "WETH", "quoteSymbol": "USDC" },
{ "baseSymbol": "cUSDT", "quoteSymbol": "WETH" },
{ "baseSymbol": "cUSDC", "quoteSymbol": "WETH" },
{ "baseSymbol": "cEURT", "quoteSymbol": "WETH" }
],
"plannedPairs": []
}

View File

@@ -53,9 +53,9 @@ Per [Contract Deployment and Verification](https://docs.cronos.org/for-dapp-deve
- Requires API key in Hardhat config
### 3. Foundry
- `forge verify-contract --verifier blockscout` fails (Cronos Explorer API format differs)
- Run `./scripts/deployment/verify-cronos-contracts.sh` to attempt; falls back to manual verification
- **Recommended**: Manual verification via the web interface above
- `forge verify-contract` does not successfully submit verification to Cronos Explorer (legacy Etherscan-style POST returns 404; Etherscan v2 does not support chain id 25).
- Run `./scripts/deployment/verify-cronos-contracts.sh` to confirm bytecode on RPC and print manual steps (optional `CRONOS_EXPORT_SOURCES=1` to regenerate Standard-JSON).
- **Recommended**: Manual verification via the web interface above and `docs/deployment/CRONOS_VERIFICATION_RUNBOOK.md`
---

View File

@@ -66,6 +66,16 @@ This is the **master index** of all project documentation. Use this as your star
- **[36-REGION-BLUEPRINT.md](deployment/36-REGION-BLUEPRINT.md)** - 36-region deployment blueprint
- **[DEPLOYMENT_CONFIGURATION_AUDIT.md](deployment/DEPLOYMENT_CONFIGURATION_AUDIT.md)** - Configuration audit
### HYBX OMNL (reserves, compliance, CCIP mirror)
- **[DEPLOYMENT_CHECKLIST.md](hybx-omnl/DEPLOYMENT_CHECKLIST.md)** — OMNL deploy and token-aggregation checklist
- **[OMNL_RECONCILE_CRON_AND_CI.md](hybx-omnl/OMNL_RECONCILE_CRON_AND_CI.md)** — IPSAS/journal anchor: cron, artifact script, GitHub Actions
- **[OPERATIONAL_COMPLIANCE.md](hybx-omnl/OPERATIONAL_COMPLIANCE.md)** — Webhooks, retention, reconciliation anchors
- **[OMNL_IPSAS_API.md](hybx-omnl/OMNL_IPSAS_API.md)** — IPSAS / GL API surface
- **[CCIP_MIRROR_FLOW.md](hybx-omnl/CCIP_MIRROR_FLOW.md)** — CCIP mirror flow
- **[HYBX_OMNL_POLICY_SPEC.md](hybx-omnl/HYBX_OMNL_POLICY_SPEC.md)** — Policy parameters (M0/M1 rules)
- **[EXTERNAL_AUDIT_CHECKLIST.md](hybx-omnl/EXTERNAL_AUDIT_CHECKLIST.md)** — Third-party audit scope
---
## 🔧 Operations

View File

@@ -37,7 +37,7 @@ besu.d-bis.org. 1 IN A 70.153.83.83 ; cf_tags=cf-proxied:true
blockscout.d-bis.org. 1 IN A 20.215.32.42 ; cf_tags=cf-proxied:true
blockscout.d-bis.org. 1 IN A 70.153.83.83 ; cf_tags=cf-proxied:true
d-bis.org. 1 IN A 20.215.32.42 ; cf_tags=cf-proxied:true
d-bis.org. 1 IN A 20.215.32.15 ; Digital Bank of International Settlements - Defi Oracle Meta Mainnet cf_tags=cf-proxied:true
d-bis.org. 1 IN A 20.215.32.15 ; Digital Bank of International Settlements - DeFi Oracle Meta Mainnet cf_tags=cf-proxied:true
docs.d-bis.org. 1 IN A 20.8.47.226 ; cf_tags=cf-proxied:false
explorer.d-bis.org. 1 IN A 20.215.32.42 ; cf_tags=cf-proxied:true
explorer.d-bis.org. 1 IN A 70.153.83.83 ; cf_tags=cf-proxied:true

View File

@@ -72,10 +72,16 @@ Repeat for each destination chain (BSC, Polygon, Base, Optimism, Avalanche, Cron
---
## 4. LINK Funding
## 4. LINK Funding and Activation Scope
- CCIP charges fees for each cross-chain message; bridges typically pay in **LINK** (or native) depending on the contracts `feeToken` and `ccipSend` usage.
- **Per chain**: Ensure the **bridge contract** holds enough **LINK** (and native for gas) so it can pay CCIP fees when users call `sendCrossChain` (or equivalent).
- Important distinction:
- **Destination-only receipt** on a chain does not by itself require LINK on that chain's bridge contract, because `ccipReceive(...)` does not pay the CCIP fee.
- **Outbound sending** from a chain does require a fee payment path on that chain's bridge contract.
- Current WEMIX nuance:
- `CCIPWETH9Bridge` may be configured for native-fee mode by setting `feeToken = address(0)`.
- `CCIPWETH10Bridge` still requires LINK in the current implementation.
- **How to fund**:
1. Get LINK on the same chain as the bridge (e.g. from an exchange or faucet).
2. Transfer LINK to the **bridge contract address** (the same address you use for `addDestination`), or use any approved mechanism (e.g. admin top-up function if the contract has one).

View File

@@ -84,6 +84,12 @@ This index helps you find the right deployment guide for your needs.
- [Cloud for Sovereignty Landing Zone](CLOUD_FOR_SOVEREIGNTY_LANDING_ZONE.md) - Cloud for sovereignty landing zone
- [Deployment Firefly Cacti](DEPLOYMENT_FIREFLY_CACTI.md) - Firefly and Cacti deployment
### HYBX OMNL (reserve commitments, compliance, CCIP mirror, IPSAS GL)
- [HYBX OMNL documentation index](../hybx-omnl/README.md) - All OMNL guides in one place
- [OMNL deployment checklist](../hybx-omnl/DEPLOYMENT_CHECKLIST.md) - Contracts, env, API, verification
- [OMNL reconcile (cron & CI)](../hybx-omnl/OMNL_RECONCILE_CRON_AND_CI.md) - Anchor hash artifacts and automation
- [OMNL IPSAS / GL API](../hybx-omnl/OMNL_IPSAS_API.md) - Reporting API surface
## Historical/Status Reports
These are historical deployment reports and status documents. Consider archiving if older than 6 months:

View File

@@ -18,40 +18,50 @@ PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
# =============================================================================
# RPC Endpoints
# =============================================================================
# Infura project (used for supported public chains)
INFURA_PROJECT_ID=43b945b33d58463a9246cf5ca8aa6286
INFURA_PROJECT_SECRET=
INFURA_GAS_API=https://gas.api.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
# Ethereum Mainnet
ETH_MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY
# Alternative: ETH_MAINNET_RPC_URL=https://mainnet.infura.io/v3/YOUR_INFURA_KEY
ETH_MAINNET_RPC_URL=https://mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
ETHEREUM_MAINNET_RPC=https://mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
# Cronos (Crypto.com)
CRONOS_RPC_URL=https://evm.cronos.org
CRONOS_MAINNET_RPC=https://evm.cronos.org
# BSC (BNB Smart Chain)
BSC_RPC_URL=https://bsc-dataseed1.binance.org
# Alternative: BSC_RPC_URL=https://bsc-dataseed.binance.org
BSC_RPC_URL=https://bsc-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
BSC_MAINNET_RPC=https://bsc-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
# Polygon PoS
POLYGON_RPC_URL=https://polygon-rpc.com
# Alternative: POLYGON_RPC_URL=https://rpc-mainnet.maticvigil.com
POLYGON_RPC_URL=https://polygon-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
POLYGON_MAINNET_RPC=https://polygon-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
# Gnosis Chain (PoA)
GNOSIS_RPC_URL=https://rpc.gnosischain.com
# Alternative: GNOSIS_RPC_URL=https://xdai-archive.blockscout.com
GNOSIS_MAINNET_RPC=https://rpc.gnosischain.com
# Avalanche C-Chain
AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc
# Alternative: AVALANCHE_RPC_URL=https://avalanche-mainnet.infura.io/v3/YOUR_KEY
AVALANCHE_RPC_URL=https://avalanche-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
AVALANCHE_MAINNET_RPC=https://avalanche-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
# Base
BASE_RPC_URL=https://mainnet.base.org
# Alternative: BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
BASE_RPC_URL=https://base-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
BASE_MAINNET_RPC=https://base-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
# Arbitrum One
ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc
# Alternative: ARBITRUM_RPC_URL=https://arbitrum-mainnet.infura.io/v3/YOUR_KEY
ARBITRUM_RPC_URL=https://arbitrum-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
ARBITRUM_MAINNET_RPC=https://arbitrum-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
# Optimism
OPTIMISM_RPC_URL=https://mainnet.optimism.io
# Alternative: OPTIMISM_RPC_URL=https://optimism-mainnet.infura.io/v3/YOUR_KEY
OPTIMISM_RPC_URL=https://optimism-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
OPTIMISM_MAINNET_RPC=https://optimism-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
# Celo
CELO_RPC_URL=https://celo-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
CELO_MAINNET_RPC=https://celo-mainnet.infura.io/v3/43b945b33d58463a9246cf5ca8aa6286
# Chain-138 (DeFi Oracle Meta Mainnet)
# For deployment use Core RPC (IP:port); do not use FQDN (DNS/tunnel can fail).
@@ -229,6 +239,11 @@ CCIPLOGGER_MAINNET=
# =============================================================================
# Default: CW_BRIDGE_ADDRESS=0x0; use per-chain overrides (best practice). In .env, CW_BRIDGE_<CHAIN> are set from the deployed bridge suite (CCIP_RELAY_BRIDGE_MAINNET, CCIPWETH9_BRIDGE_*).
# Per-chain: CW_BRIDGE_MAINNET, CW_BRIDGE_CRONOS, CW_BRIDGE_BSC, CW_BRIDGE_POLYGON, CW_BRIDGE_GNOSIS, CW_BRIDGE_AVALANCHE, CW_BRIDGE_BASE, CW_BRIDGE_ARBITRUM, CW_BRIDGE_OPTIMISM
# Bridge-only operational-role defaults for new cW* deployments:
CW_STRICT_MODE=1
CW_FREEZE_OPERATIONAL_ROLES=1
# Optional: governance multisig/controller that should hold admin and governance metadata roles on newer cW* deployments
CW_GOVERNANCE_ADMIN=
# After deploy, set:
# CWUSDT_CRONOS=0x...
# CWUSDC_CRONOS=0x...
@@ -338,4 +353,3 @@ WETH10_ADDRESS=${WETH10_MAINNET}
```
3. Never commit `.env` to version control!

View File

@@ -6,6 +6,13 @@
# `bash scripts/forge/scope.sh ...` so Forge only sees the contract subtree you
# are actively editing.
src = "contracts"
# Vendored Uniswap V2 trees pin solc 0.5.x / 0.6.x; skip lets `forge test` use 0.8.20
# without legacy solc installs. Nothing outside vendor/ imports these paths.
skip = [
"contracts/vendor/uniswap-v2-core/**/*.sol",
"contracts/vendor/uniswap-v2-periphery/**/*.sol",
"contracts/vendor/sushiswap-v2/**/*.sol",
]
out = "out"
libs = ["lib"]
solc = "0.8.20"
@@ -53,6 +60,15 @@ evm_version = "cancun"
optimizer = true
optimizer_runs = 100
via_ir = true
# Match default execution target (Chain 138 / Besu uses Cancun-capable clients).
evm_version = "cancun"
[profile.cronos_legacy]
optimizer = true
optimizer_runs = 100
via_ir = true
# Cronos chain 25 currently needs pre-Shanghai bytecode; target Paris to avoid PUSH0.
evm_version = "paris"
# RPC endpoints — use: forge create ... --rpc-url chain138
# Prevents default localhost:8545 when ETH_RPC_URL not set
@@ -66,6 +82,7 @@ cronos = "https://evm.cronos.org"
[etherscan]
avalanche = { key = "${SNOWTRACE_API_KEY:-${ETHERSCAN_API_KEY}}", url = "https://api.snowtrace.io/api" }
arbitrum = { key = "${ARBISCAN_API_KEY:-${ETHERSCAN_API_KEY}}", url = "https://api.arbiscan.io/api" }
# Cronos mainnet (chain 25). Etherscan-style API; chainid required for v2.
# API key from explorer.cronos.org/register (CRONOSCAN_API_KEY)
# Cronos mainnet (chain 25). Legacy entry — forge verify-contract does not
# successfully submit to Cronos Explorer (see CRONOS_EXPLORER_OPERATIONS.md).
# Kept for cast/ethers compatibility if tooling expects [etherscan].cronos.
cronos = { key = "${CRONOSCAN_API_KEY:-${ETHERSCAN_API_KEY}}", url = "https://explorer-api.cronos.org/mainnet/api?chainid=25" }

View File

@@ -27,6 +27,6 @@ describe('frontend network config', () => {
expect(networks.chain2138TestnetEnabled).toBe(true)
expect(networks.frontendSourceChainIds).toEqual([138, 2138])
expect(networks.defaultFrontendChainId).toBe(2138)
expect(networks.defaultFrontendChainName).toBe('Defi Oracle Meta Testnet')
expect(networks.defaultFrontendChainName).toBe('DeFi Oracle Meta Testnet')
})
})

View File

@@ -30,10 +30,10 @@ const configuredDefaultFrontendChainId = Number(
import.meta.env.VITE_DEFAULT_FRONTEND_CHAIN_ID || 138
)
/** Chain 2138 - Defi Oracle Meta Testnet (optional; enable with VITE_ENABLE_CHAIN2138) */
/** Chain 2138 - DeFi Oracle Meta Testnet (optional; enable with VITE_ENABLE_CHAIN2138) */
export const chain2138Testnet = defineChain({
id: 2138,
name: 'Defi Oracle Meta Testnet',
name: 'DeFi Oracle Meta Testnet',
network: 'chain2138-testnet',
nativeCurrency: {
decimals: 18,
@@ -46,7 +46,7 @@ export const chain2138Testnet = defineChain({
},
blockExplorers: {
default: {
name: 'Defi Oracle Meta Testnet Explorer',
name: 'DeFi Oracle Meta Testnet Explorer',
url: explorerUrl2138,
},
},

View File

@@ -24,6 +24,11 @@
"forge:test:vault": "FORGE_SCOPE=vault bash scripts/forge/scope.sh test --match-path 'test/vault/Ledger.t.sol'",
"forge:test:iso": "FORGE_SCOPE=iso4217w bash scripts/forge/scope.sh test --match-path 'test/iso4217w/*.t.sol'",
"forge:test:quick": "FORGE_SCOPE=vault bash scripts/forge/scope.sh test --match-contract LedgerTest",
"forge:test:omnl": "bash scripts/forge/scope.sh test hybx-omnl",
"omnl:verify": "bash scripts/hybx-omnl/verify-deployment.sh",
"omnl:reconcile:artifact": "bash scripts/hybx-omnl/omnl-reconcile-artifact.sh",
"omnl:validate-cross-chain": "node scripts/hybx-omnl/validate-cross-chain-config.mjs",
"omnl:sync-publish": "bash scripts/hybx-omnl/sync-to-publish.sh",
"dodo-pools:create": "bash scripts/create-all-dodo-pools-from-token-api.sh",
"dodo-pools:dry-run": "DRY_RUN=true bash scripts/create-all-dodo-pools-from-token-api.sh",
"prereqs": "bash scripts/deployment/ensure-prerequisites.sh",

View File

@@ -12,9 +12,9 @@ import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToke
* Env:
* PRIVATE_KEY (required)
* CW_BRIDGE_ADDRESS (required) — address that can mint/burn (e.g. CCIP receiver or custom bridge)
* CW_STRICT_MODE=1 (optional) — revoke deployer MINTER/BURNER after bridge grant
* CW_STRICT_MODE=0 (optional override) — by default deployer MINTER/BURNER are revoked after bridge grant
* CW_GOVERNANCE_ADMIN=0x... (optional) — grant DEFAULT_ADMIN_ROLE to governance; if strict, revoke deployer admin when governance is set
* CW_FREEZE_OPERATIONAL_ROLES=1 (optional) — freeze future MINTER/BURNER changes after setup
* CW_FREEZE_OPERATIONAL_ROLES=0 (optional override) — by default freeze future MINTER/BURNER changes after setup
* DEPLOY_CWUSDT=1, DEPLOY_CWUSDC=1, DEPLOY_CWUSDW=1, DEPLOY_CWEURC=1, ... (default all 1; set 0 to skip a token)
*/
contract DeployCWTokens is Script {
@@ -24,8 +24,8 @@ contract DeployCWTokens is Script {
uint256 pk = vm.envUint("PRIVATE_KEY");
address deployer = vm.addr(pk);
address bridge = vm.envAddress("CW_BRIDGE_ADDRESS");
bool strictMode = vm.envOr("CW_STRICT_MODE", uint256(0)) == 1;
bool freezeOperationalRoles = vm.envOr("CW_FREEZE_OPERATIONAL_ROLES", uint256(0)) == 1;
bool strictMode = vm.envOr("CW_STRICT_MODE", uint256(1)) == 1;
bool freezeOperationalRoles = vm.envOr("CW_FREEZE_OPERATIONAL_ROLES", uint256(1)) == 1;
address governanceAdmin = vm.envOr("CW_GOVERNANCE_ADMIN", address(0));
require(bridge != address(0), "CW_BRIDGE_ADDRESS required");

View File

@@ -14,6 +14,9 @@ import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToke
* CW_TOKEN_NAME (required)
* CW_TOKEN_SYMBOL (required)
* CW_TOKEN_DECIMALS (optional, default 6)
* CW_STRICT_MODE=0 (optional override) — by default deployer MINTER/BURNER are revoked after bridge grant
* CW_GOVERNANCE_ADMIN=0x... (optional) — grant DEFAULT_ADMIN_ROLE to governance; if strict, revoke deployer admin when governance is set
* CW_FREEZE_OPERATIONAL_ROLES=0 (optional override) — by default freeze future MINTER/BURNER changes after setup
*/
contract DeploySingleCWToken is Script {
function run() external {
@@ -23,6 +26,9 @@ contract DeploySingleCWToken is Script {
string memory tokenName = vm.envString("CW_TOKEN_NAME");
string memory tokenSymbol = vm.envString("CW_TOKEN_SYMBOL");
uint8 decimals_ = uint8(vm.envOr("CW_TOKEN_DECIMALS", uint256(6)));
bool strictMode = vm.envOr("CW_STRICT_MODE", uint256(1)) == 1;
bool freezeOperationalRoles = vm.envOr("CW_FREEZE_OPERATIONAL_ROLES", uint256(1)) == 1;
address governanceAdmin = vm.envOr("CW_GOVERNANCE_ADMIN", address(0));
require(bridge != address(0), "CW_BRIDGE_ADDRESS required");
require(bytes(tokenName).length != 0, "CW_TOKEN_NAME required");
@@ -34,10 +40,32 @@ contract DeploySingleCWToken is Script {
token.grantRole(token.MINTER_ROLE(), bridge);
token.grantRole(token.BURNER_ROLE(), bridge);
if (strictMode) {
token.revokeRole(token.MINTER_ROLE(), admin);
token.revokeRole(token.BURNER_ROLE(), admin);
}
if (governanceAdmin != address(0) && governanceAdmin != admin) {
token.grantRole(token.DEFAULT_ADMIN_ROLE(), governanceAdmin);
}
if (freezeOperationalRoles) {
token.freezeOperationalRoles();
}
if (strictMode && governanceAdmin != address(0) && governanceAdmin != admin) {
token.revokeRole(token.DEFAULT_ADMIN_ROLE(), admin);
}
vm.stopBroadcast();
console.log(tokenSymbol, address(token));
console.log(" bridge", bridge);
console.log(" strictMode", strictMode);
console.log(" governanceAdmin", governanceAdmin);
console.log(" operationalRolesFrozen", token.operationalRolesFrozen());
console.log(" deployerHasMinter", token.hasRole(token.MINTER_ROLE(), admin));
console.log(" deployerHasBurner", token.hasRole(token.BURNER_ROLE(), admin));
console.log(" bridgeHasMinter", token.hasRole(token.MINTER_ROLE(), bridge));
console.log(" bridgeHasBurner", token.hasRole(token.BURNER_ROLE(), bridge));
}

View File

@@ -7,9 +7,10 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title AddLiquidityPMMPoolsChain138
* @notice Add liquidity to the three DODO PMM pools on Chain 138 (cUSDT/cUSDC, cUSDT/USDT, cUSDC/USDC).
* @notice Add liquidity to the canonical DODO PMM pools on Chain 138, including the stable/WETH lanes.
* @dev Env: PRIVATE_KEY, RPC_URL_138, DODO_PMM_INTEGRATION_ADDRESS (or DODO_PMM_INTEGRATION),
* POOL_CUSDTCUSDC, POOL_CUSDTUSDT, POOL_CUSDCUSDC,
* optional POOL_CUSDTWETH, POOL_CUSDCWETH, POOL_CEURTWETH,
* ADD_LIQUIDITY_BASE_AMOUNT, ADD_LIQUIDITY_QUOTE_AMOUNT (e.g. 1000000e6 for 1M units, 6 decimals).
* Optional: ADD_LIQUIDITY_CUSDTCUSDC_BASE, ADD_LIQUIDITY_CUSDTCUSDC_QUOTE, etc. for per-pool amounts.
* Optional: NEXT_NONCE — set to pending nonce (e.g. after mints) to avoid -32001 "Nonce too low" on broadcast.
@@ -31,6 +32,9 @@ contract AddLiquidityPMMPoolsChain138 is Script {
address poolCusdtCusdc = vm.envOr("POOL_CUSDTCUSDC", address(0));
address poolCusdtUsdt = vm.envOr("POOL_CUSDTUSDT", address(0));
address poolCusdcUsdc = vm.envOr("POOL_CUSDCUSDC", address(0));
address poolCusdtWeth = vm.envOr("POOL_CUSDTWETH", address(0));
address poolCusdcWeth = vm.envOr("POOL_CUSDCWETH", address(0));
address poolCeurtWeth = vm.envOr("POOL_CEURTWETH", address(0));
uint256 defaultBase = vm.envOr("ADD_LIQUIDITY_BASE_AMOUNT", uint256(0));
uint256 defaultQuote = vm.envOr("ADD_LIQUIDITY_QUOTE_AMOUNT", uint256(0));
@@ -40,6 +44,8 @@ contract AddLiquidityPMMPoolsChain138 is Script {
address cusdc = integration.compliantUSDC();
address usdt = integration.officialUSDT();
address usdc = integration.officialUSDC();
address ceurt = vm.envOr("CEURT_ADDRESS_138", address(0xdf4b71c61E5912712C1Bdd451416B9aC26949d72));
address weth = vm.envOr("WETH9_ADDRESS_138", vm.envOr("WETH_ADDRESS_138", address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)));
// On Chain 138, cUSDT/USDT and cUSDC/USDC should point at live local mirror quote tokens.
// If the configured quote-side addresses have no code on 138, skip those pools to avoid "call to non-contract".
@@ -73,6 +79,30 @@ contract AddLiquidityPMMPoolsChain138 is Script {
console.log("Added liquidity to cUSDC/USDC pool:", poolCusdcUsdc);
}
}
if (poolCusdtWeth != address(0) && (defaultBase > 0 || defaultQuote > 0)) {
uint256 b = vm.envOr("ADD_LIQUIDITY_CUSDTWETH_BASE", defaultBase);
uint256 q = vm.envOr("ADD_LIQUIDITY_CUSDTWETH_QUOTE", defaultQuote);
if (b > 0 && q > 0) {
_approveAndAdd(integration, cusdt, weth, poolCusdtWeth, b, q);
console.log("Added liquidity to cUSDT/WETH pool:", poolCusdtWeth);
}
}
if (poolCusdcWeth != address(0) && (defaultBase > 0 || defaultQuote > 0)) {
uint256 b = vm.envOr("ADD_LIQUIDITY_CUSDCWETH_BASE", defaultBase);
uint256 q = vm.envOr("ADD_LIQUIDITY_CUSDCWETH_QUOTE", defaultQuote);
if (b > 0 && q > 0) {
_approveAndAdd(integration, cusdc, weth, poolCusdcWeth, b, q);
console.log("Added liquidity to cUSDC/WETH pool:", poolCusdcWeth);
}
}
if (poolCeurtWeth != address(0) && (defaultBase > 0 || defaultQuote > 0)) {
uint256 b = vm.envOr("ADD_LIQUIDITY_CEURTWETH_BASE", defaultBase);
uint256 q = vm.envOr("ADD_LIQUIDITY_CEURTWETH_QUOTE", defaultQuote);
if (b > 0 && q > 0) {
_approveAndAdd(integration, ceurt, weth, poolCeurtWeth, b, q);
console.log("Added liquidity to cEURT/WETH pool:", poolCeurtWeth);
}
}
vm.stopBroadcast();
}

View File

@@ -22,9 +22,9 @@ interface IDODOPMMPoolQuote {
* FLASH_QUOTE_AMOUNT_RAW gross USDC borrowed / pushed (6 decimals raw)
*
* Optional:
* POOL_CWUSDC_USDC_MAINNET default 0x69776fc607e9edA8042e320e7e43f54d06c68f0E
* CWUSDC_MAINNET default canonical cWUSDC
* USDC_MAINNET default official USDC
* MAX_FLASH_QUOTE_AMOUNT_RAW optional tighter local cap; must not exceed the defended-lane safe cap
* MIN_OUT_PMM if unset, derived from querySellQuote(receiver, amount) * MIN_OUT_PMM_NUM / MIN_OUT_PMM_DEN (defaults 985/1000)
* MIN_OUT_PMM_NUM / MIN_OUT_PMM_DEN
* MIN_OUT_UNWIND if unset, amount + ceil(amount * AAVE_FLASH_PREMIUM_BPS / 10000) + MIN_OUT_UNWIND_BUFFER_RAW
@@ -50,6 +50,7 @@ contract RunMainnetAaveCwusdcUsdcQuotePushOnce is Script {
address internal constant DEFAULT_POOL = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E;
address internal constant DEFAULT_CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
address internal constant DEFAULT_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
uint256 internal constant DEFENDED_SAFE_CAP_RAW = 2_964_298;
function run() external {
uint256 pk = vm.envUint("PRIVATE_KEY");
@@ -60,6 +61,9 @@ contract RunMainnetAaveCwusdcUsdcQuotePushOnce is Script {
address usdc = vm.envOr("USDC_MAINNET", DEFAULT_USDC);
address unwinder = vm.envAddress("QUOTE_PUSH_EXTERNAL_UNWINDER_MAINNET");
uint256 amount = vm.envUint("FLASH_QUOTE_AMOUNT_RAW");
uint256 localCap = vm.envOr("MAX_FLASH_QUOTE_AMOUNT_RAW", DEFENDED_SAFE_CAP_RAW);
_validateDefendedLane(pool, amount, localCap);
uint256 minPmmNum = vm.envOr("MIN_OUT_PMM_NUM", uint256(985));
uint256 minPmmDen = vm.envOr("MIN_OUT_PMM_DEN", uint256(1000));
@@ -150,4 +154,10 @@ contract RunMainnetAaveCwusdcUsdcQuotePushOnce is Script {
AaveQuotePushFlashReceiver(receiver).flashQuotePush(usdc, amount, p);
vm.stopBroadcast();
}
function _validateDefendedLane(address pool, uint256 amount, uint256 localCap) internal pure {
require(pool == DEFAULT_POOL, "defended pool only");
require(localCap <= DEFENDED_SAFE_CAP_RAW, "local cap exceeds defended safe cap");
require(amount <= localCap, "flash amount exceeds cap");
}
}

View File

@@ -31,6 +31,7 @@ contract RunManagedMainnetAaveCwusdcUsdcQuotePushCycle is Script {
address internal constant DEFAULT_POOL = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E;
address internal constant DEFAULT_CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
address internal constant DEFAULT_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
uint256 internal constant DEFENDED_SAFE_CAP_RAW = 2_964_298;
function run() external {
uint256 pk = vm.envUint("PRIVATE_KEY");
@@ -42,9 +43,12 @@ contract RunManagedMainnetAaveCwusdcUsdcQuotePushCycle is Script {
address usdc = vm.envOr("USDC_MAINNET", DEFAULT_USDC);
address unwinder = vm.envAddress("QUOTE_PUSH_EXTERNAL_UNWINDER_MAINNET");
uint256 amount = vm.envUint("FLASH_QUOTE_AMOUNT_RAW");
uint256 localCap = vm.envOr("MAX_FLASH_QUOTE_AMOUNT_RAW", DEFENDED_SAFE_CAP_RAW);
bool harvest = vm.envOr("QUOTE_PUSH_TREASURY_HARVEST", uint256(1)) == 1;
uint256 gasHoldbackTargetRaw = vm.envOr("QUOTE_PUSH_TREASURY_GAS_HOLDBACK_TARGET_RAW", uint256(0));
_validateDefendedLane(pool, amount, localCap);
QuotePushTreasuryManager manager = QuotePushTreasuryManager(managerAddr);
AaveQuotePushFlashReceiver.QuotePushParams memory p =
_loadQuotePushParams(receiver, pool, integration, baseToken, unwinder, amount);
@@ -70,6 +74,12 @@ contract RunManagedMainnetAaveCwusdcUsdcQuotePushCycle is Script {
console.log("receiverSweepableAfter", manager.receiverSweepableQuote());
}
function _validateDefendedLane(address pool, uint256 amount, uint256 localCap) internal pure {
require(pool == DEFAULT_POOL, "defended pool only");
require(localCap <= DEFENDED_SAFE_CAP_RAW, "local cap exceeds defended safe cap");
require(amount <= localCap, "flash amount exceeds cap");
}
function _loadQuotePushParams(
address receiver,
address pool,

View File

@@ -95,6 +95,14 @@ contract ImportProviderPoolsToIntegration is Script {
_importIfNeeded(json, providerSource, integration, explicitBase, explicitQuote, lpFeeRate, initialPrice, kFactor, enableTwap);
}
for (uint256 i = 0; json.keyExists(string.concat(".plannedPairs[", vm.toString(i), "]")); i++) {
string memory baseKey = string.concat(".plannedPairs[", vm.toString(i), "].baseSymbol");
string memory quoteKey = string.concat(".plannedPairs[", vm.toString(i), "].quoteSymbol");
string memory plannedBase = json.readString(baseKey);
string memory plannedQuote = json.readString(quoteKey);
_importIfNeeded(json, providerSource, integration, plannedBase, plannedQuote, lpFeeRate, initialPrice, kFactor, enableTwap);
}
vm.stopBroadcast();
}

View File

@@ -28,6 +28,14 @@ if [[ ${#CHAIN_FILTER[@]} -eq 0 && -n "${DEPLOY_PMM_L2S_FILTER:-}" ]]; then
for n in $DEPLOY_PMM_L2S_FILTER; do n="$(normalize_chain_name "$n")"; [[ -n "$n" ]] && CHAIN_FILTER+=("$n"); done
fi
forge_profile_for_chain() {
local chain_id="$1"
case "$chain_id" in
25) printf '%s' "cronos_legacy" ;;
*) printf '%s' "${FOUNDRY_PROFILE:-default}" ;;
esac
}
CHAINS=(
"BSC:56:BSC_RPC_URL"
"POLYGON:137:POLYGON_MAINNET_RPC"
@@ -35,18 +43,25 @@ CHAINS=(
"OPTIMISM:10:OPTIMISM_MAINNET_RPC"
"ARBITRUM:42161:ARBITRUM_MAINNET_RPC"
"AVALANCHE:43114:AVALANCHE_RPC_URL"
"CRONOS:25:CRONOS_RPC_URL"
"CRONOS:25:CRONOS_RPC_URL|CRONOS_RPC"
"GNOSIS:100:GNOSIS_MAINNET_RPC"
"CELO:42220:CELO_MAINNET_RPC"
)
for entry in "${CHAINS[@]}"; do
IFS=: read -r name chain_id rpc_var <<< "$entry"
IFS=: read -r name chain_id rpc_vars <<< "$entry"
if [[ ${#CHAIN_FILTER[@]} -gt 0 ]] && [[ ! " ${CHAIN_FILTER[*]} " =~ " $name " ]]; then continue; fi
rpc="${!rpc_var:-}"
rpc=""
IFS='|' read -r -a rpc_candidates <<< "$rpc_vars"
for rpc_var in "${rpc_candidates[@]}"; do
if [[ -n "${!rpc_var:-}" ]]; then
rpc="${!rpc_var}"
break
fi
done
if [[ -z "$rpc" ]]; then
echo "Skip $name (chain $chain_id): $rpc_var not set"
echo "Skip $name (chain $chain_id): none of ${rpc_vars} set"
continue
fi
@@ -85,12 +100,15 @@ for entry in "${CHAINS[@]}"; do
echo "WARN $name: using global OFFICIAL_USDC_ADDRESS fallback; set ${usdc_var} or ${usdc_var_alt} for chain-specific safety"
fi
forge_profile="$(forge_profile_for_chain "$chain_id")"
echo "=== Deploying DODOPMMIntegration on $name (chain $chain_id) ==="
echo "Using Foundry profile: $forge_profile"
DODO_VENDING_MACHINE_ADDRESS="$dvm" \
OFFICIAL_USDT_ADDRESS="$usdt" \
OFFICIAL_USDC_ADDRESS="$usdc" \
COMPLIANT_USDT_ADDRESS="$compliant_usdt" \
COMPLIANT_USDC_ADDRESS="$compliant_usdc" \
FOUNDRY_PROFILE="$forge_profile" \
forge script script/dex/DeployDODOPMMIntegration.s.sol:DeployDODOPMMIntegration \
--rpc-url "$rpc" \
--chain-id "$chain_id" \

View File

@@ -259,7 +259,7 @@ run_deploy_cw() {
fi
local gas_opt=""
[[ "$chain_id" == "42161" && -n "${ARBITRUM_GAS_PRICE:-}" ]] && gas_opt="--with-gas-price ${ARBITRUM_GAS_PRICE}"
run_cmd "${deploy_opts:+$deploy_opts }CW_BRIDGE_ADDRESS=$bridge forge script script/deploy/DeployCWTokens.s.sol:DeployCWTokens --rpc-url \"$rpc\" --chain-id \"$chain_id\" --broadcast --private-key \"\$PRIVATE_KEY\" --legacy ${gas_opt} -vvv" || true
run_cmd "${deploy_opts:+$deploy_opts }CW_BRIDGE_ADDRESS=$bridge CW_STRICT_MODE=${CW_STRICT_MODE:-1} CW_FREEZE_OPERATIONAL_ROLES=${CW_FREEZE_OPERATIONAL_ROLES:-1} forge script script/deploy/DeployCWTokens.s.sol:DeployCWTokens --rpc-url \"$rpc\" --chain-id \"$chain_id\" --broadcast --private-key \"\$PRIVATE_KEY\" --legacy ${gas_opt} -vvv" || true
echo " → Set CWUSDT_*, CWUSDC_*, CWEURC_*, CWEURT_*, CWGBPC_*, CWGBPT_*, CWAUDC_*, CWJPYC_*, CWCHFC_*, CWCADC_*, CWXAUC_*, CWXAUT_* in .env from script output."
}

View File

@@ -32,13 +32,42 @@ run_or_echo() {
}
ensure_rpc() { local rpc="$1"; type ensure_infura_rpc_url &>/dev/null && [[ -n "$rpc" ]] && rpc=$(ensure_infura_rpc_url "$rpc"); echo "$rpc"; }
get_token_balance() {
local token="$1" owner="$2" rpc="$3"
cast call "${token,,}" "balanceOf(address)(uint256)" "${owner,,}" --rpc-url "$rpc" 2>/dev/null || echo "0"
}
has_sufficient_link() {
local token="$1" owner="$2" rpc="$3" needed="$4"
local bal
bal=$(get_token_balance "$token" "$owner" "$rpc")
bal="${bal%% *}"
[[ "$bal" =~ ^[0-9]+$ ]] || bal="0"
python3 - <<PY
bal = int("$bal")
needed = int("$needed")
raise SystemExit(0 if bal >= needed else 1)
PY
}
print_skip() {
local label="$1" token="$2" owner="$3" rpc="$4" needed="$5"
local bal
bal=$(get_token_balance "$token" "$owner" "$rpc")
echo " Skipped (insufficient LINK balance: have ${bal%% *}, need $needed)"
}
if [[ -z "$PRIVATE_KEY" ]]; then
echo "ERROR: PRIVATE_KEY not set" >&2
exit 1
fi
DEPLOYER_ADDR=$(cast wallet address --private-key "$PRIVATE_KEY" 2>/dev/null || true)
if [[ -z "$DEPLOYER_ADDR" ]]; then
echo "ERROR: could not derive deployer address from PRIVATE_KEY" >&2
exit 1
fi
echo "Funding CCIP bridges with LINK (amount per bridge: $LINK_AMOUNT_WEI wei)"
echo "Deployer: $DEPLOYER_ADDR"
echo ""
# Chain 138 (Besu: gas estimation often fails; use explicit legacy gas like manual cast)
@@ -48,8 +77,20 @@ if [[ -n "${RPC_URL_138:-}" && -n "${LINK_TOKEN_CHAIN138:-${LINK_TOKEN:-}}" ]];
rpc=$(ensure_rpc "$RPC_URL_138")
echo "Chain 138 (RPC: ${rpc%%\?*}...)"
_gas138="--legacy --gas-limit 250000 --gas-price 2000000000"
[[ -n "${CCIPWETH9_BRIDGE_CHAIN138:-}" ]] && run_or_echo "cast send $link \"transfer(address,uint256)\" ${CCIPWETH9_BRIDGE_CHAIN138,,} $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" $_gas138"
[[ -n "${CCIPWETH10_BRIDGE_CHAIN138:-}" ]] && run_or_echo "cast send $link \"transfer(address,uint256)\" ${CCIPWETH10_BRIDGE_CHAIN138,,} $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" $_gas138"
if [[ -n "${CCIPWETH9_BRIDGE_CHAIN138:-}" ]]; then
if has_sufficient_link "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"; then
run_or_echo "cast send $link \"transfer(address,uint256)\" ${CCIPWETH9_BRIDGE_CHAIN138,,} $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" $_gas138"
else
print_skip "CHAIN138/WETH9" "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"
fi
fi
if [[ -n "${CCIPWETH10_BRIDGE_CHAIN138:-}" ]]; then
if has_sufficient_link "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"; then
run_or_echo "cast send $link \"transfer(address,uint256)\" ${CCIPWETH10_BRIDGE_CHAIN138,,} $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" $_gas138"
else
print_skip "CHAIN138/WETH10" "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"
fi
fi
echo ""
fi
@@ -58,8 +99,20 @@ if [[ -n "${ETHEREUM_MAINNET_RPC:-}" && -n "${MAINNET_LINK_TOKEN:-${CCIP_ETH_LIN
link="${MAINNET_LINK_TOKEN:-$CCIP_ETH_LINK_TOKEN}"
rpc=$(ensure_rpc "$ETHEREUM_MAINNET_RPC")
echo "Ethereum Mainnet"
[[ -n "${MAINNET_CCIP_WETH9_BRIDGE:-}" ]] && run_or_echo "cast send $link \"transfer(address,uint256)\" $MAINNET_CCIP_WETH9_BRIDGE $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" --legacy"
[[ -n "${MAINNET_CCIP_WETH10_BRIDGE:-}" ]] && run_or_echo "cast send $link \"transfer(address,uint256)\" $MAINNET_CCIP_WETH10_BRIDGE $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" --legacy"
if [[ -n "${MAINNET_CCIP_WETH9_BRIDGE:-}" ]]; then
if has_sufficient_link "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"; then
run_or_echo "cast send $link \"transfer(address,uint256)\" $MAINNET_CCIP_WETH9_BRIDGE $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" --legacy"
else
print_skip "ETH/WETH9" "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"
fi
fi
if [[ -n "${MAINNET_CCIP_WETH10_BRIDGE:-}" ]]; then
if has_sufficient_link "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"; then
run_or_echo "cast send $link \"transfer(address,uint256)\" $MAINNET_CCIP_WETH10_BRIDGE $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" --legacy"
else
print_skip "ETH/WETH10" "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"
fi
fi
echo ""
fi
@@ -89,8 +142,20 @@ for label in BSC POLYGON BASE OPTIMISM ARBITRUM AVALANCHE CRONOS GNOSIS CELO WEM
[[ -z "$addr9" && -z "$addr10" ]] && continue
echo "$label"
[[ -n "$addr9" ]] && run_or_echo "cast send ${link,,} \"transfer(address,uint256)\" ${addr9,,} $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" --legacy"
[[ -n "$addr10" ]] && run_or_echo "cast send ${link,,} \"transfer(address,uint256)\" ${addr10,,} $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" --legacy"
if [[ -n "$addr9" ]]; then
if has_sufficient_link "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"; then
run_or_echo "cast send ${link,,} \"transfer(address,uint256)\" ${addr9,,} $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" --legacy"
else
print_skip "$label/WETH9" "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"
fi
fi
if [[ -n "$addr10" ]]; then
if has_sufficient_link "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"; then
run_or_echo "cast send ${link,,} \"transfer(address,uint256)\" ${addr10,,} $LINK_AMOUNT_WEI --rpc-url \"$rpc\" --private-key \"\$PRIVATE_KEY\" --legacy"
else
print_skip "$label/WETH10" "$link" "$DEPLOYER_ADDR" "$rpc" "$LINK_AMOUNT_WEI"
fi
fi
echo ""
done

View File

@@ -125,6 +125,9 @@ fi
while IFS=$'\t' read -r base_sym quote_sym; do
add_pair "$base_sym" "$quote_sym"
done < <(jq -r '.explicitPairs[]? | [.baseSymbol, .quoteSymbol] | @tsv' "$CONFIG_JSON")
while IFS=$'\t' read -r base_sym quote_sym; do
add_pair "$base_sym" "$quote_sym"
done < <(jq -r '.plannedPairs[]? | [.baseSymbol, .quoteSymbol] | @tsv' "$CONFIG_JSON")
echo "=== Chain 138 PMM Desired-State Inventory ==="
echo "Config: $CONFIG_JSON"

View File

@@ -137,6 +137,9 @@ fi
while IFS=$'\t' read -r base_sym quote_sym; do
add_pair "$base_sym" "$quote_sym"
done < <(jq -r '.explicitPairs[]? | [.baseSymbol, .quoteSymbol] | @tsv' "$CONFIG_JSON")
while IFS=$'\t' read -r base_sym quote_sym; do
add_pair "$base_sym" "$quote_sym"
done < <(jq -r '.plannedPairs[]? | [.baseSymbol, .quoteSymbol] | @tsv' "$CONFIG_JSON")
zero_addr='0x0000000000000000000000000000000000000000'
created=0

View File

@@ -1,7 +1,18 @@
#!/usr/bin/env bash
# Verify all four Cronos contracts via Etherscan-style API.
# Uses explorer-api.cronos.org (module/action, solidity-standard-json-input).
# Run from smom-dbis-138/
# Cronos (chain 25): confirm deployments and prepare manual verification.
#
# Cronos Explorer moved off legacy Etherscan-compatible POST endpoints on the
# public /mainnet/api route (404 on contract verification). Etherscan multichain
# v2 does not list Cronos chain id 25. Foundry forge verify-contract therefore
# cannot submit verification to Cronos reliably from CI.
#
# This script: (1) checks bytecode on RPC, (2) optionally exports Standard-JSON
# inputs for the web UI, (3) prints links to the manual runbook.
#
# Run from smom-dbis-138/: ./scripts/deployment/verify-cronos-contracts.sh
#
# Optional: CRONOS_EXPORT_SOURCES=1 — run export-cronos-verification-sources.sh
# Optional: CRONOS_REQUIRE_BYTECODE=1 — exit 1 if any listed address has no code (default: 1)
set -euo pipefail
@@ -25,66 +36,56 @@ elif [[ -n "${REPO_ROOT:-}" && -f "$REPO_ROOT/.env" ]]; then
set +a
fi
RPC="${CRONOS_RPC_URL:-https://evm.cronos.org}"
REQUIRE_CODE="${CRONOS_REQUIRE_BYTECODE:-1}"
if [ -z "${CRONOSCAN_API_KEY:-}" ]; then
echo "ERROR: CRONOSCAN_API_KEY not set. Set in .env (from explorer.cronos.org/register)."
exit 1
fi
export CRONOSCAN_API_KEY
echo "Cronos verification (Etherscan-style API)"
echo " API: https://explorer-api.cronos.org/mainnet/api"
echo " Chain: cronos (from foundry.toml)"
echo "Cronos verification (Chain 25)"
echo " RPC: $RPC"
echo " Explorer: https://explorer.cronos.org"
echo " Manual UI: https://explorer.cronos.org/verifyContract"
echo ""
echo "Note: Automated forge/etherscan verification is not supported against the current"
echo " Cronos Explorer API (see docs/04-configuration/CRONOS_EXPLORER_OPERATIONS.md)."
echo ""
verify() {
local addr="$1"
local contract="$2"
local extra_args="${3:-}"
echo "Verifying $contract at $addr..."
# shellcheck disable=SC2086
if forge verify-contract \
"$addr" "$contract" \
--chain cronos \
--etherscan-api-key "$CRONOSCAN_API_KEY" \
--skip-is-verified-check \
$extra_args \
--watch 2>&1; then
echo "$contract verified"
else
echo "$contract verification failed"
return 1
fi
}
CONTRACTS=(
"0x99B3511A2d315A497C8112C1fdd8D508d4B1E506:WETH9"
"0x3304b747E565a97ec8AC220b0B6A1f6ffDB837e6:WETH10"
"0x3Cc23d086fCcbAe1e5f3FE2bA4A263E1D27d8Cab:CCIPWETH9Bridge"
"0x105F8A15b819948a89153505762444Ee9f324684:CCIPWETH10Bridge"
)
FAIL=0
# WETH9 - no constructor args
verify "0x99B3511A2d315A497C8112C1fdd8D508d4B1E506" "contracts/tokens/WETH.sol:WETH" || FAIL=$((FAIL+1))
# WETH10 - no constructor args
verify "0x3304b747E565a97ec8AC220b0B6A1f6ffDB837e6" "contracts/tokens/WETH10.sol:WETH10" || FAIL=$((FAIL+1))
# CCIPWETH9Bridge - constructor(router, weth9, linkToken)
verify "0x3Cc23d086fCcbAe1e5f3FE2bA4A263E1D27d8Cab" "contracts/ccip/CCIPWETH9Bridge.sol:CCIPWETH9Bridge" \
"--constructor-args $(cast abi-encode 'constructor(address,address,address)' 0xE26B0A098D861d5C7d9434aD471c0572Ca6EAa67 0x99B3511A2d315A497C8112C1fdd8D508d4B1E506 0x8c80A01F461f297Df7F9DA3A4f740D7297C8Ac85)" || FAIL=$((FAIL+1))
# CCIPWETH10Bridge - constructor(router, weth10, linkToken)
verify "0x105F8A15b819948a89153505762444Ee9f324684" "contracts/ccip/CCIPWETH10Bridge.sol:CCIPWETH10Bridge" \
"--constructor-args $(cast abi-encode 'constructor(address,address,address)' 0xE26B0A098D861d5C7d9434aD471c0572Ca6EAa67 0x3304b747E565a97ec8AC220b0B6A1f6ffDB837e6 0x8c80A01F461f297Df7F9DA3A4f740D7297C8Ac85)" || FAIL=$((FAIL+1))
echo "On-chain bytecode:"
for entry in "${CONTRACTS[@]}"; do
addr="${entry%%:*}"
name="${entry##*:}"
code=$(cast code "$addr" --rpc-url "$RPC" 2>/dev/null || echo "0x")
if [[ "${#code}" -gt 10 ]]; then
echo "$name $addr"
else
echo "$name $addr — no bytecode"
FAIL=$((FAIL + 1))
fi
done
echo ""
if [ "$FAIL" -eq 0 ]; then
echo "All four Cronos contracts verified."
exit 0
else
echo "$FAIL contract(s) failed."
echo ""
echo "Manual verification (recommended):"
echo " 1. ./scripts/deployment/export-cronos-verification-sources.sh"
echo " 2. Open https://explorer.cronos.org/verifyContract"
echo " 3. Follow docs/deployment/CRONOS_VERIFICATION_RUNBOOK.md"
if [[ "${CRONOS_EXPORT_SOURCES:-0}" == "1" ]] && [[ -x "$SCRIPT_DIR/export-cronos-verification-sources.sh" ]]; then
echo "Regenerating Standard-JSON inputs (.cronos-verify/)..."
bash "$SCRIPT_DIR/export-cronos-verification-sources.sh" || true
echo ""
fi
echo "Next steps (manual verification required):"
echo " 1. docs/deployment/CRONOS_VERIFICATION_RUNBOOK.md"
echo " 2. Or: ./scripts/deployment/export-cronos-verification-sources.sh"
echo " then upload each *_standard_input.json at https://explorer.cronos.org/verifyContract"
echo ""
if [[ "$REQUIRE_CODE" == "1" ]] && [[ "$FAIL" -gt 0 ]]; then
echo "ERROR: $FAIL contract(s) missing bytecode on $RPC" >&2
exit 1
fi
echo "verify-cronos-contracts.sh finished (deployment OK; verification is manual on Cronos)."
exit 0

View File

@@ -16,6 +16,8 @@ declare -A ROOT_SCRIPT_SCOPE_ALIASES=(
["DeployCCIPWETH9Bridge.s.sol"]="ccip"
["DeployCCIPWETH10Bridge.s.sol"]="ccip"
["DeployComplianceRegistry.s.sol"]="compliance"
["DeployOMNLStack.s.sol"]="hybx-omnl"
["DeployMirrorCoordinator.s.sol"]="hybx-omnl"
["DeployCompliantUSDC.s.sol"]="tokens"
["DeployCompliantUSDT.s.sol"]="tokens"
["DeployCWAssetReserveVerifier.s.sol"]="bridge/integration"
@@ -55,6 +57,8 @@ Examples:
FORGE_SCOPE=vault bash scripts/forge/scope.sh test --match-path 'test/vault/*.t.sol'
Notes:
- Root `forge test` / `forge build` (no scope) honor `foundry.toml` `[profile.default] skip` for
legacy Uniswap V2 vendor trees (old solc); scoped builds unchanged.
- Omit [scope] to use FORGE_SCOPE, otherwise default to 'full'.
- 'full' preserves the historical repo-wide Forge behavior.
- Any other scope is resolved relative to contracts/, for example:

View File

@@ -95,11 +95,14 @@ if [[ "$RUN_ADD_LIQUIDITY" == true ]]; then
export POOL_CUSDTCUSDC="${POOL_CUSDTCUSDC:-0x9e89bAe009adf128782E19e8341996c596ac40dC}"
export POOL_CUSDTUSDT="${POOL_CUSDTUSDT:-0x866Cb44b59303d8dc5f4F9E3E7A8e8b0bf238d66}"
export POOL_CUSDCUSDC="${POOL_CUSDCUSDC:-0xc39B7D0F40838cbFb54649d327f49a6DAC964062}"
export POOL_CUSDTWETH="${POOL_CUSDTWETH:-0xaE38a008Ba4Dbf8D9F141D03e9dC8f7Dbe0ce17c}"
export POOL_CUSDCWETH="${POOL_CUSDCWETH:-0xAAE68830a55767722618E869882c6Ed064Cc1eb2}"
export POOL_CEURTWETH="${POOL_CEURTWETH:-0x4a64c886cedF00db42ea5B946D6b304121ad9529}"
if [[ -n "${DODO_PMM_INTEGRATION:-}" || -n "${DODO_PMM_INTEGRATION_ADDRESS:-}" ]]; then
# Use pending nonce so broadcast does not get -32001 "Nonce too low" (mints just used N and N+1)
NEXT_NONCE=$(cast nonce "$DEPLOYER" --rpc-url "$RPC" --block pending 2>/dev/null || true)
[[ -n "$NEXT_NONCE" && "$NEXT_NONCE" =~ ^[0-9]+$ ]] && export NEXT_NONCE || unset -v NEXT_NONCE
echo "Running AddLiquidityPMMPoolsChain138 (cUSDT/cUSDC pool only if base/quote set)..."
echo "Running AddLiquidityPMMPoolsChain138..."
forge script script/dex/AddLiquidityPMMPoolsChain138.s.sol:AddLiquidityPMMPoolsChain138 \
--rpc-url "$RPC" --broadcast --private-key "$PRIVATE_KEY" --with-gas-price 1000000000 --gas-estimate-multiplier 150
echo "Add-liquidity done."
@@ -109,7 +112,8 @@ if [[ "$RUN_ADD_LIQUIDITY" == true ]]; then
fi
else
echo "To add liquidity next: set ADD_LIQUIDITY_BASE_AMOUNT and ADD_LIQUIDITY_QUOTE_AMOUNT (base units, 6 decimals),"
echo "POOL_CUSDTCUSDC (and optional POOL_CUSDTUSDT, POOL_CUSDCUSDC), DODO_PMM_INTEGRATION in .env, then run:"
echo "POOL_CUSDTCUSDC (and optional POOL_CUSDTUSDT, POOL_CUSDCUSDC, POOL_CUSDTWETH, POOL_CUSDCWETH, POOL_CEURTWETH),"
echo "DODO_PMM_INTEGRATION in .env, then run:"
echo " forge script script/dex/AddLiquidityPMMPoolsChain138.s.sol:AddLiquidityPMMPoolsChain138 --rpc-url \$RPC_URL_138 --broadcast --private-key \$PRIVATE_KEY"
echo "Or run this script with --add-liquidity to mint and add in one go (uses half of minted for cUSDT/cUSDC pool)."
fi

View File

@@ -17,7 +17,9 @@ DEST_RELAY_BRIDGE_ALLOWLIST=0x2bF74583206A49Be07E0E8A94197C12987AbD7B5
RELAYER_PRIVATE_KEY=${PRIVATE_KEY}
RELAYER_ADDRESS=0x4A666F96fC8764181194447A7dFdb7d471b301C8
START_BLOCK=2706088
# Historical cW backfill is complete; cold starts should resume near head rather
# than requeueing legacy unsupported canonical-token messages.
START_BLOCK=latest
POLL_INTERVAL=5000
CONFIRMATION_BLOCKS=1
MAX_RETRIES=3

View File

@@ -804,6 +804,33 @@ export class RelayService {
return null;
}
// Dedicated cW bridge workers can replay historical messages for canonical
// assets that are no longer configured on the destination multi-token bridge.
// When that mapping is absent, the destination bridge will always revert with
// "token not configured", so retire the message instead of retry-looping it.
if (targetBridgeContract.canonicalToMirrored && data && tokenAmounts.length === 0) {
try {
const decodedPayload = ethers.AbiCoder.defaultAbiCoder().decode(
['address', 'address', 'uint256'],
data
);
const canonicalToken = ethers.getAddress(decodedPayload[0]);
const mirroredToken = await targetBridgeContract.canonicalToMirrored(canonicalToken);
if (mirroredToken === ethers.ZeroAddress) {
this.logger.warn(
`Skipping queued message ${messageId}; canonical token ${canonicalToken} is not configured on destination bridge ${targetBridge}`
);
await this.messageQueue.markProcessed(messageId);
return null;
}
} catch (mappingProbeErr) {
this.logger.debug(
`canonicalToMirrored probe skipped for ${messageId}; continuing with relay attempt`,
mappingProbeErr
);
}
}
// Map token addresses from source chain to destination chain
const mappedTokenAmounts = tokenAmounts.map(ta => {
const sourceToken = ethers.getAddress(ta.token);

View File

@@ -17,7 +17,8 @@ export const RelayBridgeABI = [
"function ccipReceive(tuple(bytes32 messageId, uint64 sourceChainSelector, bytes sender, bytes data, tuple(address token, uint256 amount, uint8 amountType)[] tokenAmounts) calldata message) external",
"event CrossChainTransferCompleted(bytes32 indexed messageId, uint64 indexed sourceChainSelector, address indexed recipient, uint256 amount)",
"function processed(bytes32) view returns (bool)",
"function processedTransfers(bytes32) view returns (bool)"
"function processedTransfers(bytes32) view returns (bool)",
"function canonicalToMirrored(address canonicalToken) view returns (address)"
];
export const ERC20ABI = [

View File

@@ -10,10 +10,12 @@
"dev": "ts-node src/index.ts",
"test": "jest",
"test:ci": "jest --runInBand",
"test:omnl": "jest --runInBand --testPathPattern=omnl",
"lint": "eslint src --ext .ts",
"generate:route-matrix:v2": "ts-node scripts/generate-route-matrix-v2.ts",
"migrate": "node -r dotenv/config dist/database/migrations.js",
"example:partner-payloads": "node scripts/resolve-partner-payloads-example.mjs"
"example:partner-payloads": "node scripts/resolve-partner-payloads-example.mjs",
"omnl:reconcile": "node scripts/omnl-reconcile-report.mjs"
},
"dependencies": {
"axios": "^1.13.5",

View File

@@ -18,3 +18,15 @@ export const strictRateLimiter = rateLimit({
standardHeaders: true,
legacyHeaders: false,
});
/** Stricter limit for RPC-heavy /api/v1/omnl/* (stacks with apiRateLimiter). */
const omnlWindowMs = parseInt(process.env.OMNL_RATE_LIMIT_WINDOW_MS || '60000', 10);
const omnlMax = parseInt(process.env.OMNL_RATE_LIMIT_MAX || '30', 10);
export const omnlRateLimiter = rateLimit({
windowMs: omnlWindowMs,
max: omnlMax,
message: 'Too many OMNL API requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
});

View File

@@ -1,4 +1,6 @@
import express, { Express, Request, Response, NextFunction } from 'express';
import path from 'path';
import { readFileSync, existsSync } from 'fs';
import cors from 'cors';
import compression from 'compression';
import { apiRateLimiter, strictRateLimiter } from './middleware/rate-limit';
@@ -15,7 +17,10 @@ import arbitrageRoutes from './routes/arbitrage';
import aggregatorRouteMatrixRoutes from './routes/aggregator-routes';
import partnerPayloadRoutes from './routes/partner-payloads';
import plannerV2Routes from './routes/planner-v2';
import omnlRoutes from './routes/omnl';
import omnlIpsasRoutes from './routes/omnl-ipsas';
import { MultiChainIndexer } from '../indexer/chain-indexer';
import { OmnlEventPoller } from '../indexer/omnl-event-poller';
import { getDatabasePool } from '../database/client';
import winston from 'winston';
@@ -42,6 +47,7 @@ export class ApiServer {
private port: number;
private indexerEnabled: boolean;
private indexer: MultiChainIndexer | null;
private omnlPoller: OmnlEventPoller | null;
private resolveTrustProxySetting(): boolean | number | string {
const raw = (process.env.EXPRESS_TRUST_PROXY ?? process.env.TRUST_PROXY ?? '1').trim();
@@ -58,6 +64,7 @@ export class ApiServer {
this.port = parseInt(process.env.PORT || '3000', 10);
this.indexerEnabled = this.resolveFeatureFlag('ENABLE_INDEXER', true);
this.indexer = this.indexerEnabled ? new MultiChainIndexer() : null;
this.omnlPoller = this.resolveFeatureFlag('ENABLE_OMNL_EVENT_POLLER', false) ? new OmnlEventPoller() : null;
this.setupMiddleware();
this.setupRoutes();
@@ -126,6 +133,24 @@ export class ApiServer {
}
});
const dashboardPath = path.join(__dirname, '../../public/omnl-dashboard.html');
this.app.get('/omnl/dashboard', (req: Request, res: Response) => {
const tok = process.env.OMNL_DASHBOARD_TOKEN?.trim();
if (tok) {
const q = String(req.query.access_token ?? '').trim();
const h = String(req.headers['x-omnl-dashboard-token'] ?? '').trim();
if (q !== tok && h !== tok) {
res.status(401).type('text/plain').send('Unauthorized: set access_token query or X-OMNL-Dashboard-Token header');
return;
}
}
if (!existsSync(dashboardPath)) {
res.status(404).type('text/plain').send('omnl-dashboard.html missing');
return;
}
res.type('html').send(readFileSync(dashboardPath, 'utf8'));
});
// API routes
this.app.use('/api/v1', tokenRoutes);
this.app.use('/api/v1', configRoutes);
@@ -138,6 +163,8 @@ export class ApiServer {
this.app.use('/api/v1', arbitrageRoutes);
this.app.use('/api/v1', aggregatorRouteMatrixRoutes);
this.app.use('/api/v1', partnerPayloadRoutes);
this.app.use('/api/v1', omnlRoutes);
this.app.use('/api/v1', omnlIpsasRoutes);
this.app.use('/api/v2', plannerV2Routes);
// Admin routes (stricter rate limit)
@@ -152,6 +179,9 @@ export class ApiServer {
health: '/health',
api: '/api/v1',
apiV2: '/api/v2',
omnlOpenApi: '/api/v1/omnl/openapi.json',
omnlCatalog: '/api/v1/omnl/catalog',
omnlDashboard: '/omnl/dashboard',
},
});
});
@@ -183,6 +213,8 @@ export class ApiServer {
logger.info('Token aggregation indexer disabled by ENABLE_INDEXER flag');
}
this.omnlPoller?.start();
// Start server
this.app.listen(this.port, () => {
logger.info(`Token Aggregation Service listening on port ${this.port}`);
@@ -196,6 +228,7 @@ export class ApiServer {
}
async stop(): Promise<void> {
this.omnlPoller?.stop();
this.indexer?.stopAll();
logger.info('Server stopped');
}

View File

@@ -115,7 +115,7 @@ const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
[42161]: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', // Arbitrum USDT
[8453]: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', // Base USDT
[43114]: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', // Avalanche USDT
[CHAIN_25]: '0x66e4286603D22FF153A6547700f37C7Eae42F8E2', // Cronos USDT
[CHAIN_25]: '0x66e428c3f67a68878562e79A0234c1F83c208770', // Cronos USDT
[42220]: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e', // Celo USDT
[1111]: '0xA649325Aa7C5093d12D6F98EB4378deAe68CE23F', // Wemix USDT
},

View File

@@ -1,4 +1,4 @@
# Chain ID 2138 — Defi Oracle Meta Testnet
# Chain ID 2138 — DeFi Oracle Meta Testnet
# Copy to .env.chain2138 (or merge into smom-dbis-138/.env) and fill in values.
# Runbook: proxmox parent repo docs/testnet/DEFI_ORACLE_META_TESTNET_2138_RUNBOOK.md
# Chainlist metadata: pr-workspace/chains/_data/chains/eip155-2138.json

View File

@@ -3,12 +3,12 @@ pragma solidity ^0.8.20;
/**
* @title Chain2138TestnetConfig
* @notice Configuration constants for Chain ID 2138 (Defi Oracle Meta Testnet) tests.
* @notice Configuration constants for Chain ID 2138 (DeFi Oracle Meta Testnet) tests.
* @dev Mirror of Chain138Config pattern; override RPC via env in off-chain runners.
*/
library Chain2138TestnetConfig {
uint256 public constant CHAIN_ID = 2138;
string public constant NETWORK_NAME = "Defi Oracle Meta Testnet";
string public constant NETWORK_NAME = "DeFi Oracle Meta Testnet";
/// @dev Default public RPC from pr-workspace/chains/_data/chains/eip155-2138.json; use LAN/staging in CI as needed.
string public constant RPC_URL = "https://rpc.public-2138.defi-oracle.io";
string public constant EXPLORER_URL = "https://public-2138.defi-oracle.io";

View File

@@ -6,8 +6,65 @@ import "../../contracts/registry/UniversalAssetRegistry.sol";
import "../../contracts/bridge/UniversalCCIPBridge.sol";
import "../../contracts/bridge/BridgeOrchestrator.sol";
import "../../contracts/governance/GovernanceController.sol";
import "../../contracts/interfaces/IRegulatedAssetMetadata.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
/// @dev Minimal `IRegulatedAssetMetadata` so `_syncAssetMetadata` ABI-decodes cleanly
/// (Solidity try/catch does not catch return-data decode panics for empty/wrong-length returndata).
contract MockRegulatedAsset is IRegulatedAssetMetadata {
function assetId() external pure override returns (bytes32) {
return keccak256("MOCK_ASSET");
}
function assetVersionId() external pure override returns (bytes32) {
return keccak256("MOCK_VER");
}
function governanceProfileId() external pure override returns (bytes32) {
return bytes32(0);
}
function supervisionProfileId() external pure override returns (bytes32) {
return bytes32(0);
}
function storageNamespace() external pure override returns (bytes32) {
return bytes32(0);
}
function primaryJurisdiction() external pure override returns (string memory) {
return "";
}
function regulatoryDisclosureURI() external pure override returns (string memory) {
return "";
}
function reportingURI() external pure override returns (string memory) {
return "";
}
function canonicalUnderlyingAsset() external pure override returns (address) {
return address(0);
}
function supervisionRequired() external pure override returns (bool) {
return false;
}
function governmentApprovalRequired() external pure override returns (bool) {
return false;
}
function minimumUpgradeNoticePeriod() external pure override returns (uint256) {
return 0;
}
function wrappedTransport() external pure override returns (bool) {
return false;
}
}
/**
* @title UniversalBridge Integration Tests
* @notice Comprehensive tests for all asset types through full bridge flow.
@@ -86,10 +143,9 @@ contract UniversalBridgeTest is Test {
// Add validator
registry.addValidator(user1);
// Create proposal
address mockToken = makeAddr("mockToken2");
vm.etch(mockToken, hex"00");
// Create proposal (real contract: metadata sync ABI-decodes return data; empty etch breaks execute)
address mockToken = address(new MockRegulatedAsset());
bytes32 proposalId = registry.proposeAsset(
mockToken,
UniversalAssetRegistry.AssetType.Stablecoin,