Web3 Security 101Part 1 of 12SmartAlert5 min read

What a smart contract actually is: code, state, events and who can call what

A smart contract is code and storage at an address. How calls, events and state work, and what you can see in logs and what only by asking the contract.

*Part 1 of 12 in "Web3 Security 101". This series explains how contracts work, how they get broken and what you can watch. No prior knowledge of Solidity is needed.*

We write incident write-ups, and most of them start with the same two paragraphs of explanation. "The gateway is a proxy." "The threshold is a number in storage." This series puts that explanation in one place. Part 1 is the base: what lives at a contract address and what a monitor can and cannot see there.

Code and state at one address

A smart contract is a program stored on a blockchain at an address. It has two parts1:

  • Code. The compiled instructions, fixed when the contract is deployed. Nobody edits it afterwards. (Part 3 covers the case where the address stays and the code behind it still changes.)
  • State. The contract's own storage: balances, owner address, settings, counters. This is the part that changes.

Here is a small contract. It holds a fee setting and an owner:

solidity
contract Vault {
    address public owner;
    uint256 public feeBps;          // fee in basis points, 100 = 1%

    event FeeChanged(uint256 oldFee, uint256 newFee);

    constructor() { owner = msg.sender; }

    function setFee(uint256 newFee) external {
        require(msg.sender == owner, "not owner");
        emit FeeChanged(feeBps, newFee);
        feeBps = newFee;
    }
}

owner and feeBps are state. setFee is code that changes state. Everything else in this article is a variation on these three lines.

Calls: who can call what

Every action on a chain is a transaction signed by an account and sent to an address. When the transaction calls a function, the contract sees the caller as msg.sender2.

Two kinds of function matter:

  • Read functions (view) return data and change nothing. Anyone can call them for free, off-chain. feeBps() is one: Solidity generates a getter for every public variable.
  • Write functions change state. They need a transaction and gas, and they can only run if the contract lets the caller in.

"Lets the caller in" is code, written by the developer. In setFee it is one line: require(msg.sender == owner). If the check is missing, anyone can set the fee. If the check is there but owner is the wrong address, the wrong person can. That is the whole subject of Part 2.

A transaction can also revert. If a require fails, all of the call's changes are undone, and the transaction still lands in a block, marked failed. The caller pays gas. A failed attempt to call an admin function is still a visible fact: someone tried.

Events: the log a contract writes

State is stored inside the contract. Events are different. emit FeeChanged(...) writes an entry into the transaction's log3. Properties that matter for monitoring:

  • A contract cannot read events, only write them. They exist for the outside world: apps, explorers, monitors.
  • Events are cheap. Developers use them to announce important changes: Transfer on every token movement, OwnershipTransferred, Upgraded, Paused.
  • An event is only a promise by the developer. The compiler does not force setFee to emit FeeChanged. If the developer forgot the emit, the state changes and the log stays silent.

The reverse also holds: an event can be emitted without any real change, and a contract can emit a misleading one. Events are a convenient signal, not proof.

Events and state are two different questions

This is the idea the rest of the series keeps using.

Question
Event"What just happened?"
State"What is true right now?"
Where
EventTransaction logs
StateContract storage
How you see it
EventRead the logs of every block
StateCall a view function
Weak spot
EventNobody emitted it
StateNobody asked

Our Ronin write-up is the clean example. The bridge team upgraded the gateway, and the Upgraded event was expected and correct. Inside the contract, the number that decides how many operator votes a withdrawal needs became 0. No event announced that. Only a call to minimumVoteWeight() showed it, and the first withdrawal came 48 minutes later.

Part 10 goes deeper. For now, remember the split: events tell you about actions, calls tell you about results.

Native ETH has no events

One more gap. A token transfer emits Transfer, so you can watch it. Native ETH (or BNB) moving in and out of an address emits nothing. The only way to see it is to look at the balance. That matters for bridges and treasuries that hold the chain's own coin.

What the outside world can see

Put together, an observer of a contract has four sources:

  1. Events from the contract's logs.
  2. State through view calls.
  3. Balances of the address, in native coin and in tokens.
  4. Transactions themselves: who sent them, whether they succeeded, which function they targeted.

Public chains give you all four, for free, for every contract. What is missing is someone reading them at 3 a.m.

How to catch it

Which SmartAlert rules cover which source, as the product works today4:

  • Events. When you add a contract, SmartAlert reads its ABI and offers rule templates for the events it finds: OwnershipTransferred, Upgraded, Paused, RoleGranted, and others. See the recipes for ownership transfers, proxy upgrades and pauses.
  • State. A "Contract state" rule calls a view function on a schedule and fires on a condition, for example a paused() that returns true (recipe) or a number below a floor. This is the rule that would have caught the Ronin threshold. It is available on Pro and above.
  • Balances. A balance rule in dollars watches native coin as well as tokens (recipe).
  • Failed transactions. A rule on the share of failed transactions sees reverts, including a stranger's failed call to an admin function.

Two limits, stated plainly. First, a state change with no event and no matching view function cannot be watched by either kind of rule. If your contract changes a critical value silently, add a getter for it. Second, alerts are not instant: on Ethereum SmartAlert reads blocks with 12 confirmations, so the alert arrives about 2.5 to 4 minutes after the transaction.

Next, Part 2: Owners, roles and admin keys: where the door is.

Sources

  1. 1.ethereum.org, Introduction to smart contracts. ↩
  2. 2.Solidity documentation, Block and transaction properties (msg.sender). ↩
  3. 3.Solidity documentation, Events. ↩
  4. 4.SmartAlert, alert delay on Ethereum: 12 confirmations (about 144 s) plus polling and the rule check interval; see the Ronin write-up, note 8. ↩

Try it free

An address is enough. The first 14 days get Pro limits, and rules are checked on history before they go live.