Skip to main content

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:

RoleHolderCapability
MINTER_ROLEMintEnginedepositBasket()
REDEEMER_ROLERedeemEnginewithdrawBasket()
REBALANCER_ROLERebalanceControllerupdateWeight(), withdrawForRebalance(), depositForRebalance()
GUARDIAN_ROLEGuardianMultisigpause() only
UPGRADER_ROLETimelock_authorizeUpgrade()
DEFAULT_ADMIN_ROLETimelockaddToken(), 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:

PathConditionExecution
InstantOrder below $25,000 USDCAlways same-transaction execution
Fuzzy zoneOrder $25,000–$50,000 USDCProbabilistic routing (quadratic curve favouring TWAP as amount rises)
TWAPOrder above $50,000 USDCDynamic 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:

PathDescriptionFee
BasketProportional share of each basket token sent directly — no DEX, no slippage. Available even during oracle outages (max fee applies).0.20%
USDCBasket tokens sold via DEX adapter, USDC sent to user0.20%
OverweightSingle overweight token redeemed with 0.22% bonus above NAV0.00% net (bonus offsets)
Critical Ordering

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.


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_ROLE held 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 functionMINTER_ROLE is never granted after deployment
  • Lockable in VeGAME.sol for voting power
  • UUPS upgradeable — UPGRADER_ROLE held 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):

  1. Revenue: USDC mint fees in pendingRevenue; streaming and exit fees accrue as $GNDX on the contract.
  2. Swap contract $GNDX → USDC (via DEX adapter), then send treasurySplitBps of the USDC total to treasury (remainder = one swap leg).
  3. Single USDC → $GAME swap on the full remainder (via DEX adapter); caller supplies minGameOut > 0 when remainder is non-zero.
  4. Split the received $GAME between VeGAME.depositFees() (staker leg) and GAME.burn() using veGameSplitBps and buybackSplitBps over denominator veGameSplitBps + buybackSplitBps. No USDC is sent to VeGAME on this path.
  5. 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 > 150
  • IndexVault.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 only
  • EXECUTOR_ROLE: GNDXGovernor only
  • Deployer renounces DEFAULT_ADMIN_ROLE at 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:

AdapterUse Case
UniswapV3AdapterDefault — direct Uniswap V3 swaps with predictable gas. Stateless, non-upgradeable.
AggregatorAdapterForwards 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