Skip to content

Security

SUDIGITAL implements defense-in-depth security across both chains -- multisig governance, circuit breakers, rate limiting, anti-replay protection, timelock delays, and pull-payment patterns.

Multisig Governance

Both chains use 6-of-7 multisig wallets for all administrative operations.

Solana -- Squads v4

PropertyValue
Multisig3Wt7pitDH9ufwpsYwnbbYhYWXZqsGWNqmZRUgHQXa5Z4
Vault3EkpyVQAzFSynkxCfDJKUp2YdUEPG2H63hZZU4vsbwFx
Threshold6-of-7
Members7 added
StatusActive

Post-mainnet deployment, all program upgrade authorities transfer to the Squads multisig.

EVM -- Gnosis Safe

PropertyValue
Safe0xb0599ce3595Ed3768B08B9B4e7d96AF309a7f49a
ChainBase (Chain ID: 8453)
Threshold6-of-7
TreasurySame address (Safe holds treasury)
StatusActive

Circuit Breaker

Both chains implement an automatic circuit breaker to halt operations after consecutive failures.

EVM Implementation (Security.sol)

solidity
struct CircuitBreaker {
    bool isOpen;             // True = halted
    uint64 openedAt;         // When opened
    uint64 cooldownEnds;     // When can be closed
    uint32 failureCount;     // Consecutive failures
    uint32 failureThreshold; // Failures before opening
    uint32 cooldownDuration; // Seconds before reset
}
ParameterDefault
DEFAULT_FAILURE_THRESHOLD5 consecutive failures
DEFAULT_CIRCUIT_COOLDOWN1 hour (3,600 seconds)

Functions: recordFailure(), recordSuccess(), isAllowed(), tryReset()

EVM requires an explicit tryReset() call to close the breaker after cooldown.

Solana Implementation (circuit_breaker.rs)

rust
pub struct CircuitBreaker {
    pub failure_count: u8,      // u8 (vs EVM u32)
    pub is_open: bool,
    pub opened_at: i64,
    pub cooldown_seconds: i64,
    pub failure_threshold: u8,
    pub authority: Pubkey,
    pub bump: u8,
}
ParameterDefault
DEFAULT_THRESHOLD5
DEFAULT_COOLDOWN3,600 seconds (1 hour)
Account Size60 bytes
Seeds["circuit_breaker"]

Functions: record_failure(), record_success(), is_allowed(), reset(), try_auto_reset()

Key difference: Solana has try_auto_reset() -- the is_allowed() method implicitly checks if the cooldown has elapsed and auto-resets, returning true without requiring a manual reset call.

Behavior

Normal operation:
  → recordSuccess() → failureCount = 0

Failure sequence:
  → recordFailure() × 5 → circuit opens → operations blocked
  → Wait 1 hour → tryReset() (EVM) / auto-reset (Solana)
  → Circuit closes → operations resume

Manual override:
  → Admin/authority calls reset (both chains)

Rate Limiting (EVM)

Rate limiting is implemented in Security.sol as a reusable library, consumed by the MissionModule.

Library Types

solidity
struct RateLimitConfig {
    uint32 maxPerHour;
    uint32 maxPerDay;
    uint32 cooldownSeconds;
}

struct RateLimitState {
    uint32 hourlyCount;
    uint32 dailyCount;
    uint64 lastActionTime;
    uint64 hourStartTime;
    uint64 dayStartTime;
}

Library defaults: DEFAULT_MAX_PER_HOUR = 20, DEFAULT_MAX_PER_DAY = 100, DEFAULT_COOLDOWN = 5 seconds.

MissionModule Rate Limits

The MissionModule configures separate limits for deposit and claim operations:

OperationMax/HourMax/DayCooldown
Deposit105010 seconds
Claim201005 seconds

Owner can reconfigure via setDepositRateLimit() and setClaimRateLimit().

CoreModule Rate Limits

LimitValue
Max missions created per day100
Max mission joins per day500
Min creation interval60 seconds

These are per-user per-day limits, enforced in the CoreModule.

Anti-Replay Protection

Every user maintains a monotonically increasing nonce counter on both chains:

User submits proof with nonce = 5
Contract checks: 5 > user.last_nonce (4) ✓
Contract updates: user.last_nonce = 5
Next proof must have nonce > 5

EVM: Tracked in CoreModule via getUserClaimState() and isValidNonce(). Solana: Tracked per-program in UserClaimState (Core) and AirdropClaimState (Token).

This prevents:

  • Replay attacks -- resubmitting the same signed proof
  • Front-running -- cannot reorder proofs because nonces must increase
  • Double-spending -- each claim ID + nonce pair is unique

Backend-Signed Proofs

All sensitive operations require a proof signed by the trusted backend_authority:

ChainSignature SchemeVerification
EVMECDSA (secp256k1)ecrecover with ethSignedMessageHash
SolanaEd25519ed25519_program instruction verification

Exception: Solana Mission's record_winner does NOT use Ed25519 instruction verification. It relies on Solana runtime's native signer verification -- the backend_authority must be an account Signer, which proves identity at the protocol level.

