Aim of the challenge is to steal the funds from the CallMeBack contract.
This contract allows users to donate and withdraw Ether using the donate and withdraw functions.
In the withdraw function, it has a Reentrancy vulnerability which allows an attacker to repeatedly call the withdraw function before the balance is updated.
To exploit this vulnerability, an attacker can create a malicious contract that calls the withdraw function of the CallMeBack contract and in the fallback function of the malicious contract, it can call the withdraw function again until the desired amount is withdrawn.
function transfer(address _to, uint256 _value) public override lockTokens returns (bool) { super.transfer(_to, _value); }
// Prevent the initial owner from transferring tokens until the timelock has passed modifier lockTokens() { if (msg.sender == player) { require(block.timestamp > timeLock); _; } } }
The goal of the challenge is to make an transaction inspite of having a timelock of 10 years.
The Timelock contract inherits from the ERC20 contract and implements a timelock mechanism that prevents the initial owner from transferring tokens until a specified time has passed.
The transfer function is overridden to include a lockTokens modifier that checks if the sender is the initial owner and if the current time is greater than the timelock.
To bypass the timelock, we need to use other functionalities provided by the ERC20 standard like approve and transferFrom which do not have the timelock restriction.
approve allows the owner to approve another address to spend tokens on their behalf, and transferFrom allows the approved address to transfer tokens from the owner’s account.
We can use Remix IDE to perform approve function as it requires the owner’s (i.e Deployed contract) address to call it. We will allow another address (our exploit contract address) to spend the tokens.
Now we can use our exploit contract to call the transferFrom function to transfer all tokens from the owner’s account to our exploit contract.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "./Challenge.sol";
contract Solve{
Challenge public chal = Challenge(address(<CHALLENGE_ADDRESS>)); Timelock t = chal.CONTRACT();
function exploit() public { t.transferFrom(t.player(), address(this),t.INITIAL_SUPPLY()); } }
Concluding Thoughts
This was my first time trying an onchain CTF and it was a great experience overall. Learnt lots of new things like using a custom network to solve the challenge, interacting with attacker account using a private key, etc. Looking forward to more such CTFs in future.