Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 50 additions & 154 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,157 +1,53 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with
code in this repository.

## Project Overview

Decimal floating-point math library for Rainlang/DeFi. The `Float` type packs a
224-bit signed coefficient and 32-bit signed exponent into a single `bytes32`.
Decimal (not binary) representation ensures exact decimal values (e.g., `0.1`).
No NaN, Infinity, or negative zero — operations error on nonsense rather than
producing special values.

Dual implementation: Solidity for on-chain, Rust/WASM for off-chain JS/TS
consumption. The Rust crate uses revm to execute Solidity via an in-memory EVM,
ensuring identical behavior.

## Build Commands

### Solidity (Foundry)

```bash
forge build # Compile contracts
forge test # Run all Solidity tests (5096 fuzz runs)
forge test --mt testFunctionName # Run specific test by name
forge test -vvvv # Verbose trace output for debugging
```

### Rust

```bash
cargo build # Build native
cargo build --target wasm32-unknown-unknown --lib -r # Build WASM
cargo test # Run Rust tests
cargo test test_name # Run specific test
```

Rust tests depend on Foundry build artifacts (`out/`). Run `forge build` before
`cargo test` if artifacts are missing.

### JavaScript/WASM

```bash
npm install
npm run build # Full pipeline: Rust WASM → wasm-bindgen → base64 embed → CJS/ESM dist
npm test # TypeScript type check + vitest (tests in test_js/)
```

### Nix

```bash
nix develop # Enter dev shell with all tooling
```

### Deployment

Contracts are deployed deterministically via the Zoltu proxy to the same address
on all supported networks (Arbitrum, Base, Base Sepolia, Flare, Polygon). The
deterministic address is a function of bytecode + salt only — not the branch or
deployer — so a successful deploy from any branch lands at the same address a
main-branch deploy would.

**Typical flow for a source-changing PR**: trigger the `Manual sol artifacts`
GitHub workflow on the PR's branch before merge.
`gh workflow run manual-sol-artifacts.yaml --ref <branch> -f suite=decimal-float`
(use `log-tables` only when table bytecode changes, which is rare). The workflow
runs `script/Deploy.sol` with `--broadcast --verify` across all networks, using
`PRIVATE_KEY` regardless of ref. Do NOT wait for merge before deploying — there
is nothing to gain from waiting, and the CI deploy-constant tests need updating
anyway based on the deployed address.

**Two deployment suites** (log-tables must be deployed first if redeploying
tables):

```bash
DEPLOYMENT_KEY=<key> DEPLOYMENT_SUITE=log-tables forge script script/Deploy.sol:Deploy --broadcast --verify
DEPLOYMENT_KEY=<key> DEPLOYMENT_SUITE=decimal-float forge script script/Deploy.sol:Deploy --broadcast --verify
```

Expected addresses and code hashes are in
`src/lib/deploy/LibDecimalFloatDeploy.sol`. Any source change to
`LibDecimalFloat` or `LibFormatDecimalFloat` invalidates these constants; CI's
`testDeployAddress` and `testExpectedCodeHashDecimalFloat` will fail until
they're regenerated and committed. Network RPC URLs are configured in
`foundry.toml` via `CI_DEPLOY_*_RPC_URL` env vars.

## Architecture

### Solidity Layer (`src/`)

- **`lib/LibDecimalFloat.sol`** — Public API: arithmetic, comparison,
conversion, formatting, parsing. User-defined type `Float` wrapping `bytes32`.
- **`lib/implementation/`** — Internal arithmetic (512-bit intermediates for
mul/div), normalization, packing.
- **`lib/parse/`** — String-to-Float parsing.
- **`lib/format/`** — Float-to-string formatting.
- **`lib/table/`** — Log lookup tables (deployed as a data contract at a
deterministic address).
- **`concrete/DecimalFloat.sol`** — Exposes library functions as contract
methods (required for Rust/revm interop via ABI).
- **`error/`** — Custom error definitions (CoefficientOverflow,
ExponentOverflow, DivisionByZero, etc.).

