Cycle

The Scriptorium

Smart Assembly code templates and tools for on-chain development in Eve Frontier.

Building Your First Gate
IntermediateChapter 2 of 420 min read

Implementing Access Control

The heart of any Smart Gate is its access control system. In this chapter, we'll build a flexible access control system that supports public, whitelist, and tribe-based access.

The Whitelist Table

First, add a table for whitelisted addresses:

typescript
// Add to mud.config.ts
GateWhitelist: {
  keySchema: {
    gateId: "uint256",
    player: "address",
  },
  valueSchema: {
    isAllowed: "bool",
    addedAt: "uint256",
  },
},

Access Control System

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import { System } from "@latticexyz/world/src/System.sol";
import { GateConfig } from "../codegen/tables/GateConfig.sol";
import { GateWhitelist } from "../codegen/tables/GateWhitelist.sol";

contract GateAccessSystem is System {
  error NotGateOwner(uint256 gateId, address caller);
  error InvalidAccessMode(uint8 mode);

  uint8 constant ACCESS_PUBLIC = 0;
  uint8 constant ACCESS_WHITELIST = 1;
  uint8 constant ACCESS_TRIBE = 2;

  function setAccessMode(uint256 gateId, uint8 mode) public {
    _requireOwner(gateId);
    if (mode > ACCESS_TRIBE) {
      revert InvalidAccessMode(mode);
    }
    GateConfig.setAccessMode(gateId, mode);
  }

  function addToWhitelist(uint256 gateId, address player) public {
    _requireOwner(gateId);
    GateWhitelist.set(gateId, player, true, block.timestamp);
  }

  function removeFromWhitelist(uint256 gateId, address player) public {
    _requireOwner(gateId);
    GateWhitelist.set(gateId, player, false, 0);
  }

  function checkAccess(uint256 gateId, address player) public view returns (bool) {
    uint8 mode = GateConfig.getAccessMode(gateId);

    if (mode == ACCESS_PUBLIC) {
      return true;
    }

    if (mode == ACCESS_WHITELIST) {
      return GateWhitelist.getIsAllowed(gateId, player);
    }

    if (mode == ACCESS_TRIBE) {
      return _isTribeMember(gateId, player);
    }

    return false;
  }

  function _requireOwner(uint256 gateId) internal view {
    address owner = GateConfig.getOwner(gateId);
    if (_msgSender() != owner) {
      revert NotGateOwner(gateId, _msgSender());
    }
  }

  function _isTribeMember(uint256 gateId, address player) internal view returns (bool) {
    // In production, this would check the tribe registry
    // For now, we'll use a simplified check
    address owner = GateConfig.getOwner(gateId);
    return player == owner; // Placeholder
  }
}

Key Design Decisions

Why Separate Systems?

We split gate usage and access control into separate systems because:

  • Single Responsibility — each system has one job
  • Upgradeability — you can upgrade access logic without touching gate usage
  • Reusability — the access system could be used by other assemblies
  • Why Onchain Access Control?

    Checking access on-chain guarantees enforcement. A client-side check could be bypassed, but a revert in Solidity is final.

    In the next chapter, we'll add toll collection and fee management.

    Sign in to track your progress.