# Ekx Foundry

> Solidity development with Foundry (forge, cast, anvil) as used in our contract suites. Use when writing or running Solidity tests, deploying contracts with forge script, verifying on Basescan or Blockscout, forking a network for tests, debugging stack-too-deep or via_ir compile issues, managing lib/ dependencies with forge install, or inspecting chain state with cast. Covers foundry.toml config, our optimizer settings, and the deploy checklist.

- Skill: `ekinoxis-evm/ekx-foundry` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ekinoxis-evm/ekx-foundry`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ekinoxis-evm/ekx-foundry/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Ekinoxis-evm (https://skillmd.com/u/ekinoxis-evm)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ekinoxis-evm/ekx-foundry

---


# Foundry

Our contract toolchain for new Solidity work. (Two older suites are Hardhat — see
[`../ekx-hardhat/SKILL.md`](../ekx-hardhat/SKILL.md).)

Authoritative: the **`foundry-docs` MCP server**.
Web: https://book.getfoundry.sh

---

## Our `foundry.toml`

All three repos agree on the core:

```toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
optimizer = true
optimizer_runs = 200
via_ir = true

[rpc_endpoints]
base_sepolia = "${BASE_SEPOLIA_RPC_URL}"
base         = "${BASE_RPC_URL}"

[etherscan]
base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" }
base         = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api" }
```

**`via_ir = true` is required, not optional.** Our contracts hit stack-too-deep
without it. It roughly triples compile time — that is the accepted cost. If a build
suddenly takes 3 minutes, this is why; do not "fix" it by turning IR off.

`optimizer_runs = 200` favours deploy cost over call cost. Correct for contracts
deployed per-property (an AuctionManager) or per-challenge; raise it for a
singleton called constantly.

**Pin your own `solc`.** The OZ copies vendored in `lib/` declare `0.8.27`/prague
in most suites and `0.8.31`/osaka in the newest. Inheriting whatever
`lib/` says produces version drift between repos.

---

## Daily commands

```bash
forge build
forge test -vvv                     # -vvv shows console.log + revert reasons
forge test --match-test testBid -vvvv
forge test --gas-report
forge coverage
forge fmt
forge snapshot                      # gas baseline; commit .gas-snapshot
```

Fork tests — the right way to test against real USDC:

```bash
forge test --fork-url $BASE_SEPOLIA_RPC_URL
```

---

## Testing patterns

```solidity
import {Test, console} from "forge-std/Test.sol";

contract AuctionTest is Test {
    address alice = makeAddr("alice");

    function setUp() public {
        auction = new AuctionManager(...);
        deal(USDC, alice, 1_000e6);        // 6 decimals — USDC is NOT 18
    }

    function test_RevertWhen_BidTooLow() public {
        vm.prank(alice);
        vm.expectRevert(AuctionManager.BidTooLow.selector);
        auction.bid(1);
    }

    function testFuzz_BidAccepted(uint96 amount) public {
        amount = uint96(bound(amount, 1e6, 1_000e6));
        vm.prank(alice);
        auction.bid(amount);
    }
}
```

Cheats we lean on: `vm.prank` / `vm.startPrank`, `vm.warp`, `vm.roll`, `deal`,
`vm.expectRevert(Error.selector)`, `vm.expectEmit`, `makeAddr`, `bound` (never
`vm.assume` for ranges — it throws away runs).

---

## Deploying

```solidity
// script/Deploy.s.sol
contract Deploy is Script {
    function run() external {
        vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY"));
        new AuctionFactory(USDC_BASE_SEPOLIA);
        vm.stopBroadcast();
    }
}
```

```bash
forge script script/Deploy.s.sol:Deploy \
  --rpc-url base_sepolia --broadcast --verify -vvvv
```

Verify on Blockscout too (we read there):

```bash
forge verify-contract <ADDR> src/AuctionFactory.sol:AuctionFactory \
  --verifier blockscout \
  --verifier-url https://base-sepolia.blockscout.com/api
```

---

## `cast` — the tool people forget

```bash
cast call $AUCTION "highestBid()(uint256)" --rpc-url base_sepolia
cast balance $ADDR --rpc-url base_sepolia
cast send $USDC "approve(address,uint256)" $SPENDER 1000000 \
  --rpc-url base_sepolia --private-key $DEPLOYER_PRIVATE_KEY
cast 4byte-decode 0xa9059cbb...       # what function is this calldata?
cast sig "transfer(address,uint256)"
cast --to-unit 1000000 6              # 1.0  (USDC sanity check)
```

---

## Dependencies

```bash
forge install OpenZeppelin/openzeppelin-contracts@v5.0.2   # always pin a tag
forge update
```

Map them in `remappings.txt`:

```
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
forge-std/=lib/forge-std/src/
```

`lib/` is a git submodule tree. After cloning any of our contract repos:
`git submodule update --init --recursive` — otherwise nothing compiles.

---

## Gotchas

1. **`via_ir` makes builds slow.** Expected. Don't disable it.
2. **USDC is 6 decimals.** `1e6` is one dollar. `1e18` is a million million dollars and will revert on balance.
3. **`forge coverage` fails with `via_ir`** on some versions — run coverage with `--ir-minimum`.
4. **Submodules.** Missing `lib/` is the most common "it doesn't build" report.
5. **Broadcast artifacts** in `broadcast/` contain deployed addresses. Commit them; they are the deployment record.
6. **Verify on both** Basescan and Blockscout — our MCP reads use Blockscout.

---

## Pre-deploy checklist

Short form: testnet first → correct USDC address for the network → verify on both
explorers → record the address in the project README → transfer ownership off the
deployer EOA.

Related: [`../ekx-openzeppelin-upgrades/SKILL.md`](../ekx-openzeppelin-upgrades/SKILL.md) for
proxies, [`../ekx-blockscout/SKILL.md`](../ekx-blockscout/SKILL.md) for reads.