### Scripts (`script/`)

- **`Deploy.sol`** — Production deployment script using Zoltu deterministic
proxy. Deploys log tables and DecimalFloat contract to all supported networks.
- **`BuildPointers.sol`** — Generates `src/generated/LogTables.pointers.sol`
(committed to repo; must be regenerated if log table data changes).

### Rust Layer (`crates/float/`)

- **`lib.rs`** — `Float` struct wrapping `B256`, implements
`Add`/`Sub`/`Mul`/`Div`/`Neg`. Uses `alloy::sol!` macro to generate bindings
from Foundry JSON artifacts in `out/`.
- **`js_api.rs`** — `#[wasm_bindgen]` exports for JS consumption (parse, format,
arithmetic, conversions).
- **`evm.rs`** — In-memory EVM setup via revm. All Rust float operations
delegate to Solidity through this.
- **`error.rs`** — Maps Solidity error selectors to Rust error types.

### JavaScript Layer

- **`scripts/build.js`** — Build pipeline: compiles WASM, runs wasm-bindgen,
base64-encodes WASM into JS modules for both CJS and ESM.
- **`test_js/`** — Vitest tests for the WASM bindings.
- **`dist/`** — Generated output (CJS + ESM with embedded WASM).

### Dependencies (`dependencies/`)

Managed by [Soldeer](https://soldeer.xyz) (`[dependencies]` in `foundry.toml`,
`libs = ['dependencies']`), not git submodules: forge-std,
`@openzeppelin-contracts`, rain-solmem, rain-string, rain-datacontract,
rain-deploy, rain-sol-codegen. Run `forge soldeer install` to fetch them.

## Key Design Details

- 512-bit intermediate values in multiply/divide to preserve precision.
- Exponent overflow and underflow both revert from the public arithmetic surface
Decimal floating-point math for Rainlang/DeFi. `Float` packs a 224-bit signed
coefficient and a 32-bit signed exponent into one `bytes32`. Decimal, not
binary, so values like `0.1` are exact. There is no NaN, Infinity or negative
zero — operations revert on nonsense rather than producing a special value.

## Rust is not a second implementation

`crates/float` runs the Solidity in an in-memory EVM (revm), so every Rust and
WASM operation IS the Solidity one. Fix math in Solidity; never reimplement it
in Rust. Bindings are generated from Foundry's `out/`, so `cargo test` needs a
`forge build` first.

## Deploys never gate merges

A source-changing PR regenerates its deployment record and lands on that record
alone. The on-chain deploy is a separate manual dispatch
(`manual-sol-artifacts.yaml`), run when someone decides to publish, and log
tables must land before `DecimalFloat` — its constructor checks their codehash.
Both are placed by the Zoltu proxy, whose address is a pure function of the
creation code, so a deploy from any branch lands where a main deploy would.

`test/src/lib/deploy/LibDecimalFloatDeployProd.t.sol` forks the five supported
networks and asserts the current record's addresses already carry the expected
code, so it goes red between a bytecode change and the deploy that publishes it.
That is a statement about the state of the chains, not about the branch.

Addresses and code hashes are generated, never hand-written. `script/Build.sol`
writes the current record to `src/generated/` and freezes it per release under
`src/generated/<tag>/` (tag = `[package].version`, dots as underscores);
`LibDecimalFloatDeploy` only aliases the current one. Any change to
`LibDecimalFloat` or `LibFormatDecimalFloat` changes the deployed bytecode, so
re-run the script and commit its output — `LibDecimalFloatDeployTaggedConstants`
re-derives every frozen record offline and fails when the two drift.

## Semantics to know before changing arithmetic

