Overview
What it is
The on-chain half of GKOI: four Foundry/Hardhat Solidity repos, each with its own
foundry.toml, test/, and script/.
| Repo | Role | Pragma | Deploy mechanism |
|---|---|---|---|
gkoi-erc721AC | The mint collection — a SeaDrop-based ERC721A with a Creator-Token transfer-validator hook | 0.8.17 | EIP-1167 clones (SeaDrop cloneable line) |
NftCollectionFactory | Deterministic collection deployer | ^0.8.28 | CREATE2 over raw creation bytecode |
Conduit | Seaport channel-authorized transfer router (@author 0age) | 0.8.14 | Deployed/controlled by a ConduitController |
gkoi-smart-contracts | ERC-20 GKOI token, swaps, sale, batch transfer | — | — |
gkoi-erc721AC builds two ways from one tree: a standard profile and a
[profile.upgradeable] pointing at src-upgradeable/src/, tuned with
optimizer_runs = 1_000_000 and bytecode_hash = "none" for deploy-cost and
determinism. Foundry runs fast unit/fuzz; Hardhat is kept for coverage.
Enforceable royalties — the accurate mechanism
The collection is not a vendored LimitBreak royalty policy. It exposes the
Creator Token interface (ICreatorToken) and, on every non-mint/non-burn
transfer, calls out to an external, owner-configurable validator in
_beforeTokenTransfers. The validator address lives behind
setTransferValidator(...) onlyOwner, and the null address means no validator and
no enforcement:
function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256) internal virtual override {
if (from != address(0) && to != address(0)) {
address transferValidator = _transferValidator;
if (transferValidator != address(0)) {
ITransferValidator721(transferValidator).validateTransfer(msg.sender, from, to, startTokenId);
}
}
}
The interface it calls is intentionally minimal — a view gate that reverts to
block a transfer — and the token advertises the exact selector via
getTransferValidationFunction(), so a compliant marketplace knows which function
enforces policy:
interface ITransferValidator721 {
function validateTransfer(address caller, address from, address to, uint256 tokenId) external view;
}
Royalties themselves are a separate, honest thing: an ERC-2981 rate
declaration. setRoyaltyInfo reverts on a zero receiver or royaltyBps > 10_000,
and royaltyInfo is plain rate math the marketplace reads:
royaltyAmount = (_salePrice * info.royaltyBps) / 10_000;
So the rate is on-chain and honoured by marketplaces that choose to; enforcement that a sale actually pays it is delegated to whatever validator the owner points the token at. The framing is precise on purpose — not "royalties are guaranteed on-chain," but "policy lives in a hook that can enforce, and it travels with the token, and the creator can swap it."
Soulbound by default
An extra durability lever ships in ERC721SeaDropPausable: transfersPaused
starts true, so approve/setApprovalForAll revert and holder-initiated
transfers (from != 0) are blocked until the owner calls
updateTransfersPaused(false). Tokens are effectively soulbound between mint and
that flip. Siblings ERC721SeaDropSoulbound and ERC721SeaDropRandomOffset cover
the permanent-soulbound and randomized-reveal variants.
Three ways onto the allowlist
Eligibility is not one mechanism but three, each chosen for its context:
| Mechanism | Where | Leaf / proof | Trust model |
|---|---|---|---|
| SeaDrop Merkle mint | SeaDrop.mintAllowList | keccak256(abi.encode(minter, mintParams)) — proof carries per-minter params | On-chain proof against a per-contract root |
| SeaDrop signed mint | SeaDrop.mintSigned | EIP-712 domain-separated digest | Off-chain allowed-signer signature, single-use |
| Presale ECDSA claim | GKoiPresale.buyPresale | ECDSA.recover → hasRole(VALIDATOR_ROLE, signer) | Role-gated off-chain signature, deadline ≤ 5 min, single-use |
The SeaDrop Merkle path is a standard OpenZeppelin verify against the root set per
nftContract:
if (!MerkleProof.verify(proof, _allowListMerkleRoots[nftContract], keccak256(abi.encode(minter, mintParams)))) {
revert InvalidProof();
}
Note this leaf is distinct from the whitelist service's plain
keccak256(address) tree — same primitive, different payload — a distinction the
companion post pulls apart.
Deployment and routing
NftCollectionFactory.deploy is CREATE2 over packaged raw creation bytecode —
getBytecode concatenates type(ERC721Factory).creationCode with ABI-encoded
constructor args, and computeAddress pre-computes the deterministic address:
function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) external onlyOwner returns (address addr) {
address collectionAddress = Create2.deploy(amount, salt, bytecode);
emit CollectionCreated(collectionAddress, msg.sender, salt);
return collectionAddress;
}
The SeaDrop cloneable line takes the other road: OpenZeppelin Clones
(EIP-1167 minimal proxies) over a pre-deployed implementation, then
initialize(name, symbol, allowedSeaDrop, owner), with the salt mixed with
blockhash(block.number) so clone addresses don't collide across chains.
Conduit is Seaport's channel-gated router: users approve the conduit once, and
only controller-authorized channels may then move their tokens, gated by an
assembly onlyOpenChannel modifier reading the _channels mapping directly. The
token side pre-approves the conduit (ERC721AConduitPreapproved) so mint→list is
approval-free. And the caveat is quoted straight from the contract's own NatSpec,
not hidden: "a malicious or negligent owner can add a channel that allows for any
approved ERC20/721/1155 tokens to be taken immediately."
Testing & security posture
Foundry unit + fuzz tests across eleven suites, with test doubles under
src/test/ — a MockTransferValidator constructed to succeed or always revert lets
tests assert the _beforeTokenTransfers → validateTransfer call actually fires and
can block a transfer (vm.expectRevert("MockTransferValidator: always reverts")),
and that getTransferValidationFunction() advertises the right selector. Hardhat
coverage config is present alongside. Security-relevant defaults are all
conservative: transfers paused-by-default, royalty bps hard-capped at 10 000,
validator delegation opt-in and owner-gated, presale signatures single-use with a
five-minute deadline. Deploy artifacts (broadcast/) and script/*.s.sol RPC/key
references stay out of this writeup.