Smart Contracts
SignalNet uses smart contracts on Base Sepolia for trustless staking, payout settlement, and signal commitment.
Deployed Contracts
All contracts are live on Base Sepolia (chain ID: 84532).
| Contract | Address | Explorer |
|---|---|---|
| SignalNetToken | 0xdECC2df65B67e4b2e505a2816B334961AF16E773 | View on BaseScan |
| StakeVault | 0x225A784ec3C0178316aD0a083D2327CE1b0d04Ab | View on BaseScan |
| Tournament | 0x6CCB115056B87D87AEf27C4f02084F495f302f98 | View on BaseScan |
Contracts are currently deployed on Base Sepolia testnet. Mainnet deployment will follow after the Genesis Round completes successfully.
Contract Architecture
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ SignalNetToken │────▶│ StakeVault │◀────│ Tournament │
│ (ERC-20 + Mint) │ │ (Holds Funds)│ │ (Round Logic) │
└─────────────────┘ └──────────────┘ └─────────────────┘
SignalNetToken mints and manages the SIGNAL token. StakeVault holds all staked tokens with per-round accounting. Tournament orchestrates the round lifecycle and is the only contract authorized to operate the vault.
SignalNetToken (SIGNAL)
ERC-20 governance and staking token with controlled emission.
| Property | Value |
|---|---|
| Name | SignalNet |
| Symbol | SIGNAL |
| Decimals | 18 |
| Max Supply | 1,000,000,000 SIGNAL |
| Initial Mint | 400,000,000 SIGNAL (treasury) |
| Standard | ERC-20 + ERC-2612 Permit |
Key Features
- Capped supply —
totalMintedcan never exceedMAX_SUPPLY(1B tokens) - Minter roles — Only authorized addresses (Tournament contract, owner) can mint
- Permit support — Gasless approvals via EIP-2612 signatures
- Burnable — Tokens can be permanently burned
// Mint (only by authorized minters)
function mint(address to, uint256 amount) external onlyMinter;
// Set minter authorization
function setMinter(address account, bool authorized) external onlyOwner;
StakeVault
Holds staked SIGNAL tokens with per-round, per-user accounting. Only the Tournament contract can move funds.
Functions
// Deposit stake (called by Tournament during signal submission)
function deposit(uint256 roundId, address user, uint256 amount) external onlyOperator;
// Withdraw stake + reward after resolution
function withdraw(uint256 roundId, address user, uint256 amount) external onlyOperator;
// Slash a contributor's stake (max 25%)
function slash(uint256 roundId, address user, uint256 amount) external onlyOperator;
// View effective stake after slashing
function effectiveStake(uint256 roundId, address user) external view returns (uint256);
Why a Separate Vault?
- Separation of concerns — Tournament handles logic, Vault handles money
- Per-round isolation — Stakes in Round 5 can't be affected by Round 6
- Auditability — Every deposit, withdrawal, and slash emits an event
Tournament
Manages the full round lifecycle: creation, submission, resolution, and payout claims.
Constants
| Constant | Value | Description |
|---|---|---|
MIN_STAKE | 100 SIGNAL | Minimum stake per submission |
MAX_STAKE | 10,000 SIGNAL | Maximum stake per submission |
MAX_MODELS_PER_USER | 3 | Up to 3 models per account per round |
SLASH_BPS | 2500 (25%) | Maximum loss per round |
PAYOUT_MULTIPLIER_BPS | 2500 (25%) | Score × Stake × 0.25 |
Round Lifecycle
Active → Closed → Resolving → Resolved
│ │
└──── Cancelled ◀──────────────┘ (emergency only)
- Active — Accepting submissions + stakes
- Closed — Submission window ended, signal merkle root committed
- Resolving — Oracle computing scores (20 trading days)
- Resolved — Results merkle root posted, claims open
- Cancelled — Emergency escape hatch, full refunds
Core Functions
// Round management (manager only)
function createRound(uint256 closeTime, uint256 resolveTime, uint256 rewardPool) external;
function closeRound(uint256 roundId, bytes32 signalMerkleRoot) external;
function resolveRound(uint256 roundId, bytes32 resultsMerkleRoot) external;
function cancelRound(uint256 roundId) external; // owner only
// Contributor actions
function submitSignal(uint256 roundId, bytes32 signalHash, uint256 stakeAmount, uint8 modelIndex) external;
function claimReward(uint256 roundId, uint8 modelIndex, uint256 payoutAmount, bytes32[] proof) external;
function reclaimStake(uint256 roundId, uint8 modelIndex) external; // cancelled rounds only
Multi-Model Support
Each contributor can submit up to 3 independent models per round, each with its own stake and score:
tournament.submitSignal(roundId, hashModel0, 1000e18, 0); // Model 0
tournament.submitSignal(roundId, hashModel1, 2000e18, 1); // Model 1
tournament.submitSignal(roundId, hashModel2, 500e18, 2); // Model 2
What's On-Chain vs Off-Chain
| On-Chain | Off-Chain |
|---|---|
| Token staking & payouts | Signal data (predictions) |
| Signal commitment hashes | Aggregation engine |
| Tournament results (merkle roots) | Scoring computations |
| Payout claims & verification | Feature datasets |
| Round lifecycle management | Market data feeds |
Signal Commitment Scheme
When you submit a signal, a hash is committed on-chain before the round closes:
signalHash = keccak256(abi.encodePacked(
sortedPredictions,
contributorAddress
))
This proves:
- Priority — You submitted before the deadline
- Integrity — Your predictions weren't modified after submission
- Non-repudiation — You can't deny your submission
Payout Verification (Merkle Proofs)
Round results are published as a merkle root on-chain. Each contributor can independently verify their payout:
- SignalNet computes scores after 20 trading days
- A merkle tree is built:
leaf = keccak256(address, modelIndex, payoutAmount) - The merkle root is posted on-chain via
resolveRound() - Each contributor calls
claimReward()with their payout amount + merkle proof - Contract verifies the proof against the on-chain root
- Tokens transferred from StakeVault if valid
Verify, don't trust.
Source Code
All contracts are open source and verified:
- Repository: github.com/2manslkh/signal-mono
- Framework: Foundry (Solidity 0.8.24)
- Dependencies: OpenZeppelin Contracts v5
- Tests: 25 passing tests covering full lifecycle