Smart Contracts
GNDX Protocol is composed of 19 implementation smart contracts that will be deployed to Arbitrum One. Audit posture and reports are governance-managed and will be published on the Security page once engagements are confirmed.
IndexVault.sol
The core treasury contract. Holds all underlying basket tokens and enforces the 10% single-token weight cap.
Key responsibilities:
- Stores all basket token balances
- Enforces
MAX_SINGLE_TOKEN_WEIGHT_BPS = 1000(10%) — hardcoded, immutable - Mint routing exclusion: Timelock may flag tokens so MintEngine does not buy them with new USDC; NAVOracle can notify the vault after a circuit-breaker rejection when auto-exclusion is enabled
- Manages token additions/removals via governance
- Implements auto-expiring pause (72h max)
Access control roles:
| Role | Holder | Capability |
|---|---|---|
MINTER_ROLE | MintEngine | depositBasket() |
REDEEMER_ROLE | RedeemEngine | withdrawBasket() |
REBALANCER_ROLE | RebalanceController | updateWeight(), withdrawForRebalance(), depositForRebalance() |
GUARDIAN_ROLE | GuardianMultisig | pause() only |
UPGRADER_ROLE | Timelock | _authorizeUpgrade() |
DEFAULT_ADMIN_ROLE | Timelock | addToken(), removeToken(), setMintRoutingExcluded(), setCircuitBreakerAutoExcludesMint() |
Key view functions:
function getBasketTokens() external view returns (address[] memory);
function getTokenInfo(address token) external view returns (TokenInfo memory);
function getCurrentWeightBps(address token) external view returns (uint256);
function getWeightDriftBps(address token) external view returns (int256); // positive = overweight
function getTotalNAVUSD() external view returns (uint256);
function getPauseStatus() external view returns (PauseStatus memory);
function MAX_SINGLE_TOKEN_WEIGHT_BPS() external view returns (uint256); // always 1000 (constant in implementation)
function isMintRoutingExcluded(address token) external view returns (bool);
MintEngine.sol
Handles $GNDX minting. Accepts USDC deposits, purchases basket tokens via DEX adapter (ISwapAdapter), and mints $GNDX.
Three execution paths:
| Path | Condition | Execution |
|---|---|---|
| Instant | Order below $25,000 USDC | Always same-transaction execution |
| Fuzzy zone | Order $25,000–$50,000 USDC | Probabilistic routing (quadratic curve favouring TWAP as amount rises) |
| TWAP | Order above $50,000 USDC | Dynamic chunks scaled to order size (4–24 chunks, 5–20 min interval) |
Rate limiting: Per-address rolling 24-hour window ($50K cumulative cap) + global protocol budget ($500K/hour). Both are governance-adjustable.
Dynamic TWAP scaling: Per-order chunk count = clamp(ceil(orderSize / $25K), 4, 24). Per-chunk interval scales with chunk size: ≤$15K→5min, ≤$30K→10min, ≤$60K→15min, >$60K→20min. Examples: $60K → 4 × 5 min (20 min); $250K → 10 × 10 min (1h 40min); $1M → 24 × 15 min (6 hours). orderChunks and chunkInterval are stored on the order at submission.
TWAP path: Any keeper (or anyone) calls executeTWAPChunk(orderId) and earns 0.01% of the chunk value. After the final chunk, anyone calls completeMintOrder(orderId, recipient) to mint $GNDX to recipient.
Value-based minting (anti-dilution): For routed paths, $GNDX is minted from the vault value increase caused by the swap ((vaultValueAfter − vaultValueBefore) × 1e18 / nav), not from raw USDC deposited. Swap friction (DEX fees + slippage) is borne by the minter; existing holders' NAV is preserved. Genesis case (vault empty) and non-routed paths fall back to USDC-based minting.
Stall recovery: If a TWAP order stalls because all basket tokens are CB-excluded, the depositor can call forceCompleteStalledOrder() after a 2-hour grace period to receive $GNDX for the deployed portion and a refund of remaining USDC.
Mint routing: Among routable tokens only, USDC is split by targets in the flat band or sent to the most underweight routable token; excluded/inactive tokens are skipped; if none routable, reverts AllMintRoutingExcluded.
Key functions:
function submitMintOrder(uint256 usdcAmount, uint256 slippageToleranceBps, address recipient)
external returns (uint256 orderId);
function executeTWAPChunk(uint256 orderId) external;
function completeMintOrder(uint256 orderId, address recipient) external;
function cancelOrder(uint256 orderId) external;
function forceCompleteStalledOrder(uint256 orderId, address recipient) external;
function getCumulativeDeposits(address depositor) external view returns (uint256);
function MAX_MINT_FEE_BPS() external pure returns (uint256); // always 25
RedeemEngine.sol
Handles $GNDX redemption. Three redemption paths.
Redemption paths:
| Path | Description | Fee |
|---|---|---|
| Basket | Proportional share of each basket token sent directly — no DEX, no slippage. Available even during oracle outages (max fee applies). | 0.20% |
| USDC | Basket tokens sold via DEX adapter, USDC sent to user | 0.20% |
| Overweight | Single overweight token redeemed with 0.22% bonus above NAV | 0.00% net (bonus offsets) |
The proportion calculation (userShare = gndxAmount / totalSupply) is computed before any $GNDX is burned. This is enforced in the contract. Computing after burn would give an incorrect (inflated) proportion.
NAVOracle.sol
Aggregates Chainlink price feeds and computes a 20-minute TWAP NAV per $GNDX.
Architecture:
- 8 price samples per token (circular buffer)
- Sampled every 5 minutes (
SAMPLE_INTERVAL = 300s) - TWAP uses last 4 samples = 20-minute window
samplePrices()is permissionless — any keeper can call it
Key functions:
function samplePrices() external; // permissionless
function getTokenTWAP(address token) external view returns (uint256 priceUSD18);
function getNAVPerToken() external view returns (uint256 navUSD18); // returns 1e18 at genesis
function getTotalVaultValueUSD() external view returns (uint256);
function allFeedsHealthy() external view returns (bool, address[] memory staleTokens);
function TWAP_WINDOW() external pure returns (uint256); // always 1200
function STALE_THRESHOLD() external pure returns (uint256); // always 3600
For circuit breaker details, see Oracle Infrastructure.
GNDXToken.sol
Arbitrum-native ERC-20 index token with elastic supply.
- Mintable only by MintEngine (
MINTER_ROLE) - Burnable only by RedeemEngine (
BURNER_ROLE) - UUPS upgradeable —
UPGRADER_ROLEheld by Timelock - Implements standard ERC-20 interface
GAMEToken.sol
Arbitrum-native ERC-20 governance token with ERC20Votes extension.
- Fixed supply: 200,000,000 tokens — all minted to treasury at initialization
- No mint function —
MINTER_ROLEis never granted after deployment - Lockable in VeGAME.sol for voting power
- UUPS upgradeable —
UPGRADER_ROLEheld by Timelock
VeGAME.sol
Vote-escrow contract for $GAME. Users lock $GAME and receive linearly-decaying veGAME.
Five valid lock durations: 3 months (0.25×), 6 months (0.50×), 1 year (1.00×), 2 years (2.00×), 4 years (4.00×). Any other duration reverts.
Key functions:
function lock(uint256 gameAmount, uint256 durationSeconds) external;
function addToLock(uint256 additionalGameAmount) external;
function extendLock(uint256 newDurationSeconds) external;
function withdraw() external; // after lock expiry
function claimRewards() external returns (uint256 gameClaimed);
function delegate(address delegatee) external;
function balanceOf(address account) external view returns (uint256);
function balanceOfAt(address account, uint256 timestamp) external view returns (uint256);
Linear decay formula:
veBalance(t) = gameAmountLocked × multiplierBps × (lockEnd − t) / (lockDuration × 10,000)
FeeCollector.sol
Accumulates all protocol fees and executes weekly distribution.
Weekly distribution (Shape A):
- Revenue: USDC mint fees in
pendingRevenue; streaming and exit fees accrue as $GNDX on the contract. - Swap contract $GNDX → USDC (via DEX adapter), then send
treasurySplitBpsof the USDC total to treasury (remainder = one swap leg). - Single USDC → $GAME swap on the full remainder (via DEX adapter); caller supplies
minGameOut > 0when remainder is non-zero. - Split the received $GAME between
VeGAME.depositFees()(staker leg) andGAME.burn()usingveGameSplitBpsandbuybackSplitBpsover denominatorveGameSplitBps + buybackSplitBps. No USDC is sent to VeGAME on this path. - VeGAME pays staker rewards in $GAME (pro-rata to initial ve).
Bps treasurySplitBps + veGameSplitBps + buybackSplitBps must equal 10,000. Default 65 / 25 / 10 matches the legacy 65% treasury and 25:10 split on the non-treasury leg.
RebalanceController.sol
Monitors live weights vs targets. Activates the 0.22% overweight bonus when drift exceeds 500 bps (5%). Provides emergency rebalance capability for active selling of overweight positions.
- Fully permissionless for incentive checking (
checkIncentive()pays $GAME rewards) - Called by governance for quarterly scheduled rebalancing
- Emergency rebalance:
executeEmergencyRebalance()sells overweight tokens through USDC (via DEX adapter) and buys underweight tokens — GOVERNANCE_ROLE only- Validates drift direction (overweight must have positive drift, underweight negative)
- Caps sell amount at 25% of vault balance per transaction
- 3% slippage tolerance using NAVOracle TWAP pricing
- Interfaces with IndexVault via
withdrawForRebalance()/depositForRebalance()
GNDXGovernor.sol
On-chain governance contract. Extends OpenZeppelin Governor with GNDX-specific supermajority logic.
Critical parameters:
- Voting power: veGAME balance at proposal snapshot
- Quorum: 5% of total veGAME supply
- Approval threshold: 66% supermajority of (FOR + AGAINST) votes
- Proposal threshold: 1,000 veGAME
- Voting period: 7 days
Parameter bounds enforcement: Before executing any proposal, the Governor checks:
FeeCollector.setStreamingFeeBps(x): reverts if x < 25 or x > 150IndexVault.updateWeight(token, x): reverts if x > 1000
A 100-0 governance vote cannot override these bounds.
Timelock.sol
Standard 48-hour delay for all governance actions. Contract upgrades require a 7-day delay (enforced at scheduling time).
PROPOSER_ROLE: GNDXGovernor onlyEXECUTOR_ROLE: GNDXGovernor only- Deployer renounces
DEFAULT_ADMIN_ROLEat end of deployment
GuardianMultisig.sol
5-of-8 multisig with exactly one capability: call IndexVault.pause().
- Non-upgradeable
- 72-hour auto-expiring pause only
- Cannot move funds, change parameters, execute upgrades, or extend the pause
- Signer composition: 3 core team, 2 independent security researchers, 2 community-elected, 1 legal/compliance advisor
PresaleVesting.sol
Holds presale participant $GAME allocations and enforces the on-chain mint condition before vesting begins.
- Non-upgradeable (immutable once allocations are registered)
- Monitors MintEngine.getCumulativeDeposits() for qualifying deposits
- Supports both presale (3-month cliff) and seed (6-month cliff) schedules
ISwapAdapter (DEX Adapter Pattern)
All DEX swaps across the protocol (MintEngine, RedeemEngine, FeeCollector, RebalanceController) route through a unified ISwapAdapter interface rather than calling Uniswap V3 directly. This enables governance to switch DEX backends without upgrading core contracts.
Two adapter implementations:
| Adapter | Use Case |
|---|---|
UniswapV3Adapter | Default — direct Uniswap V3 swaps with predictable gas. Stateless, non-upgradeable. |
AggregatorAdapter | Forwards pre-computed calldata to aggregators (1inch, Odos) for best-price execution on large trades. |
Governance can deploy a new adapter and call setSwapAdapter() on any contract to switch — no UUPS upgrade required.
See also: Oracle Infrastructure · Security