- Multiply and divide carry 512-bit intermediates to preserve precision.
- Exponent overflow and underflow both revert from the public surface
(`ExponentOverflow` / `ExponentUnderflow`). Coefficient truncation on values
too large for int224 is silently tolerated because it preserves the order of
magnitude.
- Log/power use lookup table approximations with linear interpolation (table
deployed as a data contract).
- Three packing modes:
- `packLossless`: reverts on any precision loss.
- `packLossy`: surfaces the `lossless` flag, returns `FLOAT_ZERO` on exponent
underflow. Used by parsing where underflow → "value rounds to zero" is a
legitimate parse result reported via `ParseDecimalPrecisionLoss`.
- `packArithmeticResult`: tolerates coefficient truncation, reverts on
exponent underflow. Used by every public arithmetic operation.
- Solidity compiler: 0.8.25, EVM target: Cancun, optimizer: 1,000,000 runs.

## License

LicenseRef-DCL-1.0 (Rain Decentralized Computer License). All source files
require SPDX headers per REUSE.toml.
too large for `int224` is silently tolerated: it preserves the magnitude.
- Three packing modes, and choosing the wrong one is a behaviour change.
`packLossless` reverts on any precision loss. `packLossy` surfaces a
`lossless` flag and returns `FLOAT_ZERO` on exponent underflow — parsing wants
this, where underflow is a legitimate result reported as
`ParseDecimalPrecisionLoss`. `packArithmeticResult` tolerates coefficient
truncation and reverts on exponent underflow; every public arithmetic
operation uses it.
- Log and power are lookup-table approximations with linear interpolation, the
tables held on-chain as a data contract.

Every source file needs an SPDX header — `REUSE.toml` enforces it.
3 changes: 2 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ cbor_metadata = false
ffi = true

