diff --git a/client/src/gamedata/authors.json b/client/src/gamedata/authors.json index 2aa7533cc..7afb30561 100644 --- a/client/src/gamedata/authors.json +++ b/client/src/gamedata/authors.json @@ -182,6 +182,16 @@ "https://www.linkedin.com/in/afonso-dalvi-711635112/" ], "emails": ["gwdeps@gmail.com","afonsodalvia@gmail.com"] + }, + "eduardowestc": { + "name": [ + "Eduardo W. da Cunha" + ], + "emails": [], + "websites": [ + "https://www.linkedin.com/in/eduardo-westphal-da-cunha/" + ], + "donate": "0x8FcB647096a5A5B0Ff6B944C5B2f3d2c9e1f65A0" + } } - } } diff --git a/client/src/gamedata/en/descriptions/levels/crowdfunding.md b/client/src/gamedata/en/descriptions/levels/crowdfunding.md new file mode 100644 index 000000000..fc319aa99 --- /dev/null +++ b/client/src/gamedata/en/descriptions/levels/crowdfunding.md @@ -0,0 +1,7 @@ +A famous artist is organizing a crowdfund for his/her next big project. + +Your goal is to drain all the funds from the crowdfund. + +  +Things that might help +* Look into security issues from Solidity's `ecrecover` built-in function. diff --git a/client/src/gamedata/en/descriptions/levels/crowdfunding_complete.md b/client/src/gamedata/en/descriptions/levels/crowdfunding_complete.md new file mode 100644 index 000000000..d5a9dcafc --- /dev/null +++ b/client/src/gamedata/en/descriptions/levels/crowdfunding_complete.md @@ -0,0 +1,3 @@ +Well done! + +Signature malleability is a big issue when recovering addresses. There is actually a simple way to avoid this problem, as it is done in OpenZeppelin's ECDSA library. Check it out! \ No newline at end of file diff --git a/client/src/gamedata/gamedata.json b/client/src/gamedata/gamedata.json index ea6885cb1..897b1959b 100644 --- a/client/src/gamedata/gamedata.json +++ b/client/src/gamedata/gamedata.json @@ -496,6 +496,21 @@ "deployId": "31", "instanceGas": 750000, "author": "Waiandt&Dalvi" - } - ] -} + }, + { + "name": "Crowdfunding", + "created": "2024-04-05", + "difficulty": "7", + "description": "crowdfunding.md", + "completedDescription": "crowdfunding_complete.md", + "levelContract": "CrowdfundingFactory.sol", + "instanceContract": "Crowdfunding.sol", + "revealCode": true, + "deployParams": [], + "deployFunds": 1, + "deployId": "30", + "instanceGas": 1000000, + "author": "eduardowestc" + } + ] +} \ No newline at end of file diff --git a/contracts/src/levels/Crowdfunding.sol b/contracts/src/levels/Crowdfunding.sol new file mode 100644 index 000000000..bf6c285a0 --- /dev/null +++ b/contracts/src/levels/Crowdfunding.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract Crowdfunding { + address public owner; + address public artist; + string public projectName; + bytes public lastSignature; + + receive() external payable {} + + constructor(address owner_, string memory newProjectName) { + owner = owner_; + projectName = newProjectName; + } + + function withdraw() external { + require(msg.sender == artist, "Not artist"); + + (bool success, ) = artist.call{value: address(this).balance}(""); + + require(success, "Withdraw failed"); + } + + function setArtist(address newArtist, bytes calldata signature) external { + require( + keccak256(signature) != keccak256(lastSignature), + "already used signature" + ); + bytes32 nameHash = keccak256(abi.encodePacked(projectName)); + bytes32 messageHash = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", nameHash) + ); + (bytes32 r, bytes32 s, uint8 v) = splitSignature(signature); + require(owner == ecrecover(messageHash, v, r, s)); + + artist = newArtist; + lastSignature = signature; + } + + function splitSignature( + bytes memory signature + ) public pure returns (bytes32 r, bytes32 s, uint8 v) { + if (signature.length == 65) { + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + } + } +} diff --git a/contracts/src/levels/CrowdfundingFactory.sol b/contracts/src/levels/CrowdfundingFactory.sol new file mode 100644 index 000000000..591c4e9f7 --- /dev/null +++ b/contracts/src/levels/CrowdfundingFactory.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {Level} from "./base/Level.sol"; +import {Crowdfunding} from "./Crowdfunding.sol"; + +contract CrowdfundingFactory is Level { + bytes public signature = + hex"7986fd095b20021de58a0c43a03a9f18204bc4f0e05d2624cb539174e73e0a4c048155b5a878c337217db7244af6b3930a6ee90ffba2d488f3bce6f7258ca2251c"; + address public signer = 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266; + address public artist = 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7; + + function createInstance( + address _player + ) public payable override returns (address) { + _player; + + string memory projectName = "amazing crowdfunding"; + Crowdfunding crowdfunding = new Crowdfunding(signer, projectName); + + (bool success, ) = address(crowdfunding).call{value: 1 ether}(""); + require(success, "Failed to transfer funds"); + crowdfunding.setArtist(artist, signature); + + return address(crowdfunding); + } + + function validateInstance( + address payable _instance, + address _player + ) public view override returns (bool) { + Crowdfunding crowdfunding = Crowdfunding(_instance); + + return crowdfunding.artist() == _player && _instance.balance == 0; + } +} diff --git a/contracts/test/levels/Crowdfunding.t.sol b/contracts/test/levels/Crowdfunding.t.sol new file mode 100644 index 000000000..9fc19f169 --- /dev/null +++ b/contracts/test/levels/Crowdfunding.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test, console} from "forge-std/Test.sol"; +import {Utils} from "test/utils/Utils.sol"; + +import {Crowdfunding} from "src/levels/Crowdfunding.sol"; +import {CrowdfundingFactory} from "src/levels/CrowdfundingFactory.sol"; +import {Level} from "src/levels/base/Level.sol"; +import {Ethernaut} from "src/Ethernaut.sol"; + +contract TestCrowdfunding is Test, Utils { + Ethernaut ethernaut; + Crowdfunding instance; + + address payable owner; + address payable player; + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + function setUp() public { + address payable[] memory users = createUsers(3); + + address initialArtist = users[0]; + vm.label(initialArtist, "Initial Artist"); + + player = users[1]; + vm.label(player, "Player"); + + owner = users[2]; + vm.label(owner, "Owner"); + + vm.startPrank(owner); + ethernaut = getEthernautWithStatsProxy(owner); + CrowdfundingFactory factory = new CrowdfundingFactory(); + ethernaut.registerLevel(Level(address(factory))); + vm.stopPrank(); + + vm.startPrank(player); + uint256 gasStart = gasleft(); + instance = Crowdfunding( + payable( + createLevelInstance(ethernaut, Level(address(factory)), 1 ether) + ) + ); + uint256 gasEnd = gasleft(); + console.log("Gas to create instance:", gasStart - gasEnd); + vm.stopPrank(); + } + + function _splitSignature( + bytes memory signature + ) internal pure virtual returns (bytes32 r, bytes32 s, uint8 v) { + if (signature.length == 65) { + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + } + } + + /*////////////////////////////////////////////////////////////// + TESTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Check the intial state of the level and enviroment. + function testInit() public { + vm.prank(player); + assertFalse(submitLevelInstance(ethernaut, address(instance))); + } + + /// @notice Test the solution for the level. + function testSolve() public { + uint256 n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141; + + bytes memory signature = instance.lastSignature(); + (bytes32 r, bytes32 s, uint8 v) = _splitSignature(signature); + + uint8 manipulatedV = v % 2 == 0 ? v - 1 : v + 1; + uint256 manipulatedS = n - uint256(s); + bytes memory manipulatedSignature = abi.encodePacked( + r, + bytes32(manipulatedS), + manipulatedV + ); + + vm.startPrank(player); + instance.setArtist(player, manipulatedSignature); + instance.withdraw(); + + assertTrue(submitLevelInstance(ethernaut, address(instance))); + vm.stopPrank(); + } +}