Security
Test coverage
| Suite | Tests | Method |
|---|---|---|
| InkdRegistry | 77 | Unit + edge cases |
| InkdRegistryV2 | 52 | Unit + upgrade path |
| InkdTreasury | 29 | Unit + split math |
| InkdBuyback | 23 | Unit + slippage tests |
| InkdToken | 19 | Unit |
| InkdTimelock | 10 | Unit |
| Fuzz suite | 29 | Fuzz — 256 runs per function |
| Invariant suite | 10 | Invariant — 256 sequences |
| Other | 72 | Unit |
Fuzz tests randomize inputs across 256 iterations per function, targeting arithmetic overflow, access control bypass, and fee calculation edge cases. Invariant tests verify protocol-level properties (token supply, project count monotonicity, name uniqueness, owner existence) hold across 256 random operation sequences.
Static analysis
All contracts run Slither on every commit via CI.
| Severity | Open findings |
|---|---|
| High | 0 |
| Medium | 0 |
| Low | 0 (false positives suppressed with inline comments) |
Audit passes
| Pass | Scope | Findings | Status |
|---|---|---|---|
| Pass 1 — Initial review | All contracts | 3 Medium, 5 Low | ✅ Fixed |
| Pass 2 — Slither deep scan | All contracts | 2 Medium, 6 Low | ✅ Fixed |
| Pass 3 — Storage & slippage | Buyback, RegistryV2 | 1 Medium | ✅ Fixed |
| Pass 4 — Professional criteria | All contracts | 1 Medium, 3 Low | ✅ Fixed |
Key findings (all resolved)
Buyback deposit() — permissionless event spoofing
Anyone could call deposit(uint256.max) and emit a misleading Deposited event, creating false accounting signals for off-chain monitors.
Fix: require(msg.sender == treasury)
Slippage protection missing on Uniswap swaps
amountOutMinimum: 0 — susceptible to sandwich attacks draining the buyback balance with minimal $INKD output.
Fix: amountOutMinimum = usdcIn × (10000 − maxSlippageBps) / 10000. Default 1%, configurable up to 10%.
Registry reentrancy in pushVersion / transferProject
State updates occurred after external Treasury calls, violating the Checks-Effects-Interactions pattern.
Fix: All state mutations moved before external calls.
Empty arweaveHash / versionTag accepted
pushVersion accepted empty strings, storing permanently invalid versions on-chain.
Fix: revert EmptyArweaveHash() / revert EmptyVersionTag()
Unbounded collaborators — O(n) gas DoS
No collaborator limit. removeCollaborator is O(n), enabling gas exhaustion at scale.
Fix: Hard cap of 50 collaborators. revert CollaboratorLimitReached()
Storage gap missing in RegistryV2
UUPS upgrade without a storage gap between V1 and V2 state variables would cause slot collisions.
Fix: uint256[42] __gap added after V2 state variables.
Security architecture
- UUPS upgradeable proxies — logic can be upgraded, but only through the respective owner Safe multisig (2-of-2 on Base Mainnet).
- Three separate Safes — Registry, Treasury, and Buyback are controlled independently. No single key controls the protocol.
- EIP-3009 payments — gasless USDC transfers with unique nonces and expiry windows. Replay-safe by design.
- CEI pattern enforced — all state mutations before external calls across all write functions.
- Arweave immutability — once a version is pushed, the content hash is permanent on-chain and the file is permanent on Arweave.
Limitations
- Internal audits only. No third-party security firm has reviewed the contracts. External audit is planned before significant TVL.
- Arweave content is public. All uploaded files are permanently accessible. Encrypt sensitive data with AgentVault before uploading.
- Upgradeability is a trust assumption. The protocol team can upgrade contract logic via multisig. This is intentional for bug fixes but requires trusting the multisig signers.
Responsible disclosure
Email security@inkdprotocol.com with details. We respond within 48 hours and coordinate disclosure after a fix is deployed.
Do not open public GitHub issues for security vulnerabilities.
