Smart Contracts
Security Model

Contract Security Model

What the Superhero contracts defend against by design: the guarantees they inherit from the language, the signature schemes they verify, the economic invariants of the bonding curve, and the gates around governance. The contracts have not undergone a third-party security audit.

Language-level guarantees

Sophia is a strongly typed functional language with no arbitrary mutable global state. Each contract has exactly one state value, replaced wholesale rather than mutated in place. Every FATE instruction is typed, and a type mismatch aborts the call and reverts every change. Together these remove the class of failure that dominates EVM incident reports: partially updated state surviving a failed call, and reentrancy against a half-written storage slot.

Cryptographic integrity

Delegated posting

post_without_tip_sig verifies an Ed25519 signature over a Blake2b digest with Crypto.verify_sig. The message is framed with the æternity signed-message prefix, the 28-byte constant #1a61657465726e697479205369676e6564204d6573736167653a0a20, which decodes to a length byte, aeternity Signed Message: with a newline, and a second length byte. That framing prevents a signature produced in another æternity protocol context from being replayed as a post. The post signature carries no nonce; relayers track which signed posts they have already submitted. See Tipping v3.

Identity linking

AddressLink takes an explicit nonce argument on every link and unlink, backed by a nonce_seq : map(address, int) in state and readable through get_nonce. A reused nonce aborts with INVALID_NONCE, so replay protection is on chain.

Writes are additionally gated on the provider owner (NOT_PROVIDER_OWNER). The user's signature proves consent to the link; the provider owner's key submits it. Superhero owns the providers it operates and submits every link on the user's behalf. AENS names, by contrast, are verifiable directly against the naming system. See AddressLink.

Economic security

  • Bonding curve solvency. The sell curve sits strictly below the buy curve at every supply level, so the AE reserve held in the sale contract always covers buying back the entire circulating supply. There is no bank-run state. The gap is the 0.5% sell factor (sell_return_percentage = 9950) plus the 0.5% affiliation fee added at the sale layer.
  • Fixed spread allocation. The 1% spread splits 0.5% to the community DAO and 0.5% to the affiliation system, with the split fixed in the contracts. On a sale, the contract retains exactly what it would owe if the same count were sold straight back, and only the remainder reaches the DAO.
  • Collection creation is burned, not collected. create_collection requires exactly 555 AE and spends it to ak_11111111111111111111111111111111273Yts, the burn address. Nobody profits from namespace creation.
  • Vote creation costs 555 AE. DAO.add_vote requires exactly 555 * 10^18 aettos, paid to the DAO treasury. Proposal spam has a real price, and the price accrues to the community.
  • Slippage is caller-controlled. sell(count, minimal_return_required) reverts with MINIMAL_RETURN_NOT_MEET below your floor, and buy reverts with AE_AMOUNT_NOT_SUFFICIENT if the price moved past what you attached. Set a floor on every live sell.
  • Overpayment is returned in full. buy computes Call.value - total_price and spends the whole difference back, with no dust floor and no retention, so a slippage buffer costs nothing when the price does not move.
  • Reward withdrawal requires real trading. An inviter can withdraw affiliation rewards only after four invitees (unique_invitee_count_threshold = 4) have each bought at least 10 AE worth (unique_invitee_buy_amount_threshold = 10 * 10 ^ 18).

Governance security

  • Quorum and supermajority. Both live in DAO.apply_vote_subject, and these are the exact comparisons:

    let in_favor_percentage = (vote_state[true] * 100) / (vote_state[true] + vote_state[false])
    require(in_favor_percentage >= 55, "GREATER_55_PERCENT_AGREEMENT_REQUIRED")
    
    let stake_percentage = (total_vote_stake * 100) / get_token().total_supply()
    require(stake_percentage >= 10, "GREATER_10_PERCENT_STAKE_REQUIRED")

    The supermajority denominator is votes cast, so abstaining does not oppose. The quorum denominator is total_supply() read at execution time. Both are truncating integer divisions compared with >=. Two further requires guard the divisions: NO_VOTE_STAKE and NO_TOKEN_SUPPLY.

  • Token locking. Tokens voted with are locked in the vote contract through the AEX-9 allowance mechanism, so the same tokens cannot be transferred, sold back to the curve, or voted in a concurrent proposal. revoke_vote (open votes only) and withdraw (closed votes only) are exact complements through is_closed(), so exactly one is callable at any height and locked tokens are never stuck.

  • Results are frozen before they are actionable. final_vote_state() returns option(vote_state) and yields None while a vote is open (a height gate, Chain.block_height >= close_height). DAO.apply_vote_subject aborts with VOTE_NOT_YET_FINAL on None, so a proposal cannot be executed on a favourable mid-vote tally.

  • Community DAO votes are immutable once closed. DAOVote.revoke_vote requires !is_closed(), so a closed tally cannot be altered afterwards.

  • Time-bounded execution. A passed vote must be applied within vote_timeout key blocks of its close height (960 blocks, roughly two days), or apply_vote_subject aborts with VOTE_RESULT_TIMEOUTED. Stale proposals cannot be resurrected when circumstances change, and the short window limits how far total_supply() can move between close and execution.

  • Single execution. The votes map stores an applied flag; a second call aborts with VOTE_ALREADY_APPLIED.

  • DAO migration safeguards. A ChangeDAO vote transfers ownership, beneficiary rights and the treasury balance. The target must implement is_dao() = true, which prevents a treasury being voted into a plain account or an unrelated contract, and it must reference the same community management contract (NEW_DAO_HAS_TO_REFERENCE_SAME_COMMUNITY_MANAGEMENT).

  • Minting is gated to known sales. mint_protocol_dao_token requires Set.member(Call.caller, state.known_token_sales) and aborts with NOT_KNOWN_TOKEN_SALE. Only sale contracts the factory itself deployed can mint protocol tokens. Community token supply is likewise owner-minted: the owner is the sale contract, so supply tracks the curve.

Related