# Internal security audit: fee contracts and fee shares, October 2026

| | |
|---|---|
| Scope | Fee-edition contracts for Ethereum-style chains and Tron, the 100 fee-share NFTs on each chain family, the Solana deposit program's fee path, the Solana fee-shares program and mint tool, and the Go code that deploys and drives them |
| Type | Internal review: automated tools, fuzzing, mutation testing and three independent AI-assisted manual reviews |
| Result | No critical or high findings. 6 medium, 13 low and 24 informational findings across the three reviews (some overlap). Every medium is fixed. Lows and infos are fixed or accepted below |

**This is not an independent audit.** The reviewers were AI agents briefed by the people who wrote the code.
Nothing here is locked yet: the Ethereum-style and Tron `FeeConfig` contracts, the Solana deposit program,
its fee config and the Solana fee-shares program all stay upgradeable or owned until an outside firm has
reviewed them. See How to get Coldtill audited.

The source lives in `Fee Contracts/` at the repository root. The full reports are kept next to it:

- `Fee Contracts/audit/review-evm.md`: Ethereum, Base, Polygon and BNB Chain contracts.
- `Fee Contracts/audit/review-tron.md`: the Tron versions and the Go deploy and sweep code.
- `Fee Contracts/audit/review-solana.md`: the deposit program's fee path, the fee-shares program and the mint tool.

## What was reviewed

| Component | Files |
|---|---|
| Fee shares (Ethereum-style) | `evm/src/ColdtillFeeShares.sol` |
| Fee shares (Tron) | `evm/src/tron/TronColdtillFeeShares.sol` |
| Fee override | `evm/src/FeeConfig.sol`, `evm/src/tron/TronFeeConfig.sol` |
| Fee factories and deposit wallets | `evm/src/DepositFactoryFee.sol`, `DepositWalletFee.sol` and their Tron versions |
| Solana fee shares | `solana/programs/coldtill-fee-shares`, `solana/mint` |
| Solana deposit program (fee path) | `app/solana/programs/keyhold-deposit` |
| Go | `app/internal/chains/evm`, `app/internal/chains/tron`, `app/internal/chains/sol`, `app/cmd/coldtill-fees` |

## Method

