Skip to main content

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).

ContractAddressExplorer
SignalNetToken0xdECC2df65B67e4b2e505a2816B334961AF16E773View on BaseScan
StakeVault0x225A784ec3C0178316aD0a083D2327CE1b0d04AbView on BaseScan
Tournament0x6CCB115056B87D87AEf27C4f02084F495f302f98View on BaseScan
info

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.

PropertyValue
NameSignalNet
SymbolSIGNAL
Decimals18
Max Supply1,000,000,000 SIGNAL
Initial Mint400,000,000 SIGNAL (treasury)
StandardERC-20 + ERC-2612 Permit

Key Features

  • Capped supplytotalMinted can never exceed MAX_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

ConstantValueDescription
MIN_STAKE100 SIGNALMinimum stake per submission
MAX_STAKE10,000 SIGNALMaximum stake per submission
MAX_MODELS_PER_USER3Up to 3 models per account per round
SLASH_BPS2500 (25%)Maximum loss per round
PAYOUT_MULTIPLIER_BPS2500 (25%)Score × Stake × 0.25

Round Lifecycle

Active → Closed → Resolving → Resolved
│ │
└──── Cancelled ◀──────────────┘ (emergency only)
  1. Active — Accepting submissions + stakes
  2. Closed — Submission window ended, signal merkle root committed
  3. Resolving — Oracle computing scores (20 trading days)
  4. Resolved — Results merkle root posted, claims open
  5. 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-ChainOff-Chain
Token staking & payoutsSignal data (predictions)
Signal commitment hashesAggregation engine
Tournament results (merkle roots)Scoring computations
Payout claims & verificationFeature datasets
Round lifecycle managementMarket 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:

  1. SignalNet computes scores after 20 trading days
  2. A merkle tree is built: leaf = keccak256(address, modelIndex, payoutAmount)
  3. The merkle root is posted on-chain via resolveRound()
  4. Each contributor calls claimReward() with their payout amount + merkle proof
  5. Contract verifies the proof against the on-chain root
  6. 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