Flash-loan exploit patterns — callback reentrancy, oracle amplification, governance attacks, unauthenticated callback handlers.
64
78%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
Fix and improve this skill with Tessl
tessl review fix ./packages/decepticon/decepticon/skills/standard/contracts/flash-loan/SKILL.mdFlash loans give the attacker uncollateralized capital for a single transaction. They're not vulnerabilities themselves — they're a force multiplier for existing bugs. Sources: Aave, Balancer, Uniswap V2/V3, Maker, dYdX (deprecated).
Use loan to push a price, then trigger price-dependent action.
Cross-reference: see oracle-manipulation/SKILL.md.
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator, // ← UNVALIDATED in many contracts
bytes calldata params
) external returns (bool) {
// Anyone can call this with fake `initiator`
// and have the contract do whatever the params say
}Bug: initiator and msg.sender == pool checks are missing.
Attack: call executeOperation directly with no actual loan, malicious params → contract does the operation anyway.
// Vulnerable governance:
function propose() external {
require(getVotes(msg.sender) > THRESHOLD);
// ...
}
function getVotes(address user) public view returns (uint256) {
return token.balanceOf(user); // ← reads SPOT balance, not snapshot
}Attack: flash-loan governance tokens, propose malicious change (e.g., upgrade contract to drain), vote with the loaned tokens, execute, repay loan. MakerDAO had this pattern; mitigated by checkpoint-based voting.
Pool that uses totalSupply() or balanceOf(pool) for share math:
function deposit(uint256 amt) external {
uint256 shares = (amt * totalSupply()) / underlying.balanceOf(address(this));
_mint(msg.sender, shares);
underlying.transferFrom(msg.sender, address(this), amt);
}Attacker:
underlying.balanceOf artificiallyamt * totalSupply / inflatedBalance ≈ 0reentrancy/ overlap).A liquidator that uses flash loans to repay debt before claiming collateral — usually benign. The bug: liquidation health check happens before the seizure, but the seizure happens via callback into a manipulable function. Reentrancy + oracle dependent.
grep -rn 'executeOperation\|flashLoan\|onFlashLoan\|flashCallback\|aaveFlashLoan' src/Check:
msg.sender == known_pool (whitelist callback origin)initiator == address(this) (only respond to your own loans)amounts[i] + premiums[i] is repaid)See oracle-manipulation/SKILL.md.
Check ERC4626 virtual-shares mitigation:
// OpenZeppelin ERC4626 v4.7+
function _convertToShares(uint256 assets, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256) {
return _initialConvertToShares(assets, rounding); // adds 10**18 to total supply for share calc
}If using legacy ERC4626 or custom vault math, write a Foundry test that:
foundry_inflation_test(vault_address="...", underlying="...", target_path="src/Vault.sol")| Pattern | Severity if confirmed |
|---|---|
| Unauth callback handler → arbitrary execution | Critical 10.0 |
| Flash-loan oracle manipulation drains protocol | Critical 9-10 |
| Governance attack possible | Critical 9-10 (if exec results in fund loss) |
| ERC4626 inflation attack on live vault | Critical 9 |
| Liquidation reentrancy via callback | High 8 |
| Theoretical (low liquidity, manipulation cost > attack profit) | Medium-Low |
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "@aave-v3/interfaces/IPool.sol";
contract Test_FlashAttack is Test {
IPool constant AAVE = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
address constant TARGET = 0x...;
address constant ATTACKER = address(0xBEEF);
function setUp() public {
vm.createSelectFork("https://eth-mainnet.../<block>");
}
function test_drain() public {
vm.startPrank(ATTACKER);
uint256 beforeBal = WETH.balanceOf(ATTACKER);
// Request 10000 WETH flash loan
address[] memory assets = new address[](1);
assets[0] = address(WETH);
uint256[] memory amts = new uint256[](1);
amts[0] = 10000 ether;
uint256[] memory modes = new uint256[](1); // 0 = repay full
AAVE.flashLoan(address(this), assets, amts, modes, address(this), "", 0);
uint256 afterBal = WETH.balanceOf(ATTACKER);
assertGt(afterBal, beforeBal + 100 ether, "should profit");
}
function executeOperation(
address[] calldata assets,
uint256[] calldata amts,
uint256[] calldata premiums,
address initiator,
bytes calldata
) external returns (bool) {
// 1. Use loaned funds to manipulate TARGET
// 2. Profit
// 3. Repay
IERC20(assets[0]).approve(address(AAVE), amts[0] + premiums[0]);
return true;
}
}Flash-loan attacks leave a single transaction trail visible to:
For audit-PoC purposes (not live exploit) just use a forked anvil.
require(msg.sender == knownPool && initiator == address(this))0cf691e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.