# EVM binding

> The LOPW binding shared by Base, Ethereum, Arbitrum, Optimism, Polygon and Avalanche: Solidity contracts, EIP-712 typed data with the exact type strings, keccak-256 termsHash, secp256k1 signatures, Chainlink getRoundData(roundId) with the round id as the citation, EIP-3009 for x402, ABI-hex wire encoding, and confirmations before indexing.

*LOPW documentation · Networks.* Canonical page: https://lopw.com/docs/networks/evm.html · This Markdown twin: https://lopw.com/docs/networks/evm.md · Index of all pages: https://lopw.com/llms.txt · Everything in one file: https://lopw.com/llms-full.txt

One Solidity binding serves every EVM network. It is built against [Base](https://lopw.com/docs/networks/base.html) first (Base Sepolia for the test network) and runs unchanged on Ethereum, Arbitrum, Optimism, Polygon and Avalanche with each network’s own USDC and Chainlink addresses. Semantics are those of the protocol specification; only the carriers differ from [Stellar](https://lopw.com/docs/networks/stellar.html).

**Shared by six networks · Base first**

## At a glance

| Aspect | EVM binding |
| --- | --- |
| Contracts | Solidity (Foundry): Bond Registry, Claims, six validators; admin is `Ownable2Step`; reentrancy guard on every token-moving function; USDC moves with `SafeERC20` |
| Signatures | secp256k1 over EIP-712 typed data; the service signing key is an ordinary key held by the seller’s server and registered as an `address`, never the seller’s wallet |
| Hashing | keccak-256; `offerHash` and `receiptHash` are EIP-712 digests (`_hashTypedDataV4(structHash)`); `termsHash = keccak256(abi.encode(terms))` |
| Stake token | Native USDC (ERC-20, 6 decimals); the 100 USDC floor is `100_000_000` units |
| P6 oracle | Chainlink `AggregatorV3Interface.getRoundData(roundId)`; the cited round id plays the role of Stellar’s oracle timestamp |
| x402 scheme | `exact` on `eip155:` with USDC `transferWithAuthorization` (EIP-3009) |
| Wire form | `abi.encode(SignedOffer)` and `abi.encode(SignedReceipt)` as 0x-prefixed hex inside `extensions.lopw` |
| Indexing | `eth_getLogs` on the registry and claims addresses from the deployment block, after `EVM_CONFIRMATIONS` confirmations (default 12 on Base Sepolia) |

## Types

Solidity structs; field order is normative because it defines the ABI and the EIP-712 hashes. Field names are the Stellar names in camelCase.

*Solidity*

```text
struct OracleTerms { address feed; bytes32 asset; uint32 maxDeviationBps; uint64 maxOracleAge; }

struct Terms {
    uint8[]  predicates;   // 1..6, strictly increasing, no duplicates
    uint64   claimWindow;
    uint64   maxDataAge;
    uint16[] allowedStatus;
    uint32   maxResponseBytes;
    bool     hasOracle;
    OracleTerms oracle;    // zeroed when hasOracle is false
}

struct Offer {
    uint32  version;       // 1
    bytes32 serviceId;
    address registry;
    bytes32 termsHash;
    uint256 price;
    address asset;         // USDC
    uint256 maxClaim;
    uint64  validUntil;
    bytes32 offerNonce;
    uint32  keyVersion;
}

struct OracleEvidence { address feed; bytes32 asset; uint80 roundId; int256 oraclePrice; int256 reportedValue; }

struct Receipt {
    uint32  version;
    bytes32 serviceId;
    bytes32 offerHash;
    bytes32 termsHash;
    address buyer;
    bytes32 paymentReference;  // settlement transaction hash
    bytes32 requestHash;
    bytes32 responseHash;
    uint32  responseBytes;
    uint16  status;
    uint64  servedAt;
    uint64  dataAsOf;
    bool    hasOracle;
    OracleEvidence oracle;     // zeroed when hasOracle is false
    uint32  keyVersion;
}

struct SignedOffer   { Offer offer;     bytes signature; }   // 65-byte secp256k1 (r, s, v)
struct SignedReceipt { Receipt receipt; bytes signature; }
struct ClaimExtra    { bool hasSecond; SignedReceipt second; }   // P1 only
```

`termsHash = keccak256(abi.encode(terms))`. Service ids are `keccak256(abi.encode(address(registry), uint256(counter)))` with the counter starting at 0 per registry. `requestHash` and `responseHash` are the same SHA-256 constructions as on Stellar, so a seller’s HTTP layer is chain-agnostic.

## Hashing and signing (EIP-712)

Domain: `EIP712Domain(name="LOPW", version="1", chainId, verifyingContract=registry)`. The chain id and the registry address give the same two replay protections as Stellar’s network id and registry address in the hash.

*EIP-712 type strings*

```text
Offer(uint32 version,bytes32 serviceId,address registry,bytes32 termsHash,uint256 price,address asset,uint256 maxClaim,uint64 validUntil,bytes32 offerNonce,uint32 keyVersion)
OracleEvidence(address feed,bytes32 asset,uint80 roundId,int256 oraclePrice,int256 reportedValue)
Receipt(uint32 version,bytes32 serviceId,bytes32 offerHash,bytes32 termsHash,address buyer,bytes32 paymentReference,bytes32 requestHash,bytes32 responseHash,uint32 responseBytes,uint16 status,uint64 servedAt,uint64 dataAsOf,bool hasOracle,OracleEvidence oracle,uint32 keyVersion)
```

- `offerHash` and `receiptHash` are the EIP-712 digests `_hashTypedDataV4(structHash)`; nested structs are hashed by their own type hash.
- Verification is `ECDSA.recover(digest, signature) == registeredKey` with malleable signatures rejected (OpenZeppelin `ECDSA`).
- A receipt is accepted only if `servedAt < retiredAt` of its key version, exactly as on Stellar (decision D-006).

## Contracts

Functions keep the Stellar names where possible; errors are custom errors with the Stellar names; events carry the same names and fields in CamelCase, so one indexer schema serves every chain.

*Solidity interfaces*

```text
// Bond Registry
constructor(address admin, IERC20 usdc, ProtocolConfig config, LaunchLimits limits, FeeConfig fees)
setClaimsContract(address) -> uint64 eta          // first call immediate; later ones time-locked
applyClaimsContract(); cancelClaimsChange()
setLimits(LaunchLimits) -> uint64 eta              // tightening immediate, loosening time-locked
setPaused(bool); setSellerAllowed(address, bool); setFees(FeeConfig) -> uint64 eta
registerService(address signingKey, Terms terms, uint256 minStake) -> bytes32 serviceId
updateTerms(bytes32, Terms) -> bytes32 termsHash;  rotateKey(bytes32, address) -> uint32 keyVersion
deposit(bytes32, uint256)                          // seller must have approved USDC
requestUnbond(bytes32, uint256); cancelUnbond(bytes32); withdraw(bytes32) -> uint256; collectFee(bytes32) -> uint256
slash(bytes32, address buyer, uint256 amount) -> uint256   // onlyClaims
getService / getTerms / getKey / termsHash / getConfig / getLimits / getFees / isPaused / isSellerAllowed / getTotalStake

// Claims
constructor(address admin, address registry, uint64 changeDelay)
setValidator(uint8 predicateId, address) -> uint64 eta; applyValidator(uint8); cancelValidatorChange(uint8)
claim(uint8 predicateId, SignedOffer, SignedReceipt, ClaimExtra) -> (bool paid, uint256 amount, uint8 reason)
getClaim(bytes32 serviceId, bytes32 paymentReference) -> ClaimRecord

// Validators
validate(Terms, Offer, Receipt, ClaimExtra) external view returns (uint8)   // 0 proven, 1 NotProven, 2 NotVerifiable
```

The claim runs the seven checks in the specification’s order and reverts with the Stellar error names (`UnknownService`, `BadSignature`, `OfferMismatch`, `WindowClosed`, `AlreadyClaimed`, `PredicateNotOffered`, `NoStake`, `ValidatorNotSet`, `KeyRetired`, `UnsupportedVersion`, `SlashRateLimited`). The window check uses `block.timestamp`. Outcomes are `ClaimPaid(serviceId, predicateId, buyer, amount, paymentReference)` and `ClaimRejected(serviceId, predicateId, reason, paymentReference)` with reason 1 = NotProven and 2 = NotVerifiable. The P1 second receipt is signature-checked by Claims before the validator runs. Fees accrue per second with `SECONDS_PER_YEAR = 31_536_000` and basis points over 10,000.

## P6 with Chainlink

1. `receipt.oracle.feed == terms.oracle.feed && receipt.oracle.asset == terms.oracle.asset`, otherwise proven (terms mismatch).
2. `(, int256 answer,, uint256 updatedAt,) = feed.getRoundData(receipt.oracle.roundId)`; a revert, `answer <= 0` or `updatedAt == 0` is NotVerifiable.
3. `updatedAt > servedAt` or `servedAt − updatedAt > maxOracleAge`: proven (stale).
4. `|reportedValue − answer| × 10000 > maxDeviationBps × answer`: proven, exact integer arithmetic (decision D-009).

Chainlink rounds are immutable once published, so a cited `roundId` is the citation, exactly as the oracle timestamp is on Stellar. Terms name the feed by its pair label (`asset = bytes32("ETH/USD")`) and the reference service reports values in the feed’s decimals (8 on Chainlink USD feeds, against 14 on Reflector); the deviation check is decimal-agnostic because both sides come from the same feed.

## x402 on EVM

Scheme `exact` on `eip155:` with USDC `transferWithAuthorization` (EIP-3009). The seller’s `PaymentRequirements.extra` carries the asset’s EIP-712 name and version as x402 requires (`{ "name": "USDC", "version": "2" }` on Base Sepolia, verified on-chain). `paymentReference` is the settlement transaction hash from `PAYMENT-RESPONSE.transaction`; `buyer` is the `payer` address. The `lopw` extension is carried exactly as on Stellar; only the signed-offer and signed-receipt encoding changes.

*402 PaymentRequired, EVM*

```http
"extensions": {
  "lopw": {
    "info": {
      "version": 1,
      "network": "base-sepolia",
      "registry": "0x…",                    // Bond Registry contract address
      "service_id": "…64 hex…",
      "offer": "0x0000…"                    // abi.encode(SignedOffer { offer, signature }) as hex
    },
    "schema": { "type": "object" }
  }
}
// PAYMENT-RESPONSE.extensions.lopw.receipt: abi.encode(SignedReceipt { receipt, signature }) as 0x-hex
```

## SDK

```ts
import { EvmBinding, EVM_NETWORKS } from "@lopw/binding-evm";

const binding = new EvmBinding(EVM_NETWORKS["base-sepolia"]);   // chain id 84532, USDC and feed addresses included
// EVM_RPC_URL overrides the public JSON-RPC; LOPW_ORACLE overrides the feed; registry and claims come from deployments/<name>.json
```

| `EvmNetwork` field | Meaning |
| --- | --- |
| `name` | Network name used in API paths, for example `base-sepolia` |
| `caip2` | `eip155:`, used by x402 |
| `chainId` | The EIP-712 domain chain id |
| `rpcUrl` | Public JSON-RPC used by default |
| `usdc`, `usdcDecimals` | Circle’s native USDC contract, 6 decimals; checked on-chain (`symbol()`, `decimals()`) before use |
| `registry`, `claims` | LOPW contract addresses, filled from `deployments/.json` once a deployment is published |
| `oracle` | The Chainlink feed the reference service measures against, verified on-chain (`description()`, `decimals()`) |
| `explorerTx`, `explorerContract` | Explorer URL builders for the network |

## Networks on this binding

| Chain | Test network | Chain id | Production network | Chain id | LOPW deployment |
| --- | --- | --- | --- | --- | --- |
| [Base](https://lopw.com/docs/networks/base.html) | `base-sepolia` | 84532 | `base` | 8453 | None published |
| [Ethereum](https://lopw.com/docs/networks/ethereum.html) | `sepolia` | 11155111 | `ethereum` | 1 | None published |
| [Arbitrum](https://lopw.com/docs/networks/arbitrum.html) | `arbitrum-sepolia` | 421614 | `arbitrum` | 42161 | None published |
| [Optimism](https://lopw.com/docs/networks/optimism.html) | `optimism-sepolia` | 11155420 | `optimism` | 10 | None published |
| [Polygon](https://lopw.com/docs/networks/polygon.html) | `polygon-amoy` | 80002 | `polygon` | 137 | None published |
| [Avalanche](https://lopw.com/docs/networks/avalanche.html) | `avalanche-fuji` | 43113 | `avalanche` | 43114 | None published |

## For agents

> **For agents**
>
> | Item | Value |
> | --- | --- |
> | Network name in API paths | `base-sepolia`, `base`, `sepolia`, `ethereum`, `arbitrum-sepolia`, `arbitrum`, `optimism-sepolia`, `optimism`, `polygon-amoy`, `polygon`, `avalanche-fuji`, `avalanche`; a network is served once a deployment is published for it |
> | CAIP-2 id | `eip155:` (the chain id is also the EIP-712 domain chain id) |
> | Wire encoding of `extensions.lopw` | `info.offer` = `abi.encode(SignedOffer)` and `receipt` = `abi.encode(SignedReceipt)`, 0x-prefixed lowercase hex; JSON Schemas [offer-evm.json](https://lopw.com/docs/schemas/offer-evm.json), [receipt-evm.json](https://lopw.com/docs/schemas/receipt-evm.json), [terms-evm.json](https://lopw.com/docs/schemas/terms-evm.json) |
> | Signature | 65 bytes (r, s, v) over the EIP-712 digest; recover and compare with `getKey(serviceId, keyVersion)` |
> | Amounts | Decimal strings of USDC base units, 6 decimals |
