The following smart contract follows the checks-effects-interaction pattern aiming at reducing the surface attack for malicious contracts that may attempt to hijack control flow after an external call:
1- Checks : Validate all conditions before making any state change or external calls,
2- Effects - Perform all necessary state changes,
3- Interactions : interact with other contracts or transfer funds only after state changes are complete.
The goal of this pattern is to prevent re-entrancy attack (the famous dao-hack from 2016 kind of issue),
where an external contract call can recursively call back into the vulnerable function before the initial execution complete, potentially leading to unexpected behavior and security issues:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import { ReentrancyGuard } from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
contract SecureBanking is ReentrancyGuard {
// @dev mapping named' balances' that maps Ethereum addresses to unsigned integers, which represents the balance of each user.
mapping(address => uint256) public balances ;
/**
* @dev Emitted when a user makes a deposit
* @param user The adress of the user making the deposit
* @param amount The amount of Ether deposited by the user .
*/
event Deposit(address indexed user, uint amount);
/**
* @dev emitted when a user withdrw funds .
* @param user The address of the user making the withdrawal
* @param amount The amount of Ether withdrawn by the user
*/
event Withdrawal(address indexed user, uint amount);
/**
* @notice Deposits Ether into the contract.
* @dev Increases the balance of the sender by the amount of Ether sent with the transaction.
* Emits a {Deposit} event.
*/
function depositFunds() public payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
/**
* @notice Withdraws a specified amount of Ether from the contract.
* @dev Reduces the sender's balance by the specified amount and transfers the amount to the sender.
* Uses the `nonReentrant` modifier to prevent reentrancy attacks.
* Emits a {Withdrawal} event.
* @param amount The amount of Ether to withdraw.
* Requirements:
* - The sender must have a balance greater than or equal to the `amount`.
*/
function withdrawFunds(uint256 amount) public nonReentrant {
//Checks - This line checks if the user has sufficient balancer to withdraw the specified amount;
require(balances[msg.sender] >= amount, "Insufficient balance");
// Effects - this line updates the state by reducing the balance of the user.
balances[msg.sender] -= amount;
// Interactions - This line transfers the specified amount to the user.
payable(msg.sender).transfer(amount);
emit Withdrawal(msg.sender, amount);
}
}
The analysis of the provided smart contract snippet reveals it does indeed follow the
Checks-Effects-Interactions pattern and offers protection against re-entrancy attacks.
Short breakdown:
Checks:
The withdrawFunds function starts with a check using ‘require’ .
It verifies if the user’s balance ’(balances[msg.sender])’ is greater than or equal to the withdrawal amount (amount).
This ensures the user has sufficient funds before proceeding.
Effects:
If the check passes, the contract modifies the state by subtracting the withdrawal amount from the user’s balance using ‘balances[msg.sender] -= amount’ .
Interactions:
Finally, the contract interacts with the external environment by transferring the requested amount to the user using ‘payable(msg.sender).transfer(amount)’. This occurs only after the balance update, preventing manipulation during the transfer process.
Protection Against Re-entrancy:
The nonReentrant modifier provided by the imported ReentrancyGuard library plays a crucial role.
This modifier ensures a function can only be called once per transaction, effectively blocking
any recursive calls from a malicious external contract during the withdrawal process.