Solidity Threat Modeling
Solidity threat modeling is the practice of naming what a contract protects, who can touch it, and what each person must not be able to do. The model lists the assets, the actors, and the forbidden action for each actor.
Users withdraw only their own funds
AssetVault records each deposit under the depositor. A stranger with no deposit cannot withdraw the funds of another person.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract AssetVault {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "too much");
balances[msg.sender] -= amount;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "send failed");
}
}
Only the owner changes the fee
FeeVault guards setFee with an owner check. The stranger call reverts and the owner call succeeds.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract FeeVault {
address public owner;
uint256 public feePercent;
constructor() {
owner = msg.sender;
}
function setFee(uint256 newFee) public {
require(msg.sender == owner, "not owner");
feePercent = newFee;
}
}
Only the minter creates new tokens
MintToken gives the mint power to one minter address. A stranger mint reverts and the supply stays at zero.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MintToken {
address public minter;
uint256 public totalSupply;
mapping(address => uint256) public balances;
constructor() {
minter = msg.sender;
}
function mint(address to, uint256 amount) public {
require(msg.sender == minter, "not minter");
totalSupply += amount;
balances[to] += amount;
}
}
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ThreatVault {
address public owner;
uint256 public feePercent;
mapping(address => uint256) public balances;
constructor() {
owner = msg.sender;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "too much");
balances[msg.sender] -= amount;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "send failed");
}
function setFee(uint256 newFee) public {
require(msg.sender == owner, "not owner");
feePercent = newFee;
}
}
Key points
- Assets: An asset is ether, a token balance, or an admin power the contract guards.
- Actors: An actor is the owner, a user, a stranger, or a validator ordering transactions.
- Boundaries: A boundary is a check that lets one actor act and makes another actor revert.
- Owner power: The owner alone may change settings like the fee.
- User funds: A user may withdraw only the funds recorded under their own address.
In this example, ThreatVault holds user balances, a fee percent, and an owner. deposit records ether under the caller. withdraw pays only from the caller's own recorded balance. setFee is owner only, so a stranger cannot change the fee and cannot drain another user's funds.
Note
A validator orders transactions in a block, so no critical rule may depend on exact transaction order alone.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A threat model lists ether, the owner, a user, and a stranger. What must the model state for the stranger?