A single‑pair lending protocol (EUR⇄USD) that keeps user balances fully encrypted on‑chain using Zama’s FHEVM.
Borrowing and liquidations are driven by public static factors and a public price; no per‑epoch per‑user recomputation is needed. Lenders earn yield via index accrual. All amounts and user balances remain confidential. Liquidation are done without revealing any informations about the user's postion (debt and collat).
Built on Zama’s fhEVM. This document explains how the provided
ConfLendMarketcontract works, field‑by‑field and function‑by‑function, with 6‑decimals math throughout.
🌟 Winner of Zama's October 2025 builder program 🌟
⚠️ To work, OBOL relies on CAMM
OBOL gets its price data from CAMM (Confidential AMM), using an oracle and a relayer.
CAMM repo : https://github.com/6ygb/CAMM
Oracle relayer : https://github.com/6ygb/CAMM-OBOL-RELAYER
⚠️ Please read the docs first⚠️
Front-end POC with test contracts at : https://obol.6ygb.dev
Deployed on Sepolia :
-
EUR to USD Market (https://sepolia.etherscan.io/address/0x7Ca52270cA3F7bcAd11ddEa2BEF19dA246991Bac#code) :
0x7Ca52270cA3F7bcAd11ddEa2BEF19dA246991Bac -
USD to EUR Market (https://sepolia.etherscan.io/address/0x9749421BF36CA88FbB5Ae5Ca33d4d9E7B3665865#code) :
0x9749421BF36CA88FbB5Ae5Ca33d4d9E7B3665865 -
USD Token (https://sepolia.etherscan.io/address/0x066E08fabeaB12965F5aD467bbBf6954B73B9d27#code) :
0x066E08fabeaB12965F5aD467bbBf6954B73B9d27 -
EUR Token (https://sepolia.etherscan.io/address/0x60B5d3BFca0E45B65D5F0C1Ce15DB06f45c7AD5F#code) :
0x60B5d3BFca0E45B65D5F0C1Ce15DB06f45c7AD5F -
Oracle (https://sepolia.etherscan.io/address/0xa7a7AF22A88C5dc519A7811c0c5604dce692BA65#code) :
0xa7a7AF22A88C5dc519A7811c0c5604dce692BA65
Front end repo available at https://github.com/6ygb/OBOL-front
- Design Overview
- Core Math: A/B Factors, Price, Indexes
- Data Structures
- Parameters & Constants
- Lifecycle & Flows
- Public Health Check
- Rate Accrual & APR
- Oracle & Price Semantics
- Event Reference
- Key Functions - Code Snippets & Commentary
- OBOL - Tasks & Deployment Guide
- License
- One contract per direction (via
Directionenum at construction):EURtoUSD: debt asset is EUR, collateral asset is USD.USDtoEUR: debt asset is USD, collateral asset is EUR.
- Encrypted balances with fhEVM types (
euint64) for collateral and debt principals. - Static public factors per user:
A = s * collat * LT(scaled 1e6)B = s * debtPrincipal(scaled 1e6)
A fresh encrypted secrets(per user) is generated and kept on‑chain (encrypted). Factors are refreshed only when a user modifies their position or gets liquidated.
- HF off‑chain: Watchers compute Health Factor using only A, B, the public price and the public borrow index ratio. There’s no per‑epoch FHE recompute.
- Lenders supply the debt asset (the asset borrowers borrow) and receive confidential oTokens (this contract itself extends
ERC7984to act as the share token). - Liquidations: Anyone can liquidate an unhealthy account using a public check; the seized collateral is queued and then claimed by the liquidator.
Everything is 6‑decimals (the same scale as your confidential tokens).
For each user
Where:
| Symbol | Meaning |
|---|---|
user’s encrypted random secret, drawn on-chain as an euint32
|
|
| encrypted collateral amount (confidential balance) | |
| encrypted debt principal | |
| liquidation threshold (e.g. 0.85 × 1e6 = 850000) |
Thus,
When do A/B change?
- When the user’s encrypted position changes: add/remove collateral, borrow/repay, or liquidation.
- They remain static while only prices and indexes evolve - minimizing on-chain FHE recomputation.
At every moment, the oracle provides a single price quote
function _getPrice() internal view returns (uint128) {
uint128 rawPrice = oracle.price6(); // USD per 1 EUR (1e6 scale)
if (direction == Direction.EURtoUSD) {
uint256 numerator = 1_000_000_000_000; // 1e12
uint256 inv = (numerator + rawPrice - 1) / rawPrice; // ceil division
return uint128(inv);
} else {
return rawPrice;
}
}Formally:
To account for time-dependent interest, the protocol maintains accrual indexes that scale all positions proportionally:
-
$I_b(t)$ : borrow index (starts at$1.000.000$ ) -
$I_s(t)$ : supply index (starts at$1.000.000$ )
Each index evolves continuously according to the current annualized APR (
In Solidity:
uint256 inc = (idx6 * apr6 * dt) / (1_000_000 * SECONDS_PER_YEAR);When a user borrows, their debt evolves proportionally to:
where
The Health Factor measures the ratio between a user’s adjusted collateral value and their current debt exposure.
Conceptually:
Using the protocol’s masked scalars
Because
The random secret ( s_u ) cancels out, meaning the health factor can be fully computed without knowing
All the required data -
-
$HF > 1$ : position is healthy (collateral sufficiently covers debt). -
$HF = 1$ : user is exactly at the liquidation threshold. -
$HF < 1$ : position becomes liquidatable.
In Solidity, isLiquidatablePublic() performs the inequality check corresponding to:
Even though anyone can compute HF_u(t) from public values, they cannot deduce the user’s actual collateral or debt amounts because both are scaled by an unknown, user-specific secret
Without
Only the relative ratios across assets (if multiple) could be inferred - not the totals.
Thus:
- Risk visibility (HF, liquidatability) is public.
- Wealth confidentiality (actual holdings) is preserved.
struct UserPos {
euint64 eCollat; // encrypted collateral balance
euint64 eDebt; // encrypted debt principal
uint256 A; // s * collat * LT (public)
uint256 B; // s * debt principal (public)
uint64 posEpoch; // bumps when A/B recomputed
uint128 userBorrowIndex6; // snapshot for interest folding
euint32 secret; // encrypted per-user random secret s
bool updatePending; // locks while factors are refreshing
euint64 maxBorrow; // encrypted “you can borrow up to” (allowed to user)
}
struct PendingLiqStruct {
euint64 seizedCollat; // encrypted collateral queued for a liquidator
bool exists;
}pos[u]stores all per‑user state.pendingLiquidations[user][liquidator]queues seized collateral for later claim.
LT_collat6- liquidation threshold for the collateral (e.g.,850000for 85%).LIQ_BONUS_BPS- liquidation incentive in bps (e.g.,500= 5%).HYST_BPS- public hysteresis in bps to avoid flapping around HF≈1 (default100= 1%).borrowApr6,supplyApr6- per‑year APRs, 1e6 scale (set byrateRelayer).borrowIndex6,supplyIndex6- indices starting at1_000_000.RESERVE_MIN6- minimum debt‑asset reserve kept in the pool to preserve liquidity for withdrawals.CLOSE_FACTOR_BPS- maximum debt percentage a liquidator may repay per call (present as a constant; use as policy if desired).
Lenders provide the debt asset (EUR in EURtoUSD, USD in USDtoEUR) and receive confidential oTokens (this ERC7984) that appreciate as supplyIndex6 grows.
Deposit (mint oTokens):
function depositDebtAsset(externalEuint64 encryptedAmount, bytes calldata proof) external- Pull encrypted amount from the lender.
- Mint oTokens at:
shares = amount * 1e6 / supplyIndex6(all 1e6‑scaled). - On the very first deposit, keep a
RESERVE_MIN6buffer in the pool.
Withdraw (burn oTokens):
function withdrawDebtAsset(externalEuint64 encryptedAmount, bytes calldata proof) external- Burn requested shares (or clamp by liquidity).
- Pay out underlying:
underlying = shares * supplyIndex6 / 1e6. - Enforce the
RESERVE_MIN6floor.
oTokens are fully confidential (encrypted share amounts), since
ConfLendMarketinheritsERC7984and mints/burns usingeuint64amounts.
function addCollateral(externalEuint64 encryptedAmount, bytes calldata proof) external- Pull encrypted collateral from user and increase
eCollat. - Trigger factor refresh to recompute public
AandBwith a fresh secret (see below).
function removeCollateral(externalEuint64 encryptedAmount, bytes calldata proof) external- Using public scalars (A, B, price, indexes), compute how much “safety buffer” in A remains.
- Encrypted check:
requested * (s*LT) <= excessusing the encryptedsecretand LT. - Transfer the safe amount; refresh factors.
function borrow(externalEuint64 encryptedAmount, bytes calldata proof) external- Fold interest into encrypted principal (
updateDebtInterest), keeping balances consistent. - Compute encrypted maxBorrow from current collateral via:
maxBorrowDebt ≈ (eCollat * LT) / (price * idxRatio) idxRatio = borrowIndex6 / userBorrowIndex6 - Clamp to that maximum, transfer debt to user, increase
eDebt, refresh factors.
function repay(externalEuint64 encryptedAmount, bytes calldata proof) external- Fold interest, clamp repay to outstanding, pull encrypted repay, reduce
eDebt, refresh factors.
Pre‑check (public):
function isLiquidatablePublic(address u) public view returns (bool)Compares A to the RHS built from B, price, idxRatio, LT, and HYST_BPS (see Public Health Check).
Execute liquidation:
function liquidate(address targetUser, externalEuint64 encryptedAmount, bytes calldata proof) external- Fold interest and clamp
repayto debt. Pull encrypted repay from the liquidator. - Compute seize in collateral units:
perUnit6 = price6 * (1 + LIQ_BONUS_BPS/10000) seize = repay * perUnit6 / 1e6 - Clamp by available
eCollat, reduceeDebt, reduceeCollatbyseize. - Queue the seized collateral in
pendingLiquidations[targetUser][liquidator]. - Liquidator later calls:
to receive the encrypted seized collateral transfer; then the victim’s factors are refreshed.
function claimLiquidation(address user) external
Seize and repay amounts are never made public. Transfers are confidential; only the liquidatability condition is public.
function isLiquidatablePublic(address u) public view returns (bool) {
UserPos memory p = pos[u];
if (p.B == 0) return false;
uint128 price6 = _getPrice();
uint256 userIdx = (p.userBorrowIndex6 == 0) ? borrowIndex6 : p.userBorrowIndex6;
uint256 idxRatio6 = (uint256(borrowIndex6) * 1_000_000) / userIdx;
// debt side with price and index ratio (1e6)
uint256 rhs = (((uint256(p.B) * uint256(price6)) / 1_000_000) * idxRatio6) / 1_000_000;
// include LT and hysteresis on RHS
uint256 rhsWithLT = rhs * uint256(LT_collat6);
uint256 rhsWithHyst = (rhsWithLT * (10_000 + HYST_BPS)) / 10_000;
return uint256(p.A) < rhsWithHyst;
}Interpretation: liquidate if the public A (masked collateral * LT) is less than the debt‑side expression inflated by LT and hysteresis at the current price and index ratio.
APR parameters are per year at 1e6 scale (borrowApr6, supplyApr6) and are set by a rateRelayer.
function setRates(uint64 brPerSec6, uint64 srPerSec6) external onlyRateRelayerIndexes update with wall‑clock time:
function updateIndexes() public {
uint64 dt = uint64(block.timestamp) - lastAccrualTs;
if (dt == 0) return;
borrowIndex6 = _acc(borrowIndex6, borrowApr6, dt);
supplyIndex6 = _acc(supplyIndex6, supplyApr6, dt);
lastAccrualTs = uint64(block.timestamp);
}
function _acc(uint128 idx6, uint64 apr6, uint64 dt) internal pure returns (uint128) {
// idx_new = idx * (1 + APR * dt / YEAR)
uint256 inc = (uint256(idx6) * uint256(apr6) * uint256(dt)) / (1_000_000 * SECONDS_PER_YEAR);
return uint128(uint256(idx6) + inc);
}Borrower debt growth is folded into the encrypted principal on user touch via:
eDebt := eDebt * (borrowIndex6 / userBorrowIndex6)
userBorrowIndex6 := borrowIndex6
Supply APY flows into the oToken exchange rate (supplyIndex6).
Obol works with a price oracle. This price oracle must be fed by a relayer (the oracle deployer). The price data should come from a reliable source. In the demo web-app, the oracle gets its data from the associated CAMM pair.
contract ObolPriceOracle {
event PriceUpdated(uint128 price6, uint64 epoch, uint256 ts);
address public immutable relayer;
uint128 public price6;
uint64 public epoch;
uint256 public lastTs;
uint256 public immutable staleTtl;
modifier onlyRelayer() {
require(msg.sender == relayer, "RELAYER");
_;
}
constructor(address _relayer, uint256 _staleTtl) {
relayer = _relayer;
staleTtl = _staleTtl;
}
function setPrice(uint128 _price6, uint64 _epoch) external onlyRelayer {
require(_price6 > 0, "ZERO_PRICE");
require(_epoch > epoch, "STALE_EPOCH");
price6 = _price6;
epoch = _epoch;
lastTs = block.timestamp;
emit PriceUpdated(_price6, _epoch, lastTs);
}
function isFresh() public view returns (bool) {
return block.timestamp - lastTs <= staleTtl;
}
}
The market converts this into the collateral per debt quote (1e6 scale) used in risk and liquidation math via _getPrice().
All state‑changing user actions that depend on price are guarded by
fresh(oracle staleness check).
RatesUpdated(uint64 brPerSec6, uint64 srPerSec6)- APRs updated.Accrued(uint128 borrowIndex6, uint128 supplyIndex6)- indices updated.decryptionRequested(address user, uint256 blockNumber, uint256 requestID)- factor refresh requested.marketFactorsRefreshed(address user, uint256 requestID, uint256 blockNumber, uint256 A, uint256 B)- A/B refreshed.LiquidationQueued(address user, address liquidator, uint256 blockNumber)- a liquidation seized collateral (queued).LiquidationClaimed(address user, address liquidator, uint256 blockNumber)- liquidator claimed seized collateral.
function refreshMarketFactors(address user) public {
if (!FHE.isInitialized(pos[user].eCollat)) {
pos[user].eCollat = FHE.asEuint64(0);
FHE.allowThis(pos[user].eCollat);
FHE.allow(pos[user].eCollat, user);
}
if (!FHE.isInitialized(pos[user].eDebt)) {
pos[user].eDebt = FHE.asEuint64(0);
FHE.allowThis(pos[user].eDebt);
FHE.allow(pos[user].eDebt, user);
}
pos[user].secret = _generateRNG(0, 27);
FHE.allowThis(pos[user].secret);
FHE.allow(pos[user].secret, user);
euint64 eCollat = pos[user].eCollat;
euint64 eDebt = pos[user].eDebt;
euint32 eSecret = pos[user].secret;
euint128 uncompleteUAFactor = FHE.mul(FHE.asEuint128(eSecret), FHE.asEuint128(eCollat));
euint128 uBFactor = FHE.mul(FHE.asEuint128(eSecret), FHE.asEuint128(eDebt));
bytes32[] memory cts = new bytes32[](2);
cts[0] = FHE.toBytes32(uncompleteUAFactor);
cts[1] = FHE.toBytes32(uBFactor);
uint256 requestID = FHE.requestDecryption(cts, this.refreshMarketFactorsCallback.selector);
factorDecBundle[requestID] = user;
pos[user].updatePending = true;
emit decryptionRequested(user, block.number, requestID);
}
function refreshMarketFactorsCallback(
uint256 requestID,
bytes memory cleartexts,
bytes memory decryptionProof
) external {
FHE.checkSignatures(requestID, cleartexts, decryptionProof);
(uint128 uncompleteUAFactor, uint128 uBFactor) = abi.decode(cleartexts, (uint128, uint128));
address user = factorDecBundle[requestID];
uint256 uAFactor = uint256(uncompleteUAFactor) * uint256(LT_collat6);
pos[user].A = uAFactor;
pos[user].B = uint256(uBFactor);
pos[user].updatePending = false;
emit marketFactorsRefreshed(user, requestID, block.number, pos[user].A, pos[user].B);
}What’s happening:
- A fresh encrypted secret
s(generated at each factor refresh) multiplies the encrypted balances to form masked products. - Only the masked products are decrypted by the gateway; raw balances remain confidential.
A = (s * collat) * LT_collat6,B = s * debtare finalized and stored publicly.
function _maxBorrowFromCollat(address user) internal returns (euint64) {
updateIndexes();
uint128 price6 = _getPrice();
uint256 userIdx = (pos[user].userBorrowIndex6 == 0) ? borrowIndex6 : pos[user].userBorrowIndex6;
uint256 idxRatio6 = (uint256(borrowIndex6) * 1_000_000) / userIdx;
// den = price * idxRatio (1e6 scale)
uint256 den = (uint256(price6) * idxRatio6) / 1_000_000;
// eMax ≈ (eCollat * LT) / den
euint128 num = FHE.mul(FHE.asEuint128(pos[user].eCollat), uint128(LT_collat6));
euint64 eMax = FHE.asEuint64(FHE.div(num, uint128(den)));
// Store and allow the encrypted maxBorrow to the user for off-chain decryption
pos[user].maxBorrow = eMax;
FHE.allowThis(pos[user].maxBorrow);
FHE.allow(pos[user].maxBorrow, user);
return eMax;
}The user can call maxBorrow() to have the latest value allowed to them for local decryption.
function liquidationSeizePerUnit6() public view returns (uint128) {
uint128 price6 = _getPrice(); // collat per 1 debt, 1e6
uint256 v = (uint256(price6) * uint256(10_000 + LIQ_BONUS_BPS)) / 10_000;
return uint128(v);
}Seize is then seize = repay * perUnit6 / 1e6, clamped by current encrypted collateral.
Hardhat tasks to deploy, operate and inspect ConfLendMarket (Obol) with Zama fhEVM.
Amounts use 6 decimals (1e6 scale) across tokens, prices and indices.
- Hardhat with
hardhat-deployand the fhEVM plugin (@fhevm/hardhat-plugin). - Your network config set (e.g.,
--network sepolia). - A signer with funds and permissions to deploy & send txs.
Tip: when a task mentions operator, it refers to ERC-7984 operator for confidential transfers (the market/oracle must be set as an operator to move your encrypted funds).
npx hardhat obol:deploy --network sepolia \
[--relayer 0xRateRelayer] \
[--tokenUsd 0xPreExistingUSD] \
[--tokenEur 0xPreExistingEUR]- If
--tokenUsd/--tokenEurare omitted, fixture ConfidentialToken contracts are deployed. - Writes all addresses into
OBOL.json(see config section below).
Artifacts registered:
TokenUSD(USD ConfidentialToken),TokenEUR(EUR ConfidentialToken)ObolPriceOracleConfLendMarket_EURtoUSDandConfLendMarket_USDtoEUR(two directed markets)ConfLendMarketABI mirrored for both addresses
Update or fix values in OBOL.json manually via task:
npx hardhat obol:set_defaults \
[--usd 0x...] [--eur 0x...] [--oracle 0x...] \
[--m1 0x...] [--m2 0x...] [--relayer 0x...] \
[--deadline 86400] \
--network sepoliadeadlineis the default operator validity in seconds forsetOperatortasks.
npx hardhat obol:airdrop --network sepoliaClaims the example airdrop on both USD and EUR test tokens for the caller.
npx hardhat obol:get_balances --network sepoliaPrints clear USD, EUR balances and oUSD, oEUR (market share tokens) by decrypting your own ciphertexts via fhEVM CLI.
Allow a spender (market/oracle/escrow) to move your encrypted tokens:
npx hardhat obol:set_operator \
--token 0xToken \
--spender 0xSpender \
[--seconds 86400] \
--network sepolia- Sets
setOperator(spender, deadline)if not already set. - Uses
OPERATOR_DEADLINE_SECSfromOBOL.jsonwhen available.
npx hardhat obol:set_price --price6 1100000 --network sepoliaSets e.g. 1.10 USD per 1 EUR. Epoch is Date.now()/1000 by default.
npx hardhat obol:get_price --network sepoliaPrints the last price and timestamp (humanized).
npx hardhat obol:set_rates --borrow 50000 --supply 15000 --network sepolia
# = 5.0000% borrow APR, 1.5000% supply APRRequires the rate relayer role set during deploy.
npx hardhat obol:get_rates --network sepolianpx hardhat obol:update_indexes --network sepoliaApplies time delta to borrowIndex6 and supplyIndex6 immediately.
Two directed markets exist:
EURtoUSD: collateral EUR, debt USD, oToken = oUSDUSDtoEUR: collateral USD, debt EUR, oToken = oEUR
All human amounts are multiplied by
1e6under the hood.
npx hardhat obol:add_collat \
--market EURtoUSD \
--amount 1000 \
--network sepolia- Ensures the market is an operator on the collateral token.
- Encrypts the amount and calls
addCollateral(bytes32,bytes). - Waits for
marketFactorsRefreshed(A/B recompute).
npx hardhat obol:remove_collat \
--market USDtoEUR \
--amount 250 \
--network sepolia- Amount is capped so HF stays above the threshold (done under FHE).
npx hardhat obol:deposit_debt \
--market EURtoUSD \
--amount 500 \
--network sepolia- Ensures market is an operator on the debt token for the caller.
- Mints oTokens at
shares = amount * 1e6 / supplyIndex6.
npx hardhat obol:withdraw_debt \
--market USDtoEUR \
--shares 100 \
--network sepolia- Requires the market contract to be operator of your oTokens (the market pulls your shares from you).
- Pays underlying up to pool liquidity above the internal reserve floor.
npx hardhat obol:borrow \
--market EURtoUSD \
--amount 200 \
--network sepolia- Computes your encrypted maxBorrow under FHE and clamps the request.
- Transfers actual debt tokens; updates encrypted principal; refreshes A/B.
npx hardhat obol:repay \
--market USDtoEUR \
--amount 75 \
--network sepolia- Market must be operator on the debt token to pull the repayment.
- Overpay is clamped to outstanding under FHE; A/B refreshed.
npx hardhat obol:max_borrow \
--market EURtoUSD \
--network sepolia- Calls
maxBorrow()then decrypts your ownpos.maxBorrowhandle via fhEVM CLI.
npx hardhat obol:is_liq \
--market EURtoUSD \
[--user 0xVictim] \
--network sepoliaUses public A, B, price and index ratio; no private data needed.
npx hardhat obol:liquidate \
--market EURtoUSD \
--victim 0xVictim \
--amount 50 \
--network sepolia- Ensures market is operator on your debt token (as liquidator).
- Repay is clamped to victim’s outstanding encrypted debt; seize is computed with bonus and capped by encrypted collateral.
- Waits for
LiquidationQueuedevent.
npx hardhat obol:claim_liquidation \
--market EURtoUSD \
--victim 0xVictim \
--network sepolia- Transfers seized encrypted collateral to the liquidator.
- Prints before/after clear balances by decrypting your own ciphertexts.
- Triggers factor refresh on the victim’s position.
npx hardhat obol:pos --market USDtoEUR --network sepoliaOutputs handles for encrypted fields + decrypted values for your collat/debt/maxBorrow when available.
npx hardhat obol:price_effective --market EURtoUSD --network sepoliaUses oracle raw price and market direction (inversion for EUR→USD).
npx hardhat obol:seize_rate --market USDtoEUR --network sepoliaPrints liquidationSeizePerUnit6 and bonus basis points relative to price.
You can deploy price oracle independently and then provide the address in the deploy task.
npx hardhat --network sepolia obol:deploy_oracle --relayer 0x94f37a938FC67c3e61cC0dbbeff33373122507ec
-> Oracle address : 0xF0c298A3c3300D89bA610C3a0a968eFa031dD868Then
npx hardhat --network sepolia obol:deploy --reset --oracle 0xF0c298A3c3300D89bA610C3a0a968eFa031dD868If your current signer is not the price relayer address on the oracle, the obol:set_price task won't work.
To check what oracle is currently used by the markets :
npx hardhat --network sepolia obol:get_oracle_addressTo check what is the current price relayer address :
npx hardhat --network sepolia obol:get_price_relayerCreated/updated by tasks at repo root:
- Used by most tasks to resolve addresses & operator deadline defaults.
- Units: All amounts and indices are 1e6-scaled (6 decimals).
- Event waits: Tasks like
add_collat,borrow, and liquidations wait for their respective events (e.g.,marketFactorsRefreshed,LiquidationQueued,LiquidationClaimed). - fhEVM CLI: Tasks that decrypt your values run
fhevm.initializeCLIApi()and decrypt only your own handles (privacy preserving).
- Contract is released under the BSD 3‑Clause Clear License (same as your other projects).
- Dependencies retain their original licenses (e.g., fhEVM / OZ forks).
{ "ORACLE_ADDRESS": "0x...", "TOKEN_USD": "0x...", "TOKEN_EUR": "0x...", "MARKET_EURtoUSD": "0x...", "MARKET_USDtoEUR": "0x...", "RATE_RELAYER": "0x...", "OPERATOR_DEADLINE_SECS": 86400 }