skills/vulnerability-patterns/logic-errors/SKILL.md
Protocol logic bug patterns, exploit examples, and invariant-driven review strategies.
npx skillsauth add apegurus/solidity-argus logic-errorsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Logic bugs are flaws in business logic that allow unintended behavior. Unlike pattern-based vulnerabilities (reentrancy, overflow), these require understanding the protocol's intended behavior to identify.
Key insight: Logic bugs are the hardest to find automatically and cause the largest losses.
// VULNERABLE: Can withdraw multiple times
enum Status { Pending, Approved, Withdrawn }
mapping(uint256 => Status) public requestStatus;
function withdraw(uint256 requestId) external {
require(requestStatus[requestId] == Status.Approved, "Not approved");
// Missing: requestStatus[requestId] = Status.Withdrawn;
payable(msg.sender).transfer(amounts[requestId]);
}
// VULNERABLE: Update after external call
function redeem(uint256 shares) external {
uint256 assets = convertToAssets(shares);
asset.transfer(msg.sender, assets); // External call first
_burn(msg.sender, shares); // State update after
}
// SECURE: CEI pattern
function redeem(uint256 shares) external {
uint256 assets = convertToAssets(shares);
_burn(msg.sender, shares); // State update first
asset.transfer(msg.sender, assets); // External call after
}
// VULNERABLE: Doesn't account for existing balance
function deposit() external payable {
balances[msg.sender] = msg.value; // Overwrites, doesn't add!
}
// SECURE
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// VULNERABLE: Division before multiplication
function calculateReward(uint256 amount, uint256 rate) external pure returns (uint256) {
return amount / 1000 * rate; // Loses precision
}
// SECURE: Multiplication before division
function calculateReward(uint256 amount, uint256 rate) external pure returns (uint256) {
return amount * rate / 1000;
}
// VULNERABLE: Division by zero when totalSupply is 0
function pricePerShare() external view returns (uint256) {
return totalAssets() / totalSupply(); // Reverts if no shares
}
// SECURE
function pricePerShare() external view returns (uint256) {
uint256 supply = totalSupply();
return supply == 0 ? 1e18 : totalAssets() * 1e18 / supply;
}
What happened:
confirmAt mapping with zeroRoot cause: Invalid initialization treated as valid state
// The bug: messages[_messageHash] = 0 was treated as confirmed
function process(bytes memory _message) public returns (bool _success) {
bytes32 _messageHash = keccak256(_message);
require(acceptableRoot(messages[_messageHash]), "not accepted");
// 0 passed acceptableRoot check!
}
Lesson: Be explicit about valid states. Zero should not be a valid confirmed state.
What happened:
comptroller.compAccrued() returned wrong valuesRoot cause: Incorrect accounting in upgrade
Lesson: Test upgrades extensively. State transitions are dangerous.
What happened:
Root cause: Missing state update after reward claim
What happened:
Root cause: Incorrect share calculation with rounding
enum State { Created, Active, Completed, Cancelled }
modifier inState(State expected) {
require(state == expected, "Invalid state");
_;
}
function complete() external inState(State.Active) {
state = State.Completed; // Explicit transition
// Do completion logic
}
function _checkInvariants() internal view {
assert(totalAssets >= totalLiabilities);
assert(totalShares == 0 || totalAssets > 0);
// Add protocol-specific invariants
}
function deposit(uint256 assets) external {
// ... deposit logic ...
_checkInvariants();
}
function withdraw(uint256 amount) external {
// CHECKS
require(balances[msg.sender] >= amount, "Insufficient");
// EFFECTS
balances[msg.sender] -= amount;
// INTERACTIONS
payable(msg.sender).transfer(amount);
}
require() statements for input or state validationrequire condition is overly restrictive (rejects valid inputs) or too loose (accepts invalid inputs)require() validates return values from external contracts whose behavior doesn't match assumptions// Overly restrictive: blocks legitimate use case
function withdraw(uint256 amount) external {
// Fails if user has exactly the required amount (should be >=)
require(balances[msg.sender] > amount, "insufficient");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
// External contract assumption mismatch
function processPayment(IERC20 token, uint256 amount) external {
// Assumes transfer returns true, but some tokens don't return a value
// require reverts on tokens like USDT
require(token.transfer(msg.sender, amount), "failed");
}
// Missing error message
function setRate(uint256 rate) external {
require(rate > 0); // No error message — difficult to debug
}
require(), verify the condition matches the documented business logic (e.g., > vs >=, < vs <=)require() validates an external call's return value — verify the external contract actually returns what's expectedrequire() without error messages — while not a vulnerability, it makes debugging difficultrequire checks?require condition exactly matches the specificationrequire condition against the specification: > vs >=, < vs <=function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
// Solidity >=0.8.4: custom errors for gas efficiency
error InsufficientBalance(uint256 available, uint256 requested);
function withdrawV2(uint256 amount) external {
if (balances[msg.sender] < amount)
revert InsufficientBalance(balances[msg.sender], amount);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
testing
Specialist profile for mechanically applying the attack-vector deck and classifying vectors as skip, drop, or investigate.
tools
Specialist profile for libraries, helpers, base contracts, adapters, encoders, wrappers, and integration glue.
testing
Specialist profile for rounding, scale, decimal, downcast, and arithmetic accounting edge cases.
testing
Specialist profile for extracting conservation laws and state couplings, then searching for violating paths.