skills/vulnerability-patterns/reentrancy/SKILL.md
Reentrancy attack patterns, real incidents, and defensive coding checks for Solidity protocols.
npx skillsauth add apegurus/solidity-argus reentrancyInstall 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.
Reentrancy occurs when an external call allows the callee to re-enter the calling function before state updates complete.
Types:
Loss: ~$60M (3.6M ETH) Root Cause: Classic reentrancy
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
// External call BEFORE state update
(bool success,) = msg.sender.call{value: _amount}("");
require(success);
// State update AFTER - never reached during reentrancy
balances[msg.sender] -= _amount;
}
contract Attacker {
DAO dao;
function attack() external payable {
dao.deposit{value: 1 ether}();
dao.withdraw(1 ether);
}
receive() external payable {
if (address(dao).balance >= 1 ether) {
dao.withdraw(1 ether); // Re-enter before balance update
}
}
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount; // State update FIRST
(bool success,) = msg.sender.call{value: _amount}("");
require(success);
}
Loss: ~$130M Root Cause: Cross-contract reentrancy via AMP token (ERC777)
// Cream's borrow function
function borrow(uint amount) external {
// Check collateral
require(getAccountLiquidity(msg.sender) >= amount);
// Transfer tokens (ERC777 calls receiver hook)
token.transfer(msg.sender, amount); // REENTRANCY HERE
// Update borrow balance
borrowBalances[msg.sender] += amount;
}
borrow() for AMP tokenfunction borrow(uint amount) external nonReentrant {
require(getAccountLiquidity(msg.sender) >= amount);
borrowBalances[msg.sender] += amount; // Update FIRST
token.transfer(msg.sender, amount);
}
Loss: ~$80M Root Cause: Read-only reentrancy
// Protocol A's view function
function getExchangeRate() public view returns (uint) {
return totalAssets / totalShares; // Reads state
}
// Protocol B uses this
function deposit() external {
uint rate = protocolA.getExchangeRate(); // Gets STALE rate
uint shares = amount * rate;
// ...
}
withdraw() on Protocol ALoss: ~$80M Root Cause: Cross-function reentrancy
function exitMarket(address cToken) external {
// Check no borrows
require(borrowBalances[msg.sender][cToken] == 0);
// Update membership
markets[msg.sender][cToken] = false;
}
function borrow(address cToken, uint amount) external {
require(markets[msg.sender][cToken] == true); // Must be in market
// Transfer - REENTRANCY POINT
cToken.transfer(msg.sender, amount);
borrowBalances[msg.sender][cToken] += amount;
}
borrow()exitMarket() - passes because borrow balance not yet updatednonReentrant modifier on vulnerable functions| Token Type | Reentrancy Vector |
|------------|-------------------|
| ETH | call{value:}() |
| ERC777 | tokensToSend, tokensReceived hooks |
| ERC721 | onERC721Received |
| ERC1155 | onERC1155Received, batch operations |
| Flash loans | Callback functions |
submodules/DeFiHackLabs/test/Reentrancy/submodules/learn-evm-attacks/test/Reentrancy/.call(), .send(), .transfer(), callback hook)nonReentrant modifier) on the function// Single-function reentrancy
function withdraw() external {
uint256 bal = balances[msg.sender];
// External call BEFORE state update
(bool success,) = msg.sender.call{value: bal}("");
require(success);
// State update AFTER external call — attacker reenters withdraw()
// and balances[msg.sender] is still the original value
balances[msg.sender] = 0;
}
// Hidden external calls that trigger callbacks:
// ERC721._safeMint() -> onERC721Received()
// ERC1155.safeTransferFrom() -> onERC1155Received()
// ERC777 token transfers -> tokensReceived() hook
.call(), .send(), .transfer(), token transfers, _safeMint(), _safeTransfer(), ERC777/ERC1155 safe transfersnonReentrant guard is present on the function — flag if absent_safeMint, _safeTransfer, ERC777 hooks, ERC1155 hooks — these are external calls even though they don't look like .call()nonReentrant modifier is applied to the functionview/pure and cannot modify stateReentrancyGuard with nonReentrant modifier to all functions that make external callsfunction withdraw() external nonReentrant {
uint256 bal = balances[msg.sender];
balances[msg.sender] = 0; // State update BEFORE external call
(bool success,) = msg.sender.call{value: bal}("");
require(success);
}
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.