fs_permissions = [
{ access = "read", path = "foundry.toml" },
{ access = "read-write", path = "./src/generated" },
{ access = "read", path = "./out" },
{ access = "read-write", path = "./crates/float/abi" },
Expand All @@ -45,7 +46,7 @@ forge-std = "1.16.1"
"rain-string" = "0.2.0"
"rain-datacontract" = "0.1.0"
"rain-deploy" = "0.1.3"
"rain-sol-codegen" = "0.1.0"
rain-sol-codegen = "0.1.4"

[soldeer]
recursive_deps = false
Expand Down
2 changes: 1 addition & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ forge-std-1.16.1/=dependencies/forge-std-1.16.1/
rain-datacontract-0.1.0/=dependencies/rain-datacontract-0.1.0/
rain-deploy-0.1.2/=dependencies/rain-deploy-0.1.2/
rain-deploy-0.1.3/=dependencies/rain-deploy-0.1.3/
rain-sol-codegen-0.1.0/=dependencies/rain-sol-codegen-0.1.0/
rain-sol-codegen-0.1.4/=dependencies/rain-sol-codegen-0.1.4/
rain-solmem-0.1.3/=dependencies/rain-solmem-0.1.3/
rain-string-0.2.0/=dependencies/rain-string-0.2.0/
126 changes: 126 additions & 0 deletions script/Build.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {Script} from "forge-std-1.16.1/src/Script.sol";
import {LibCodeGen} from "rain-sol-codegen-0.1.4/src/lib/LibCodeGen.sol";
import {LibFs} from "rain-sol-codegen-0.1.4/src/lib/LibFs.sol";
import {LibSnapshot} from "rain-sol-codegen-0.1.4/src/lib/LibSnapshot.sol";
import {LibDataContract} from "rain-datacontract-0.1.0/src/lib/LibDataContract.sol";
import {LibRainDeploy} from "rain-deploy-0.1.3/src/lib/LibRainDeploy.sol";
import {LibLogTable} from "../src/lib/table/LibLogTable.sol";
import {LibDecimalFloatDeploy} from "../src/lib/deploy/LibDecimalFloatDeploy.sol";
import {DecimalFloat} from "../src/concrete/DecimalFloat.sol";

contract Build is Script {
/// @notice The log/antilog lookup table data consumed by
/// `LibDecimalFloatDeploy.combinedTables()`. This is source data, not a
/// deployment record, so it is not part of the per-release snapshot.
function buildLogTablesData() internal {
LibFs.buildFileForContract(
vm,
address(0),
"LogTables",
string.concat(
LibCodeGen.bytesConstantString(
vm, "/// @dev Log tables.", "LOG_TABLES", LibLogTable.toBytes(LibLogTable.logTableDec())
),
LibCodeGen.bytesConstantString(
vm,
"/// @dev Log tables small.",
"LOG_TABLES_SMALL",
LibLogTable.toBytes(LibLogTable.logTableDecSmall())
),
LibCodeGen.bytesConstantString(
vm,
"/// @dev Log tables small alt.",
"LOG_TABLES_SMALL_ALT",
LibLogTable.toBytes(LibLogTable.logTableDecSmallAlt())
),
LibCodeGen.bytesConstantString(
vm,
"/// @dev Anti log tables.",
"ANTI_LOG_TABLES",
LibLogTable.toBytes(LibLogTable.antiLogTableDec())
),
LibCodeGen.bytesConstantString(
vm,
"/// @dev Anti log tables small.",
"ANTI_LOG_TABLES_SMALL",
LibLogTable.toBytes(LibLogTable.antiLogTableDecSmall())
)
)
);
}

/// @notice The deployment record for one deployable: its Zoltu-deterministic
/// address, the creation bytecode it is deployed FROM, and the runtime
/// bytecode it is verified AGAINST on-chain. `LibFs` prepends `BYTECODE_HASH`
/// derived from the passed instance, so the record is complete — address +
/// codehash + creation + runtime. A pin carrying only address + codehash
/// cannot reproduce or independently verify a past release.
///
/// One file PER contract: `BYTECODE_HASH` identifies a single instance, so
/// combining two deployables into one file would leave it meaningless.
function buildDeployRecordFor(string memory contractName, bytes memory creationCode, address deployed) internal {
LibFs.buildFileForContract(
vm,
deployed,
contractName,
string.concat(
LibCodeGen.addressConstantString(
vm,
"/// @dev Address of the contract deployed via Zoltu's deterministic\n"
"/// deployment proxy. Identical across all EVM-compatible networks.",
"DEPLOYED_ADDRESS",
deployed
),
LibCodeGen.bytesConstantString(
vm, "/// @dev The creation bytecode of the contract.", "CREATION_CODE", creationCode
),
LibCodeGen.bytesConstantString(
vm, "/// @dev The runtime bytecode of the contract.", "RUNTIME_CODE", deployed.code
)
)
);
}

/// @notice This release's deployment record: both deployables, each in its
/// own generated file. Every address is a pure function of its creation code
/// (Zoltu CREATE2), so the whole record is computed offline through a locally
/// etched factory. Frozen per release by `LibSnapshot`.
function buildDeployRecords() internal {
// The log tables must land first: DecimalFloat's constructor calls
// `checkLogTablesDeployed()`, which reads the codehash at their address.
bytes memory logTablesCreationCode =
LibDataContract.contractCreationCode(LibDecimalFloatDeploy.combinedTables());
buildDeployRecordFor("LogTablesDeploy", logTablesCreationCode, LibRainDeploy.deployZoltu(logTablesCreationCode));

bytes memory decimalFloatCreationCode = type(DecimalFloat).creationCode;
buildDeployRecordFor(
"DecimalFloatDeploy", decimalFloatCreationCode, LibRainDeploy.deployZoltu(decimalFloatCreationCode)
);
}

/// @notice The generated files that make up this release's deployment
/// record, frozen per release tag by `LibSnapshot`. The log-tables DATA is
/// deliberately absent: it is source input, not a deployment record.
function snapshotContractNames() internal pure returns (string[] memory names) {
names = new string[](2);
names[0] = "LogTablesDeploy";
names[1] = "DecimalFloatDeploy";
}

function run() external {
LibRainDeploy.etchZoltuFactory(vm);

buildLogTablesData();
buildDeployRecords();

// Freeze this release's record into `src/generated/<tag>/`. The tag, the
// freeze and the guard that refuses to rewrite a frozen record without a
// `[package].version` bump all live in the shared `LibSnapshot` — this
// repo does not carry its own copy.
LibSnapshot.freezeSnapshot(vm, snapshotContractNames());
}
}
Loading
Loading