AZ
Back to constellation
Web3Draft

GKOI Contracts

The Foundry/Solidity contracts behind GKOI — a SeaDrop ERC721A collection with a swappable Creator-Token transfer validator, a CREATE2 factory, and a Conduit router.

SolidityFoundryHardhatSeaDrop (ERC721A)ICreatorToken / ITransferValidatorERC-2981OpenZeppelinCREATE2 + EIP-1167

Overview

What it is

The on-chain half of GKOI: four Foundry/Hardhat Solidity repos, each with its own foundry.toml, test/, and script/.

RepoRolePragmaDeploy mechanism
gkoi-erc721ACThe mint collection — a SeaDrop-based ERC721A with a Creator-Token transfer-validator hook0.8.17EIP-1167 clones (SeaDrop cloneable line)
NftCollectionFactoryDeterministic collection deployer^0.8.28CREATE2 over raw creation bytecode
ConduitSeaport channel-authorized transfer router (@author 0age)0.8.14Deployed/controlled by a ConduitController
gkoi-smart-contractsERC-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:

MechanismWhereLeaf / proofTrust model
SeaDrop Merkle mintSeaDrop.mintAllowListkeccak256(abi.encode(minter, mintParams)) — proof carries per-minter paramsOn-chain proof against a per-contract root
SeaDrop signed mintSeaDrop.mintSignedEIP-712 domain-separated digestOff-chain allowed-signer signature, single-use
Presale ECDSA claimGKoiPresale.buyPresaleECDSA.recoverhasRole(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 bytecodegetBytecode 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.

Related write-ups

Write-up in progress

A companion post for this project is being drafted. Check back soon.

GKOI Contracts · Abdullah Zakariyya