From 14622b5d5533810d3671ba68f5a6b82a2b27eb79 Mon Sep 17 00:00:00 2001 From: jtardioli Date: Sun, 9 Mar 2025 13:51:39 -0300 Subject: [PATCH 01/11] fix FuzzThis intro typo --- Intros/1_FuzzThis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Intros/1_FuzzThis.md b/Intros/1_FuzzThis.md index e7836e5..d8d8a81 100644 --- a/Intros/1_FuzzThis.md +++ b/Intros/1_FuzzThis.md @@ -1,6 +1,6 @@ # FuzzThis To win this challenge, you need `FuzzThis::dontHackMePlease()` to return `win`. -In very other challenge you want the test case to pass to win, but for this challenge you want the test case to fail. +In every other challenge you want the test case to pass to win, but for this challenge you want the test case to fail. This challenge is meant to help you use basic fuzz testing in foundry. If you are unfamiliar with Fuzz testing in Foundry, check out their [docs](https://book.getfoundry.sh/forge/fuzz-testing). This should be the easiest challenge of the series so with that, good luck, and don't forget to read the writeup after! From e8307980ebbe0b5578414c699c3bab57d88de5cf Mon Sep 17 00:00:00 2001 From: jtardioli Date: Thu, 19 Jun 2025 11:51:41 -0400 Subject: [PATCH 02/11] fix: double the --- write_ups/3_IKnowYulNeverHackThis.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/write_ups/3_IKnowYulNeverHackThis.md b/write_ups/3_IKnowYulNeverHackThis.md index 40a91dc..a4e0938 100644 --- a/write_ups/3_IKnowYulNeverHackThis.md +++ b/write_ups/3_IKnowYulNeverHackThis.md @@ -1,5 +1,7 @@ # IKnowYulNeverHackThis Solution + Hopefully you picked the winning team in one try. The correct way to steal all of the funds was to join the read team. Here is the solution: + ``` function test_GetThisPassing_3() public { address hacker = address(0xBAD); @@ -14,7 +16,9 @@ function test_GetThisPassing_3() public { assertEq(hacker.balance, 10 ether); } ``` + To win you simply need to: + 1. Join the Red Team. 2. Call `defineWinners()` with the parameter set to false. @@ -22,6 +26,7 @@ To win you simply need to: Winning this challenge is fairly easy. However, understanding how you won is far more important. To get a grasp on how joining the red team allows you to win this challenge, lets start by viewing what happens when you join the blue team. Here is the code from that section with comments explaining each lines utility: + ``` // creates an empty address array in memory with a size of one address[] memory winners = new address[](1); @@ -43,16 +48,16 @@ if (_isBlueTeam) { // loads the free memory pointer let freeMem := mload(0x40) - // gets the memory location of the the word after the free memory pointer + // gets the memory location of the word after the free memory pointer let newMsize := add( freeMem, 0x20 ) - /** + /** if the free memory pointer is not equal to the next available memory location after the array, we need to move each memory variable one word further. This will prevent us from overwriting an existing variable if they exist. (In this scenario they do not exist, but I needed to add more code to throw you off :) */ if iszero( eq( freeMem, nextMemoryLocation) ){ let currVal let prevVal - + // loop through the variables that need to be rewritten for { let i := nextMemoryLocation } lt(i, newMsize) { i := add(i, 0x20) } { // get the current variables value from memory @@ -61,7 +66,7 @@ if (_isBlueTeam) { mstore(i, prevVal) // save the current value to the stack prevVal := currVal - + } } @@ -78,18 +83,19 @@ if (_isBlueTeam) { mstore(0x40, newMsize ) } } +} ``` We now know the proper way to update the winnings array. Although, admittedly, there are far more efficient ways to do so for this particular function. The issue is in the code of the else statement (the flow if the red team is the winning team) stems from missing the following line of code: + ``` mstore( location, length ) ``` + Although we do update the `length` variable, we only update it on the stack, and forget to store it in memory. All of the addresses were written to memory, but because the length never gets updated it keeps being over written by the next address. We were the last address in the red team array so our address is written to the `winnings` array at the end of the for loop. Since the assembly block forgets to update the length of the array, the size is still one. This causes the calculation for the share of winnings to assign the entire balance to `shareOfPrize`. This sends the entirety of the winnings to our address. - - Bonus Points if you noticed that the game cannot function properly after the first time. From 82d958d0907987fa9f82b50ed68f04e87e5ce201 Mon Sep 17 00:00:00 2001 From: jtardioli Date: Thu, 19 Jun 2025 11:52:09 -0400 Subject: [PATCH 03/11] fix: unused local variable --- src/3_IKnowYulNeverHackThis.sol | 74 +++++++++++++++------------------ 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/src/3_IKnowYulNeverHackThis.sol b/src/3_IKnowYulNeverHackThis.sol index c14d542..cc80592 100644 --- a/src/3_IKnowYulNeverHackThis.sol +++ b/src/3_IKnowYulNeverHackThis.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; - contract IKnowYulNeverHackThis { - address[] public redTeam; address[] public blueTeam; bool locked; uint256 constant minAmountOfPlayers = 10; uint256 public playersCount; mapping(address => bool) public isPlaying; - enum Team { Red, Blue } - + enum Team { + Red, + Blue + } function joinRedTeam() external payable { require(msg.value == 1 ether, "This game ain't free"); @@ -21,19 +21,19 @@ contract IKnowYulNeverHackThis { redTeam.push(msg.sender); isPlaying[msg.sender] = true; - playersCount++; + playersCount++; } function joinBlueTeam() external payable { require(msg.value == 1 ether, "This game ain't free"); require(!isPlaying[msg.sender], "Start playing already!"); - blueTeam.push(msg.sender); + blueTeam.push(msg.sender); isPlaying[msg.sender] = true; - playersCount++; + playersCount++; } - + function defineWinners(bool _isBlueTeam) external { require(locked == false, "This isn't a reentrancey challenge"); require(playersCount >= minAmountOfPlayers, "Game isn't over yet"); @@ -43,74 +43,67 @@ contract IKnowYulNeverHackThis { address[] memory winners = new address[](1); if (_isBlueTeam) { - for(uint256 j; j < blueTeam.length; ++j) { + for (uint256 j; j < blueTeam.length; ++j) { address winner = blueTeam[j]; assembly { - let location := winners - + let length := mload(winners) - - let nextMemoryLocation := add( location, mul( length, 0x20 ) ) + + let nextMemoryLocation := add(location, mul(length, 0x20)) let freeMem := mload(0x40) - let newMsize := add( freeMem, 0x20 ) + let newMsize := add(freeMem, 0x20) - if iszero( eq( freeMem, nextMemoryLocation) ){ + if iszero(eq(freeMem, nextMemoryLocation)) { let currVal let prevVal - + for { let i := nextMemoryLocation } lt(i, newMsize) { i := add(i, 0x20) } { - currVal := mload(i) mstore(i, prevVal) prevVal := currVal - } } mstore(nextMemoryLocation, winner) - - length := add( length, 1 ) - - mstore( location, length ) - mstore(0x40, newMsize ) + length := add(length, 1) + + mstore(location, length) + + mstore(0x40, newMsize) } } - } else { - for(uint256 j; j < redTeam.length; ++j) { + for (uint256 j; j < redTeam.length; ++j) { address winner = redTeam[j]; assembly { - let location := winners - + let length := mload(winners) - - let nextMemoryLocation := add( location, mul( length, 0x20 ) ) + + let nextMemoryLocation := add(location, mul(length, 0x20)) let freeMem := mload(0x40) - let newMsize := add( freeMem, 0x20 ) + let newMsize := add(freeMem, 0x20) - if iszero( eq( freeMem, nextMemoryLocation) ){ + if iszero(eq(freeMem, nextMemoryLocation)) { let currVal let prevVal - + for { let i := nextMemoryLocation } lt(i, newMsize) { i := add(i, 0x20) } { - currVal := mload(i) mstore(i, prevVal) prevVal := currVal - } } - + mstore(nextMemoryLocation, winner) - - length := add( length, 1 ) - mstore(0x40, newMsize ) + length := add(length, 1) + + mstore(0x40, newMsize) } } } @@ -118,11 +111,10 @@ contract IKnowYulNeverHackThis { uint256 shareOfPrize = address(this).balance / winners.length; uint256 i; - uint256 winnersLength = winners.length; - for(i; i < winners.length; ++i) { + for (i; i < winners.length; ++i) { (bool sent, bytes memory data) = winners[i].call{value: shareOfPrize}(""); } locked = false; } -} \ No newline at end of file +} From 3a22711156f60eee77605ca7e7fa8db8b79e762c Mon Sep 17 00:00:00 2001 From: jtardioli Date: Thu, 19 Jun 2025 11:54:17 -0400 Subject: [PATCH 04/11] fix: formatting --- src/3_IKnowYulNeverHackThis.sol | 73 +++++++++++++++------------- write_ups/3_IKnowYulNeverHackThis.md | 18 +++---- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/src/3_IKnowYulNeverHackThis.sol b/src/3_IKnowYulNeverHackThis.sol index cc80592..f572341 100644 --- a/src/3_IKnowYulNeverHackThis.sol +++ b/src/3_IKnowYulNeverHackThis.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; + contract IKnowYulNeverHackThis { + address[] public redTeam; address[] public blueTeam; bool locked; uint256 constant minAmountOfPlayers = 10; uint256 public playersCount; mapping(address => bool) public isPlaying; + enum Team { Red, Blue } + - enum Team { - Red, - Blue - } function joinRedTeam() external payable { require(msg.value == 1 ether, "This game ain't free"); @@ -21,19 +21,19 @@ contract IKnowYulNeverHackThis { redTeam.push(msg.sender); isPlaying[msg.sender] = true; - playersCount++; + playersCount++; } function joinBlueTeam() external payable { require(msg.value == 1 ether, "This game ain't free"); require(!isPlaying[msg.sender], "Start playing already!"); - blueTeam.push(msg.sender); + blueTeam.push(msg.sender); isPlaying[msg.sender] = true; - playersCount++; + playersCount++; } - + function defineWinners(bool _isBlueTeam) external { require(locked == false, "This isn't a reentrancey challenge"); require(playersCount >= minAmountOfPlayers, "Game isn't over yet"); @@ -43,67 +43,74 @@ contract IKnowYulNeverHackThis { address[] memory winners = new address[](1); if (_isBlueTeam) { - for (uint256 j; j < blueTeam.length; ++j) { + for(uint256 j; j < blueTeam.length; ++j) { address winner = blueTeam[j]; assembly { - let location := winners + let location := winners + let length := mload(winners) - - let nextMemoryLocation := add(location, mul(length, 0x20)) + + let nextMemoryLocation := add( location, mul( length, 0x20 ) ) let freeMem := mload(0x40) - let newMsize := add(freeMem, 0x20) + let newMsize := add( freeMem, 0x20 ) - if iszero(eq(freeMem, nextMemoryLocation)) { + if iszero( eq( freeMem, nextMemoryLocation) ){ let currVal let prevVal - + for { let i := nextMemoryLocation } lt(i, newMsize) { i := add(i, 0x20) } { + currVal := mload(i) mstore(i, prevVal) prevVal := currVal + } } mstore(nextMemoryLocation, winner) + + length := add( length, 1 ) + + mstore( location, length ) - length := add(length, 1) - - mstore(location, length) - - mstore(0x40, newMsize) + mstore(0x40, newMsize ) } } + } else { - for (uint256 j; j < redTeam.length; ++j) { + for(uint256 j; j < redTeam.length; ++j) { address winner = redTeam[j]; assembly { - let location := winners + let location := winners + let length := mload(winners) - - let nextMemoryLocation := add(location, mul(length, 0x20)) + + let nextMemoryLocation := add( location, mul( length, 0x20 ) ) let freeMem := mload(0x40) - let newMsize := add(freeMem, 0x20) + let newMsize := add( freeMem, 0x20 ) - if iszero(eq(freeMem, nextMemoryLocation)) { + if iszero( eq( freeMem, nextMemoryLocation) ){ let currVal let prevVal - + for { let i := nextMemoryLocation } lt(i, newMsize) { i := add(i, 0x20) } { + currVal := mload(i) mstore(i, prevVal) prevVal := currVal + } } - + mstore(nextMemoryLocation, winner) + + length := add( length, 1 ) - length := add(length, 1) - - mstore(0x40, newMsize) + mstore(0x40, newMsize ) } } } @@ -111,10 +118,10 @@ contract IKnowYulNeverHackThis { uint256 shareOfPrize = address(this).balance / winners.length; uint256 i; - for (i; i < winners.length; ++i) { + for(i; i < winners.length; ++i) { (bool sent, bytes memory data) = winners[i].call{value: shareOfPrize}(""); } locked = false; } -} +} \ No newline at end of file diff --git a/write_ups/3_IKnowYulNeverHackThis.md b/write_ups/3_IKnowYulNeverHackThis.md index a4e0938..7c0016d 100644 --- a/write_ups/3_IKnowYulNeverHackThis.md +++ b/write_ups/3_IKnowYulNeverHackThis.md @@ -1,7 +1,5 @@ # IKnowYulNeverHackThis Solution - Hopefully you picked the winning team in one try. The correct way to steal all of the funds was to join the read team. Here is the solution: - ``` function test_GetThisPassing_3() public { address hacker = address(0xBAD); @@ -16,9 +14,7 @@ function test_GetThisPassing_3() public { assertEq(hacker.balance, 10 ether); } ``` - To win you simply need to: - 1. Join the Red Team. 2. Call `defineWinners()` with the parameter set to false. @@ -26,7 +22,6 @@ To win you simply need to: Winning this challenge is fairly easy. However, understanding how you won is far more important. To get a grasp on how joining the red team allows you to win this challenge, lets start by viewing what happens when you join the blue team. Here is the code from that section with comments explaining each lines utility: - ``` // creates an empty address array in memory with a size of one address[] memory winners = new address[](1); @@ -51,13 +46,13 @@ if (_isBlueTeam) { // gets the memory location of the word after the free memory pointer let newMsize := add( freeMem, 0x20 ) - /** + /** if the free memory pointer is not equal to the next available memory location after the array, we need to move each memory variable one word further. This will prevent us from overwriting an existing variable if they exist. (In this scenario they do not exist, but I needed to add more code to throw you off :) */ if iszero( eq( freeMem, nextMemoryLocation) ){ let currVal let prevVal - + // loop through the variables that need to be rewritten for { let i := nextMemoryLocation } lt(i, newMsize) { i := add(i, 0x20) } { // get the current variables value from memory @@ -66,7 +61,7 @@ if (_isBlueTeam) { mstore(i, prevVal) // save the current value to the stack prevVal := currVal - + } } @@ -83,19 +78,18 @@ if (_isBlueTeam) { mstore(0x40, newMsize ) } } -} ``` We now know the proper way to update the winnings array. Although, admittedly, there are far more efficient ways to do so for this particular function. The issue is in the code of the else statement (the flow if the red team is the winning team) stems from missing the following line of code: - ``` mstore( location, length ) ``` - Although we do update the `length` variable, we only update it on the stack, and forget to store it in memory. All of the addresses were written to memory, but because the length never gets updated it keeps being over written by the next address. We were the last address in the red team array so our address is written to the `winnings` array at the end of the for loop. Since the assembly block forgets to update the length of the array, the size is still one. This causes the calculation for the share of winnings to assign the entire balance to `shareOfPrize`. This sends the entirety of the winnings to our address. -Bonus Points if you noticed that the game cannot function properly after the first time. + + +Bonus Points if you noticed that the game cannot function properly after the first time. \ No newline at end of file From 57d1636a6dc61c1f2b4fe71a18f3a9e0c6c300c5 Mon Sep 17 00:00:00 2001 From: jtardioli Date: Thu, 19 Jun 2025 11:55:36 -0400 Subject: [PATCH 05/11] style: formatting again --- write_ups/3_IKnowYulNeverHackThis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/write_ups/3_IKnowYulNeverHackThis.md b/write_ups/3_IKnowYulNeverHackThis.md index 7c0016d..809ea11 100644 --- a/write_ups/3_IKnowYulNeverHackThis.md +++ b/write_ups/3_IKnowYulNeverHackThis.md @@ -92,4 +92,4 @@ Since the assembly block forgets to update the length of the array, the size is -Bonus Points if you noticed that the game cannot function properly after the first time. \ No newline at end of file +- Bonus Points if you noticed that the game cannot function properly after the first time. \ No newline at end of file From e248447868a0b9984cdcc678d50f506a5a9cd262 Mon Sep 17 00:00:00 2001 From: jtardioli Date: Thu, 19 Jun 2025 11:56:33 -0400 Subject: [PATCH 06/11] styles: formatting --- write_ups/3_IKnowYulNeverHackThis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/write_ups/3_IKnowYulNeverHackThis.md b/write_ups/3_IKnowYulNeverHackThis.md index 809ea11..7c0016d 100644 --- a/write_ups/3_IKnowYulNeverHackThis.md +++ b/write_ups/3_IKnowYulNeverHackThis.md @@ -92,4 +92,4 @@ Since the assembly block forgets to update the length of the array, the size is -- Bonus Points if you noticed that the game cannot function properly after the first time. \ No newline at end of file +Bonus Points if you noticed that the game cannot function properly after the first time. \ No newline at end of file From 90c767bb7869d52eb74caa8a98a542bd130fc7ff Mon Sep 17 00:00:00 2001 From: jtardioli Date: Thu, 19 Jun 2025 16:31:12 -0400 Subject: [PATCH 07/11] fix: update function to pure --- src/5_BuyMyTokens.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/5_BuyMyTokens.sol b/src/5_BuyMyTokens.sol index 49f7325..fd69bab 100644 --- a/src/5_BuyMyTokens.sol +++ b/src/5_BuyMyTokens.sol @@ -24,7 +24,7 @@ contract BuyMyTokens { tokenPrices[token3] = 0.3 ether; } - function _checkPurchasingPower(uint256 _amountOfEtherAvailable, uint256 _amount, uint256 _price) internal { + function _checkPurchasingPower(uint256 _amountOfEtherAvailable, uint256 _amount, uint256 _price) internal pure { uint256 cost = _price * _amount; require(cost >= _amountOfEtherAvailable, "dont be that guy; pay for your tokens"); } From 3421e2521af6a86ecf1c46faddfcd6113c75e990 Mon Sep 17 00:00:00 2001 From: jtardioli Date: Fri, 20 Jun 2025 11:21:08 -0400 Subject: [PATCH 08/11] fix: typos --- write_ups/6_NotForTrusting.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/write_ups/6_NotForTrusting.md b/write_ups/6_NotForTrusting.md index 8ee2253..9b62527 100644 --- a/write_ups/6_NotForTrusting.md +++ b/write_ups/6_NotForTrusting.md @@ -12,9 +12,9 @@ The actual vulnerability is a cross contract reentrancy attack. Nowadays, most d Since the two smart contracts do not share the same state for the reentrancy guard modifier, they are not aware when the other contract is locked. To fix this issue, I recommend that protocols with multiple smart contracts should share the state of the reentrancy guard modifier by creating a separate smart contract that they both read and write to the state of the contract. -The basics of the rentrancey revolves around claiming twice, and taking advantage of an NFT's `safeTransforFrom()` function call to `onERC721Received()` in the receiving smart contract. We will go over the affected code in more depth later, but for now let's look into how we perform the exploit. +The basics of the reentrancy revolves around claiming twice, and taking advantage of an NFT's `safeTransferFrom()` function call to `onERC721Received()` in the receiving smart contract. We will go over the affected code in more depth later, but for now let's look into how we perform the exploit. -To exploit the vulnerability, we need a separate smart contract to perform the rentrancey. Let's take a look at my implementation of it: +To exploit the vulnerability, we need a separate smart contract to perform the reentrancy. Let's take a look at my implementation of it: ``` // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; @@ -118,7 +118,7 @@ function unstake(uint256 _tokenId, bool claim) external guard { rewarder.claim(msg.sender); } - // we update the state before transfering the NFT out of the protocol + // we update the state before transferring the NFT out of the protocol isStaking[msg.sender] = false; userStake = StakeData(0, 0, 0); From e8831a928bcbc44113df54e7553e6c4ccec04552 Mon Sep 17 00:00:00 2001 From: jtardioli Date: Fri, 20 Jun 2025 14:25:20 -0400 Subject: [PATCH 09/11] fix: typo --- src/7_CantStopMe.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/7_CantStopMe.sol b/src/7_CantStopMe.sol index 2e3be89..5ef1e61 100644 --- a/src/7_CantStopMe.sol +++ b/src/7_CantStopMe.sol @@ -143,7 +143,7 @@ contract TrustyOracle { auction.updatePriceDifferential(minPrice, maxPrice); } - // adds current price to + // adds current price to the price history function addPriceToHistory() external { require(msg.sender == address(auction)); uint256 lastPrice = auction.lastPrice(); From 38310ce20ebb9986e606e9cd1023a2ab7f95a86d Mon Sep 17 00:00:00 2001 From: jtardioli Date: Fri, 20 Jun 2025 15:29:34 -0400 Subject: [PATCH 10/11] fix: typos in file name --- Intros/{8_TransientToruble.md => 8_TransientTrouble.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Intros/{8_TransientToruble.md => 8_TransientTrouble.md} (100%) diff --git a/Intros/8_TransientToruble.md b/Intros/8_TransientTrouble.md similarity index 100% rename from Intros/8_TransientToruble.md rename to Intros/8_TransientTrouble.md From cdabb7fc24a92e81d4f668295746bbed16b1b4d1 Mon Sep 17 00:00:00 2001 From: jtardioli Date: Fri, 20 Jun 2025 15:36:56 -0400 Subject: [PATCH 11/11] fix: add missing staking validation --- src/6_NotForTrusting.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/6_NotForTrusting.sol b/src/6_NotForTrusting.sol index 9bc1d59..bd737d0 100644 --- a/src/6_NotForTrusting.sol +++ b/src/6_NotForTrusting.sol @@ -114,6 +114,7 @@ contract Rewarder is IStaking{ function claim(address _for) external guard { bool isStaking = staking.isStaking(_for); StakeData memory userStake = staking.getStakerData(msg.sender); + require(isStaking, "gotta stake to make money"); require(msg.sender == address(staking), "not your rewards"); require(block.number >= userStake.stakeStart + userStake.stakeDuration, "you know about vm.roll(), right?"); require(!hasClaimed[msg.sender], "no double dipping");