1. **Static analysis:** Slither and Aderyn on all Solidity, plus clippy and cargo-audit on both Solana
   programs. Raw output is in `Fee Contracts/audit/static/`. Every finding is either fixed or explained under
   [Tool findings](#tool-findings).
2. **Tests:**
   - Unit, fuzz and invariant tests in Foundry, covering the full path: payment, sweep, fee withdrawal,
     shares, release and claim.
   - LiteSVM tests against the real Metaplex Core program.
   - Go tests on all three editions.
   - Local-validator tests of the deposit program.
   - Fork tests against real USDC and USDT on Ethereum and Base.
3. **Deep campaign:** 100,000 fuzz runs per test and 2,000 invariant runs of depth 128 on the shares contract.
   Four invariants are checked:
   - every share can still be paid;
   - no share gets more than 1%;
   - recorded receipts match funding;
   - releases never exceed receipts.
4. **Mutation testing:** Gambit mutants of every fee contract, run in a scratch copy against the test suite.
   Report in `Fee Contracts/audit/mutation-report.txt`.
5. **Three independent manual reviews**, each by a separate model with a written brief and no access to the
   others' findings. Each review wrote its own proof-of-concept tests:
   - Ethereum-style chains: Claude Opus. An earlier pass by GPT was superseded.
   - Tron: Claude Fable.
   - Solana: Claude Fable.
6. **Reproducible bytecode:** two clean builds give identical bytecode. Hashes are in
   `Fee Contracts/audit/bytecode-hashes.txt`. The `.bin` files embedded in the Go binary match a fresh build
   byte for byte and are pinned by `TestBytecodePinned`.

Severity follows the October 2026 audit.

## Findings and resolutions

### Ethereum-style chains (`review-evm.md`)

| ID | Severity | Finding | Resolution |
|---|---|---|---|
| M-01 | Medium | The Go binary embedded the factory compiled before the `feeRecipient()` fixes | **Fixed.** `make contracts` rerun and pins updated. The staleness test now covers `FeeConfig` and the shares contract too. The v1 factory bytecode is unchanged, so no live deposit address moves |
| M-02 | Medium | One failing token reverted the whole sweep of a deposit address. An issuer freezing the factory would have locked merchant principal in that token | **Fixed.** Each token is flushed in its own self-call: it either fully succeeds or stays put with a `FlushFailed` event, which the sweeper already retries. If the factory can't take the fee, the fee goes straight to the shares contract. Native coin still reverts the sweep, because otherwise `destroy()` would send the whole balance to a cold wallet that refuses ETH, with no fee |
| L-01 | Low | A token with two contract addresses sharing one balance (e.g. TUSD's legacy address) is counted twice | **Accepted.** An allow-list would need an owner, and the shares contract deliberately has none. Fees only arrive in the tokens the gateway sweeps (native coin, USDC, USDT). Documented in [fee shares](https://coldtill.com/docs/fee-shares.md) |
| L-02 | Low | The `FeeConfig` owner could set a broken recipient and lock it. Ownership transfer was one-step | **Fixed.** `lock()` now requires the override to be cleared, so locking always makes the shares contract the permanent destination. Ownership is two-step (`acceptOwnership`) |
| L-03 | Low | Anyone could push a share's payout into an escrow or vault holding the NFT | **Fixed.** `release` skips shares held by contracts unless that contract calls. Wallets with no code and EIP-7702 delegated wallets are paid as before |
| I-01 | Info | A negative rebase made `releasable` revert | **Fixed.** Saturates at zero. Rebasing tokens are still unsupported |
| I-02 | Info | Override changes can be front-run by a permissionless `withdrawFees` | **Accepted.** Documented. The override is for emergencies only |
| I-03 | Info | The fee rounds to zero on very small sweeps | **Accepted.** Under 50 base units at 2%. Worth less than the gas |
| I-04 | Info | No deterministic deploy for the shares contract, yet one address is used on every chain | **Fixed.** `coldtill-fees predict-shares <holder>` deploys through the same CREATE2 deployer as `FeeConfig`. Checked on a local chain. The factory constructor now also refuses a shares or config address with no code, so a chain where shares aren't deployed can't get a factory |
| I-05 | Info | Polygon's native-coin ERC-20 shim | Checked safe. No change |
| I-06 | Info | A seller can claim accrued fees just before a sale settles | **Accepted.** Documented for buyers |

### Tron (`review-tron.md`)

| ID | Severity | Finding | Resolution |
|---|---|---|---|
| M-01 | Medium | Embedded Tron factory bytecode predated the fixes | **Fixed** (same as Ethereum-style M-01) |
| M-02 | Medium | Deploy budget sized for the v1 factory. The fee factory needs about 1.6M energy, so a hot wallet funded to the advertised 150 TRX would burn it and fail | **Fixed.** `tron.DeployBudget(version)`: v1 keeps 400 TRX limit / 150 TRX minimum, the fee factory uses 800 TRX limit / 400 TRX minimum. The dashboard shows the right minimum per edition |
| L-01 | Low | All-or-nothing flush | **Fixed** (same as Ethereum-style M-02). The Tron wallet also refuses a partial first fee transfer before falling back, so the fee can't be sent twice |
| L-02 | Low | No code check on the `feeShares` / `feeConfig` addresses | **Fixed.** Both factory constructors revert with `FeeConfigNotContract` / `FeeSharesNotContract`, which also catches a Nile address baked into a mainnet build |
| I-01 | Info | `releasable` reverted after Tether's `destroyBlackFunds` | **Fixed** (saturating, as above) |
| I-02 | Info | TRC10 tokens are stuck in the contracts, and `selfdestruct` forwards them to the cold wallet with no fee | **Accepted.** TRC10 is unsupported. Documented |
| I-03 | Info | Energy cost of large `release` batches and of the shares deploy | **Accepted.** Documented: release in batches of 20 or fewer, deploy with `fee_limit` of at least 1,500 TRX |

### Solana (`review-solana.md`)

| ID | Severity | Finding | Resolution |
|---|---|---|---|
| KD-01 | Medium | The mainnet deposit program is the pre-fee build, and the upgrade changes the `Config` layout from 73 to 75 bytes | **Deployment step.** No merchant `Config` exists on mainnet yet. Before the upgrade, `getProgramAccounts` must return none. Then upgrade and run `init_fee_config` in the same session, before any merchant onboards |
| KD-02 | Low | `u64` overflow in the fee maths made huge balances unsweepable | **Fixed.** `fee_of` computes in `u128` |
| KD-03 | Low | Small sweeps fail rent checks before the fee vault, treasury or distributor exist | **Deployment step.** Deploy order is: shares program `initialize`, then deposit upgrade, then `init_fee_config`. Merchants fund their treasury at onboarding |
| KD-04 | Low | Token-2022 hooks, transfer fees and permanent delegates | **Accepted.** The gateway only offers plain USDC and USDT on Solana. Documented |
| KD-05 | Info | One key controls upgrades and fee routing | **Accepted for now.** Moves to a multisig before locking. Nothing is locked before the outside audit |
| KD-06 | Info | Hygiene: unused `fee_vault` account, pre-existing vault ATA | The server creates the vault's token account idempotently before every sweep. The unused account stays to keep the instruction layout stable |
| KD-07 | Info | Rent for pre-funded accounts comes out of early fees | **Accepted.** `initialize` runs before fees are enabled, so no fees are affected |
| FS-01 | Low | A burned share's 1% stays in the pool forever | **Accepted.** Documented: burning forfeits the share. The program stays upgradeable until the outside audit, which covers a future Core layout change |
| FS-02 | Low | Permissionless release pays escrow or program addresses | **Fixed.** `release_sol` / `release_token` require an on-curve owner (`OwnerNotWallet`). Program-held shares keep their fees until a wallet owns them or the owner claims |
| FS-03 | Low | A permanent delegate draining the vault made shares fail with `Math` | **Fixed.** Saturating subtraction |
| FS-04 | Info | The mint tool aborted if the distributor address was pre-funded | **Fixed.** A system-owned empty account counts as uninitialised, with length checks before indexing. Tested on a local validator |
| FS-05 | Info | `initialize` didn't check the collection | **Fixed.** The collection is passed as an account and must be a Core `CollectionV1` (`NotCoreCollection`) |
| FS-06 | Info | Minor code quality, stale rent constant in tests | The test now reads rent from the runtime. The rest is noted |

## Tool findings

**Slither** (`static/slither-fee-contracts.txt`). Nothing exploitable:

- **arbitrary-send-eth:** payouts go to the NFT owner, the cold wallet, the factory or the shares contract. These
  addresses are immutables or `ownerOf`, not caller input.
- **reentrancy-eth / reentrancy-events:**
  - `release` and `claim` are `nonReentrant`, and the reentrancy tests confirm it.
  - `flushOne` is callable only by the wallet itself, and the wallet only by its factory.
  - The events after calls are informational.
- **incorrect-equality:** `== 0` checks on balances and amounts, used only to skip work.
- **uninitialized-local:** `total` starts at zero on purpose.
- **missing-zero-check:**
  - Zero is a valid `FeeConfig` recipient: it means no override.
  - The wallet's immutables come from a factory that already checks them.
- **calls-loop:** batches are caller-sized. A failing token in `flush` is isolated.
- **assembly:**
  - CREATE2 in `sweep`;
  - `extcodesize` / `extcodecopy` in `_isWallet`.
- **low-level-calls:** needed for tokens that return nothing (USDT) or misreport (Tron USDT).

**Aderyn** (`static/aderyn-fee-contracts.md`):

- **H-1 `encodePacked` collision:** the input is fixed-length clone bytecode around one address, so no collision
  is possible.
- **H-2 `selfdestruct`:** used in the same transaction as the clone's creation, which still deletes the account
  under EIP-6780 (and on the TVM, per the Tron review).
- **L-7 unchecked return:** `_requireOwned` reverts and its return value isn't needed.
- **L-9 `_mint`:** minting to the cold wallet at deploy is intended.
- **Other lows:** centralisation of `FeeConfig` (see L-02), literals, loops. Style only.

**Clippy:** clean with `-D warnings` on both Solana programs.

**cargo-audit:** no vulnerabilities, only 5 unmaintained-crate warnings in Solana SDK dependencies.

## Test results

| Suite | Result |
|---|---|
| Foundry, all fee contracts (excluding forks) | 60 tests pass, including 4 invariants at the default 256 runs |
| Foundry fork tests (real USDC and USDT, Ethereum and Base) | Pass |
| Deep fuzz and invariant campaign | 33 of 33 pass: 100,000 runs per fuzz test; invariants held over 2,000 runs and 256,000 calls with 0 reverts (`Fee Contracts/audit/deep-campaign.log`) |
| Mutation testing | 429 of 429 mutants killed across all 8 fee contracts (`Fee Contracts/audit/mutation-report.txt`) |
| Solana fee-shares (LiteSVM with real Core) | 8 tests pass |
| Solana deposit program on a local validator | All pass, including fee withdraw, override and clear |
| Go, free / standard / pro editions | All pass |
| Mint tool on a local validator, with a pre-funded distributor | 100 assets minted, registered, finalised, collection authority given up |

## Before mainnet

1. Deploy the shares contract with `predict-shares` and confirm the same address on every chain. Then deploy
   `FeeConfig` with no override. Then bake both addresses into the Free and Standard builds.
2. On Tron, deploy shares and `TronFeeConfig` with a `fee_limit` of at least 1,500 TRX.
3. On Solana:
   - deploy the fee-shares program;
   - run the mint tool, which initialises, registers, finalises and gives up the collection authority;
   - confirm no deposit `Config` accounts exist;
   - upgrade the deposit program and run `init_fee_config`.
4. Verify every contract on Etherscan, Tronscan and with `solana-verify`.
5. Do not lock anything (`FeeConfig.lock`, `lock_fee_config`, or either Solana program's upgrade authority)
   until an outside firm has reviewed this code and the keys sit behind a multisig.

---

Source: https://coldtill.com/docs/security-audit-2026-10-fees/ · All Coldtill docs: https://coldtill.com/llms.txt
