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
| Property | Value |
|---|---|
| Multisig | 3Wt7pitDH9ufwpsYwnbbYhYWXZqsGWNqmZRUgHQXa5Z4 |
| Vault | 3EkpyVQAzFSynkxCfDJKUp2YdUEPG2H63hZZU4vsbwFx |
| Threshold | 6-of-7 |
| Members | 7 added |
| Status | Active |
Post-mainnet deployment, all program upgrade authorities transfer to the Squads multisig.
EVM -- Gnosis Safe
| Property | Value |
|---|---|
| Safe | 0xb0599ce3595Ed3768B08B9B4e7d96AF309a7f49a |
| Chain | Base (Chain ID: 8453) |
| Threshold | 6-of-7 |
| Treasury | Same address (Safe holds treasury) |
| Status | Active |
Circuit Breaker
Both chains implement an automatic circuit breaker to halt operations after consecutive failures.
EVM Implementation (Security.sol)
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
}| Parameter | Default |
|---|---|
DEFAULT_FAILURE_THRESHOLD | 5 consecutive failures |
DEFAULT_CIRCUIT_COOLDOWN | 1 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)
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,
}| Parameter | Default |
|---|---|
DEFAULT_THRESHOLD | 5 |
DEFAULT_COOLDOWN | 3,600 seconds (1 hour) |
| Account Size | 60 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
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:
| Operation | Max/Hour | Max/Day | Cooldown |
|---|---|---|---|
| Deposit | 10 | 50 | 10 seconds |
| Claim | 20 | 100 | 5 seconds |
Owner can reconfigure via setDepositRateLimit() and setClaimRateLimit().
CoreModule Rate Limits
| Limit | Value |
|---|---|
| Max missions created per day | 100 |
| Max mission joins per day | 500 |
| Min creation interval | 60 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 > 5EVM: 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:
| Chain | Signature Scheme | Verification |
|---|---|---|
| EVM | ECDSA (secp256k1) | ecrecover with ethSignedMessageHash |
| Solana | Ed25519 | ed25519_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.
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
}| Parameter | Value |
|---|---|
MIN_DELAY | 1 day |
MAX_DELAY | 30 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:
- Contract attempts to send ETH to winner via
claimReward() - If transfer fails (recipient is a contract that rejects, etc.)
- Amount stored in
pendingWithdrawals[recipient] - 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
| Contract | Guard Type |
|---|---|
| StakingModule | OZ ReentrancyGuard |
| VestingModule | OZ ReentrancyGuard |
| MissionModule | OZ ReentrancyGuard |
| NftModule | OZ ReentrancyGuard |
| AirdropModule | Custom reentrancy guard (NOT OZ) |
| CoreModule | None (no external calls with value) |
| EconomyModule | None (no external calls with value) |
The AirdropModule uses a custom _locked storage variable instead of OpenZeppelin's ReentrancyGuard.
Anti-Whale Protection
| Mechanism | Value | Purpose |
|---|---|---|
| Max stake per wallet | 100,000 tokens | Prevent staking concentration |
| Max NFTs per wallet | 2 | Prevent NFT hoarding |
| Max roles per wallet | 2 | Distribute roles across users |
| Annual emission cap | 10,000,000 tokens | Prevent inflation |
| Min holders for staking | 1,111 | Ensure distribution |
| Min TVL for staking | $111,111 | Ensure liquidity |
Token Security Features
| Feature | Chain | Description |
|---|---|---|
| Pausable | EVM | Owner can pause all transfers (emergency) |
| Platform Active | Solana | is_active flag in PlatformConfig |
| Freezable | EVM | Owner can freeze individual accounts |
| Burnable | Both | Any holder can burn their own tokens |
| UUPS Upgradeable | EVM | Contracts can be upgraded (owner only) |
| Program Upgrades | Solana | Via Squads multisig (post-mainnet) |
| Storage Gaps | EVM | 48-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).
| Module | Source Range | On-Chain Range | Key Errors |
|---|---|---|---|
| Core | 100-199 | 6100-6199 | 6110 Unauthorized, 6120 ProofExpired, 6130 PlatformNotActive |
| Economy | 200-299 | 6200-6299 | 6211 InvalidXpAmount, 6230 AlreadyCheckedInToday |
| NFT | 300-499 | 6300-6499 | 6312 MaxSharesReached, 6331 InsufficientPayment |
| Mission | 500-799 | 6500-6799 | 6500 EscrowNotActive, 6560 CircuitBreakerOpen |
| Token | 800-999 | 6800-6999 | 6810 SupplyExceeded, 6840 ProgramPaused |
| Staking | 1000-1199 | 7000-7199 | 7004 StakingDisabled, 7005 ExceedsMaxStake |
| Vesting | 1200-1399 | 7200-7399 | 7201 CliffNotPassed, 7202 VestingRevoked |
| Airdrop | 1400-1499 | 7400-7499 | 7400 AirdropNotActive, 7401 InvalidProof |
Upgrade Path
EVM
- Deploy new implementation contract
- Multisig (6-of-7) approves upgrade transaction
- Timelock delay (1-30 days) via
Security.TimelockOp - UUPS proxy delegates to new implementation
- Storage layout preserved via 48-50 slot gaps
Solana
- Build new program binary
- Submit upgrade via Squads multisig (6-of-7)
- Members approve (6 of 7 required)
- Program authority executes upgrade
- Account data structures versioned
Audit Status
| Item | Status |
|---|---|
| Internal security review | Complete |
| 400+ automated tests | Passing |
| External audit | Planned (pre-mainnet) |
| Bug bounty program | Planned |
Related
- Contract Architecture -- design patterns, proxy model
- EVM Contracts -- contract details, custom errors
- Solana Programs -- program details, PDA seeds
- Governance -- decentralization roadmap