The backend authority is set during initialization and can be rotated:

  • EVM: CoreModule.updateBackendAuthority() (Owner only)
  • Solana: Core.update_platform_config() (WALLET_CREATOR only)

Proof expiry: Each proof includes an expiry timestamp. Proofs older than 5 minutes are rejected on-chain (both chains default to 300-second validity).

Timelock (EVM Library)

The Security.sol library provides a timelock primitive for delayed admin operations. There is no standalone Timelock contract -- modules opt in by using the TimelockOp struct.

solidity
struct TimelockOp {
    bytes32 operationId;   // Unique operation identifier
    uint64 scheduledAt;    // When scheduled
    uint64 executableAt;   // When can be executed
    bool executed;         // Whether executed
    bool cancelled;        // Whether cancelled
}
ParameterValue
MIN_DELAY1 day
MAX_DELAY30 days

Functions: scheduleOperation(op, delay), isReady(op), executeOperation(op), cancelOperation(op)

Events (in Events.sol):

  • TimelockScheduled(operationId, proposer, executableAt)
  • TimelockExecuted(operationId, executor, timestamp)
  • TimelockCancelled(operationId, canceller, timestamp)

This gives the community time to review and react to proposed admin changes.

Pull-Payment Pattern (EVM)

The MissionModule uses a pull-payment pattern for ETH transfers:

  1. Contract attempts to send ETH to winner via claimReward()
  2. If transfer fails (recipient is a contract that rejects, etc.)
  3. Amount stored in pendingWithdrawals[recipient]
  4. Recipient calls withdrawPending() to claim later

This prevents:

  • DoS via revert -- a malicious contract can't block rewards for others
  • Gas limit issues -- failed transfers don't halt batch operations
  • Fund lockup -- failed payments are always recoverable

Reentrancy Protection

ContractGuard Type
StakingModuleOZ ReentrancyGuard
VestingModuleOZ ReentrancyGuard
MissionModuleOZ ReentrancyGuard
NftModuleOZ ReentrancyGuard
AirdropModuleCustom reentrancy guard (NOT OZ)
CoreModuleNone (no external calls with value)
EconomyModuleNone (no external calls with value)

The AirdropModule uses a custom _locked storage variable instead of OpenZeppelin's ReentrancyGuard.

Anti-Whale Protection

MechanismValuePurpose
Max stake per wallet100,000 tokensPrevent staking concentration
Max NFTs per wallet2Prevent NFT hoarding
Max roles per wallet2Distribute roles across users
Annual emission cap10,000,000 tokensPrevent inflation
Min holders for staking1,111Ensure distribution
Min TVL for staking$111,111Ensure liquidity

Token Security Features

FeatureChainDescription
PausableEVMOwner can pause all transfers (emergency)
Platform ActiveSolanais_active flag in PlatformConfig
FreezableEVMOwner can freeze individual accounts
BurnableBothAny holder can burn their own tokens
UUPS UpgradeableEVMContracts can be upgraded (owner only)
Program UpgradesSolanaVia Squads multisig (post-mainnet)
Storage GapsEVM48-50 reserved slots for future upgrades

Note: NftModule does NOT inherit PausableUpgradeable. Instead, it checks coreModule.isPlatformActive() for pause behavior.

Error Code System

Error messages are stored off-chain (human-readable descriptions mapped to codes). On-chain errors contain only numeric codes.

EVM: String constants in Errors.sol (e.g., string public constant UNAUTHORIZED = "6110"). Solana: Numeric error codes in Anchor #[error_code] enums. IDL adds 6000 offset (source code 100 becomes 6100 in the IDL).

ModuleSource RangeOn-Chain RangeKey Errors
Core100-1996100-61996110 Unauthorized, 6120 ProofExpired, 6130 PlatformNotActive
Economy200-2996200-62996211 InvalidXpAmount, 6230 AlreadyCheckedInToday
NFT300-4996300-64996312 MaxSharesReached, 6331 InsufficientPayment
Mission500-7996500-67996500 EscrowNotActive, 6560 CircuitBreakerOpen
Token800-9996800-69996810 SupplyExceeded, 6840 ProgramPaused
Staking1000-11997000-71997004 StakingDisabled, 7005 ExceedsMaxStake
Vesting1200-13997200-73997201 CliffNotPassed, 7202 VestingRevoked
Airdrop1400-14997400-74997400 AirdropNotActive, 7401 InvalidProof

Upgrade Path

EVM

  1. Deploy new implementation contract
  2. Multisig (6-of-7) approves upgrade transaction
  3. Timelock delay (1-30 days) via Security.TimelockOp
  4. UUPS proxy delegates to new implementation
  5. Storage layout preserved via 48-50 slot gaps

Solana

  1. Build new program binary
  2. Submit upgrade via Squads multisig (6-of-7)
  3. Members approve (6 of 7 required)
  4. Program authority executes upgrade
  5. Account data structures versioned

Audit Status

ItemStatus
Internal security reviewComplete
400+ automated testsPassing
External auditPlanned (pre-mainnet)
Bug bounty programPlanned

One backend. Three products. One token.