Multi-round token presale with integrated vesting on Base.
contracts/
├── SELFToken.sol (~20 lines - Standard ERC20)
└── SELFPresale.sol (~700 lines - Multi-round presale)
- Standard OpenZeppelin ERC20
- Fixed supply: 500,000,000 tokens
- OpenZeppelin v4.9.6
- 5 sequential rounds: February 1 - March 12, 2026
- Progressive pricing: $0.06 → $0.10 per token
- Target raise: $2.5M ($500k soft cap, $2.5M hard cap)
- Contribution limits: $100 - $10,000 per wallet (cumulative)
- Vesting: 30-50% TGE unlock + linear 10-month vesting
- Payment: USDC (native Circle) 6 decimals
- OpenZeppelin v4.9.6: AccessControl, ReentrancyGuard, Pausable, SafeERC20
- Role-based access control (5 roles)
- Timelock delays (2-7 days on critical operations)
- Circuit breaker ($500k daily withdrawal limit)
- Refund mechanism (if soft cap not met)
- Unclaimed refund recovery (after 30-day window)
- Flash loan protection (2-block cooldown)
- Whale protection (10% max per tx)
- Rate limiting ($100k/hour per wallet)
- Custom errors (gas optimized)
Base USDC uses 6 decimals (native Circle USDC).
Contract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
uint256 constant MIN_CONTRIBUTION = 100 * 1e6; // $100
uint256 constant MAX_CONTRIBUTION = 10_000 * 1e6; // $10,000
uint256 constant SOFT_CAP = 500_000 * 1e6; // $500k
uint256 constant HARD_CAP = 2_500_000 * 1e6; // $2.5MTotal Supply: 500,000,000 SELF (fixed, non-mintable)
Initial Distribution:
- All tokens minted to multisig wallet:
0x8b2fE271c13C94c679b1fF69466C2D6d034b2e8c - Presale allocation transferred to presale contract: sufficient for all potential claims
- Remaining tokens retained by multisig for ecosystem development
Security Measures (SEA-01):
- 2-of-3 multisig controls all undistributed tokens
- Private key security managed via hardware wallets and geographic distribution
- Full token allocation published at:
https://docs.self.app/tokenomics - Team committed to implementing vesting schedules for non-presale allocations
All privileged contract roles and undistributed tokens are controlled by a 2-of-3 multisig wallet for security.
Safe Multisig Address: 0x8b2fE271c13C94c679b1fF69466C2D6d034b2e8c
Signers:
- Signer 1:
0x0Ef1692fb24e9baFCdF599f72fBe81841E52c349 - Signer 2:
0xD7286BB3983316FF3b2e8A27CABc976aA820Ac97 - Signer 3:
0xF1164C0208168676DF682f7b66AFF4921ec4bF32
Verification: Multisig configuration (threshold and owners) can be verified upon request via screen-share or by providing a verification script. Role assignments on the presale contract are publicly verifiable via BaseScan.
- DEFAULT_ADMIN_ROLE
- TREASURY_ROLE
- TGE_ENABLER_ROLE
- ROUND_MANAGER_ROLE
- PAUSER_ROLE
All privileged operations require 2-of-3 signer approval plus a timelock delay.
All critical operations enforce mandatory timelock delays for community transparency:
| Operation | Timelock | Role Required | Guardrails |
|---|---|---|---|
| Enable TGE | 2 days | TGE_ENABLER_ROLE | Requires soft cap reached, all rounds finalized |
| Withdraw USDC | 2 days | TREASURY_ROLE | Circuit breaker: $500k daily limit |
| Emergency SELF Withdrawal | 7 days | DEFAULT_ADMIN_ROLE | Blocked if any user allocations exist |
| Update Rate Limit | None | DEFAULT_ADMIN_ROLE | Bounded: $100 - $1M range |
| Pause/Unpause | None | PAUSER_ROLE | Emergency circuit breaker only |
Monitoring Events:
TimelockRequested(action, executionTime)- Initiates countdownTimelockExecuted(action)- Confirms execution after delayTimelockCancelled(action)- Operation cancelled before execution
All events are publicly visible on BaseScan for community monitoring.
Centralization Mitigation:
- Short-term: Timelock + multisig combination (implemented)
- Long-term: DAO governance transition planned post-launch
- Permanent: Critical user protections enforced on-chain (solvency, soft cap)
Published source: https://docs.self.app/tokenomics
The presale contract enforces critical security invariants in code, not operational policy:
- USDC Refunds: Withdrawals blocked until presale succeeds (soft cap reached + ended). Refund path remains available if soft cap not met.
- SELF Token Claims: Contributions require sufficient SELF balance on-chain. Contract verifies
balance >= outstandingClaims + newAllocationbefore accepting contributions. - Emergency Safeguards: Emergency SELF withdrawals blocked once any user allocations exist.
- Token Generation Event can only be enabled once
- Multiple layers prevent TGE time from being changed after activation:
- Pending request check (prevents request overwrites)
- Execution guard (prevents multiple executions)
- Explicit cancellation required to replace pending requests
- TGE enablement requires
totalRaised >= SOFT_CAP(hard-checked in both request and execution) - Refunds automatically available if soft cap not reached after presale ends
- Mutual exclusivity: TGE and refunds cannot both be active
- Precision Math: All token calculations round up in favor of users
- Dust Handling: Allows exact completion of rounds when remaining capacity < minimum contribution
- Refund Accounting: Both global (
totalRaised) and per-round (rounds[i].raised) amounts decrement on refunds, maintaining accounting consistency - 30-Day Refund Window: Users have 30 days to claim refunds if soft cap not met
Even with compromised privileged keys, users remain protected:
- Cannot drain USDC before presale successfully completes
- Cannot drain SELF tokens needed for user claims
- Cannot change TGE time after activation
- Cannot bypass soft cap requirements
- Timelock delays provide transparency window (2-7 days)
- Circuit breaker limits withdrawal velocity ($500k/day)
Public view functions for external verification:
getExcessSELFBalance()- Shows withdrawable excess vs. outstanding claimsgetClaimableAmount(user)- Shows user's vested + unlocked tokensgetUserContribution(user)- Complete user allocation breakdowngetPresaleStats()- Aggregate presale state
All privileged operations emit events for on-chain monitoring.
CertiK Audit: All findings addressed (14/14)
- 2 Medium severity issues resolved with hard on-chain enforcement
- 5 Minor protocol correctness issues fixed
- 5 Design/informational improvements implemented
- 2 Centralization disclosures provided with verifiable evidence
Post-Audit Fix (V1.4): Per-round accounting consistency
- Issue:
claimRefund()decrementedtotalRaisedbut did not decrementrounds[i].raised, causing per-round statistics to become permanently inflated after refunds - Resolution:
claimRefund()now decrements bothtotalRaisedand eachrounds[i].raisedusing the user'scontributionsByRounddata before clearing it - Invariant Maintained:
Σ rounds[i].raised == totalRaisedholds true after refunds
Centralization Findings:
- SEA-01: Initial token distribution controlled by multisig - addressed via 2-of-3 Safe, published allocation, hardware wallet key management
- SEA-02: Privileged roles - mitigated via timelock delays (2-7 days), circuit breakers, multisig, and on-chain invariants
Unsold Token Recovery (SEA-16):
The contract implements comprehensive protection against locked tokens in both success and failure scenarios:
Success Path (Soft Cap Reached):
withdrawExcessSELF()allows treasury to reclaim unsold tokens after TGE- Protected: Cannot withdraw tokens needed for outstanding user claims
- Formula:
excess = balance - (totalAllocated - totalClaimed)
Failure Path (Soft Cap Not Reached):
enableRefunds()allows users to claim full USDC refundsrecoverUnclaimedRefunds()recovers USDC after 30-day window expiresexecuteEmergencyWithdrawSELF()recovers all SELF tokens (7-day timelock)- After refund deadline expires, SELF recovery is allowed even if allocations remain (users forfeited claims)
Mutual Exclusivity: tgeEnabled and refundEnabled cannot both be true, ensuring tokens are never permanently locked. The withdrawExcessSELF function correctly blocks withdrawals when refunds are active, as SELF tokens must remain available for the alternative recovery path.
Test Coverage: 53 passing tests, zero compiler warnings
Deployed Contracts (Base Mainnet):
- SELFToken: Pending deployment
- SELFPresale: Pending deployment
npm install
npx hardhat testTests cover token functionality, presale logic, vesting, and edge cases.
contracts/test/MockUSDC.sol is a test utility only (6-decimal USDC simulator).
contracts/
├── SELFToken.sol # Audit scope
├── SELFPresale.sol # Audit scope
└── test/MockUSDC.sol # Test utility
test/
├── SELFToken.test.cjs
└── SELFPresale.test.cjs
scripts/
├── deploy-token.js
├── deploy-presale.js
├── initialize-rounds.js
└── verify-contracts.js
docs/
└── architecture.md
Network: Base Mainnet
USDC: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Compiler: Solidity 0.8.20
Launch: February 1, 2026
Audit Ready: December 14, 2025
Skyharbor Updated V1.1: December 25, 2025
Skyharbor Updated V1.2: December 30, 2025
SEA-16 Fix V1.3: December 31, 2025
Per-Round Accounting Fix V1.4: January 1, 2026
Base Migration V1.5: March 13, 2026