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
- 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. - 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.
- 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.
- 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. - 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.
- Reproducible bytecode: two clean builds give identical bytecode. Hashes are in
Fee Contracts/audit/bytecode-hashes.txt. The.binfiles embedded in the Go binary match a fresh build byte for byte and are pinned byTestBytecodePinned.
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 |
| 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:
releaseandclaimarenonReentrant, and the reentrancy tests confirm it.flushOneis callable only by the wallet itself, and the wallet only by its factory.- The events after calls are informational.
- incorrect-equality:
== 0checks on balances and amounts, used only to skip work. - uninitialized-local:
totalstarts at zero on purpose. - missing-zero-check:
- Zero is a valid
FeeConfigrecipient: it means no override. - The wallet's immutables come from a factory that already checks them.
- Zero is a valid
- calls-loop: batches are caller-sized. A failing token in
flushis isolated. - assembly:
- CREATE2 in
sweep; extcodesize/extcodecopyin_isWallet.
- CREATE2 in
- low-level-calls: needed for tokens that return nothing (USDT) or misreport (Tron USDT).
Aderyn (static/aderyn-fee-contracts.md):
- H-1
encodePackedcollision: 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:
_requireOwnedreverts 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 | See Fee Contracts/audit/deep-campaign.log |
| Mutation testing | See 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
- Deploy the shares contract with
predict-sharesand confirm the same address on every chain. Then deployFeeConfigwith no override. Then bake both addresses into the Free and Standard builds. - On Tron, deploy shares and
TronFeeConfigwith afee_limitof at least 1,500 TRX. - On Solana:
- deploy the fee-shares program;
- run the mint tool, which initialises, registers, finalises and gives up the collection authority;
- confirm no deposit
Configaccounts exist; - upgrade the deposit program and run
init_fee_config.
- Verify every contract on Etherscan, Tronscan and with
solana-verify. - 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.