Module 8: Security Considerations

Module 8: Security Considerations and Best Practices

8.1 Common Security Vulnerabilities

Smart contracts are immutable once deployed, making security a critical concern. Understanding common vulnerabilities is essential for writing secure Solidity code.

Reentrancy Attacks

Reentrancy occurs when an external contract call is made before state updates, allowing the called contract to recursively call back into the original function.

Vulnerable Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VulnerableBank {
    mapping(address => uint) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint _amount) public {
        require(balances[msg.sender] >= _amount, "Insufficient balance");

        // Vulnerable: External call before state update
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success, "Transfer failed");

        balances[msg.sender] -= _amount;
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

contract Attacker {
    VulnerableBank public bank;
    address public owner;

    constructor(address _bankAddress) {
        bank = VulnerableBank(_bankAddress);
        owner = msg.sender;
    }

    // Fallback function to execute the reentrancy attack
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw(1 ether);
        }
    }

    function attack() external payable {
        require(msg.value >= 1 ether, "Need at least 1 ether to attack");
        bank.deposit{value: 1 ether}();
        bank.withdraw(1 ether);
    }

    function withdraw() public {
        require(msg.sender == owner, "Not owner");
        payable(owner).transfer(address(this).balance);
    }
}

Secure Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SecureBank {
    mapping(address => uint) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint _amount) public {
        require(balances[msg.sender] >= _amount, "Insufficient balance");

        // Secure: Update state before external call
        balances[msg.sender] -= _amount;

        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success, "Transfer failed");
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

Reentrancy Guard Pattern

For more complex contracts, you can use a reentrancy guard:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ReentrancyGuard {
    bool private locked;

    modifier nonReentrant() {
        require(!locked, "ReentrancyGuard: reentrant call");
        locked = true;
        _;
        locked = false;
    }

    function protectedFunction() public nonReentrant {
        // Function logic that includes external calls
    }
}

Integer Overflow and Underflow

Before Solidity 0.8.0, integer overflow and underflow were common vulnerabilities. In versions 0.8.0 and later, Solidity automatically checks for overflow/underflow and reverts if they occur.

Vulnerable Example (pre-0.8.0)

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

contract VulnerableToken {
    mapping(address => uint8) public balances;

    function transfer(address _to, uint8 _amount) public {
        // Vulnerable: No check for underflow
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
    }
}

If a user with a balance of 10 tries to transfer 20 tokens, their balance would underflow to 246 (256 - 20 + 10) instead of reverting.

Secure Example (pre-0.8.0)

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

contract SecureToken {
    mapping(address => uint8) public balances;

    function transfer(address _to, uint8 _amount) public {
        // Secure: Check for underflow
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
    }
}

Using SafeMath (pre-0.8.0)

For pre-0.8.0 contracts, you can use the SafeMath library:

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "SafeMath: subtraction overflow");
        return a - b;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) return 0;
        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0, "SafeMath: division by zero");
        return a / b;
    }
}

contract SafeToken {
    using SafeMath for uint256;
    mapping(address => uint256) public balances;

    function transfer(address _to, uint256 _amount) public {
        balances[msg.sender] = balances[msg.sender].sub(_amount);
        balances[_to] = balances[_to].add(_amount);
    }
}

Unchecked Return Values

Some low-level functions like send() and call() return a boolean indicating success or failure. Failing to check these return values can lead to unexpected behavior.

Vulnerable Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VulnerablePayment {
    function sendPayment(address payable _recipient, uint _amount) public {
        // Vulnerable: Return value not checked
        _recipient.send(_amount);
    }
}

Secure Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SecurePayment {
    function sendPayment(address payable _recipient, uint _amount) public {
        // Secure: Check return value
        bool success = _recipient.send(_amount);
        require(success, "Payment failed");
    }

    function sendPaymentWithCall(address payable _recipient, uint _amount) public {
        // Secure: Check return value from call
        (bool success, ) = _recipient.call{value: _amount}("");
        require(success, "Payment failed");
    }
}

Denial of Service (DoS)

DoS attacks can occur when a contract's functionality can be blocked by malicious actors.

Vulnerable Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VulnerableAuction {
    address public currentLeader;
    uint public highestBid;

    function bid() public payable {
        require(msg.value > highestBid, "Bid not high enough");

        // Vulnerable: Refund can fail if recipient is a contract that rejects payments
        payable(currentLeader).transfer(highestBid);

        currentLeader = msg.sender;
        highestBid = msg.value;
    }
}

Secure Example (Pull over Push)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SecureAuction {
    address public currentLeader;
    uint public highestBid;
    mapping(address => uint) public refunds;

    function bid() public payable {
        require(msg.value > highestBid, "Bid not high enough");

        // Secure: Store refund for later withdrawal
        if (currentLeader != address(0)) {
            refunds[currentLeader] += highestBid;
        }

        currentLeader = msg.sender;
        highestBid = msg.value;
    }

    function withdrawRefund() public {
        uint refund = refunds[msg.sender];
        require(refund > 0, "No refund available");

        refunds[msg.sender] = 0;
        payable(msg.sender).transfer(refund);
    }
}

Front-Running

Front-running occurs when someone sees a pending transaction and submits their own transaction with a higher gas price to be processed first.

Vulnerable Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VulnerableNameRegistry {
    mapping(string => address) public domains;

    function register(string memory domain) public {
        require(domains[domain] == address(0), "Domain already registered");
        domains[domain] = msg.sender;
    }
}

Secure Example (Commit-Reveal)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SecureNameRegistry {
    mapping(bytes32 => address) public commitments;
    mapping(string => address) public domains;

    // Step 1: User commits a hash (domain name + salt)
    function commit(bytes32 commitment) public {
        commitments[commitment] = msg.sender;
    }

    // Step 2: User reveals the domain and salt
    function register(string memory domain, bytes32 salt) public {
        bytes32 commitment = keccak256(abi.encodePacked(domain, salt, msg.sender));
        require(commitments[commitment] == msg.sender, "Invalid commitment");
        require(domains[domain] == address(0), "Domain already registered");

        domains[domain] = msg.sender;
        delete commitments[commitment];
    }
}

Timestamp Dependence

Using block.timestamp for critical logic can be manipulated by miners within a small window.

Vulnerable Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VulnerableRandomness {
    function random() public view returns (uint) {
        // Vulnerable: Timestamp can be manipulated by miners
        return uint(keccak256(abi.encodePacked(block.timestamp, msg.sender)));
    }
}

Secure Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SecureRandomness {
    uint private nonce = 0;

    function random() public returns (uint) {
        // More secure: Includes nonce and blockhash
        nonce++;
        return uint(keccak256(abi.encodePacked(block.difficulty, block.timestamp, msg.sender, nonce)));
    }
}

For truly secure randomness, consider using an oracle like Chainlink VRF.

8.2 Security Best Practices

Check-Effects-Interactions Pattern

Always follow the Check-Effects-Interactions pattern: 1. Check all preconditions 2. Apply effects to the contract's state 3. Interact with other contracts or external addresses

function withdraw(uint amount) public {
    // 1. Checks
    require(balances[msg.sender] >= amount, "Insufficient balance");

    // 2. Effects
    balances[msg.sender] -= amount;

    // 3. Interactions
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
}

Use Specific Compiler Pragma

Avoid floating pragmas to ensure your contract is compiled with the expected compiler version:

// Bad: Floating pragma
pragma solidity ^0.8.0;

// Good: Specific pragma
pragma solidity 0.8.17;

Proper Error Handling

Use descriptive error messages and custom errors (Solidity 0.8.4+) for better debugging and gas efficiency:

// Using require with descriptive message
require(amount > 0, "Amount must be greater than zero");

// Using custom errors (more gas efficient)
error InsufficientBalance(uint requested, uint available);

function withdraw(uint amount) public {
    if (balances[msg.sender] < amount) {
        revert InsufficientBalance({
            requested: amount,
            available: balances[msg.sender]
        });
    }
    // Function logic
}

Input Validation

Always validate function inputs:

function transfer(address to, uint amount) public {
    require(to != address(0), "Cannot transfer to zero address");
    require(amount > 0, "Amount must be greater than zero");
    require(balances[msg.sender] >= amount, "Insufficient balance");

    // Transfer logic
}

Use SafeERC20

When interacting with ERC20 tokens, use the SafeERC20 library to handle tokens that don't return a boolean:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract TokenHandler {
    using SafeERC20 for IERC20;

    function transferTokens(IERC20 token, address to, uint amount) public {
        token.safeTransfer(to, amount);
    }
}

Avoid tx.origin for Authentication

Using tx.origin for authentication is vulnerable to phishing attacks:

// Vulnerable
function withdraw() public {
    require(tx.origin == owner, "Not owner");
    // Withdraw logic
}

// Secure
function withdraw() public {
    require(msg.sender == owner, "Not owner");
    // Withdraw logic
}

Use transfer() and send() Cautiously

The transfer() and send() functions have a gas stipend of 2300 gas, which may not be enough for complex receivers:

// May fail if receiver is a contract with complex logic
payable(recipient).transfer(amount);

// Better approach
(bool success, ) = payable(recipient).call{value: amount}("");
require(success, "Transfer failed");

Protect Against Signature Replay

When using signatures for authentication, include a nonce or other unique identifier to prevent replay attacks:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SecureSignature {
    mapping(bytes32 => bool) public usedSignatures;

    function claimReward(uint amount, uint nonce, bytes memory signature) public {
        bytes32 messageHash = keccak256(abi.encodePacked(msg.sender, amount, nonce, address(this)));
        bytes32 signedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));

        require(!usedSignatures[signedHash], "Signature already used");

        // Verify signature logic

        usedSignatures[signedHash] = true;
        // Reward logic
    }
}

Use OpenZeppelin Contracts

Leverage battle-tested libraries like OpenZeppelin for common functionality:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SecureToken is ERC20, Ownable, ReentrancyGuard {
    constructor() ERC20("SecureToken", "SECURE") {
        _mint(msg.sender, 1000000 * 10**18);
    }

    function withdraw() public onlyOwner nonReentrant {
        // Secure withdrawal logic
    }
}

8.3 Security Tools and Auditing

Static Analysis Tools

Several tools can help identify vulnerabilities in your code:

  1. Slither: A static analysis framework that detects common vulnerabilities
  2. MythX: A security analysis platform for Ethereum smart contracts
  3. Solhint: A linter for Solidity that identifies coding style and security issues

Example Slither usage:

pip install slither-analyzer
slither your_contract.sol

Formal Verification

Formal verification mathematically proves the correctness of your contract:

  1. Certora Prover: Verifies that your contract satisfies specific properties
  2. SMTChecker: Built into Solidity, can prove certain properties of your code

Security Audits

Professional security audits are essential for contracts handling significant value:

  1. Manual code review: Experienced auditors review your code line by line
  2. Automated tools: Auditors use specialized tools to identify vulnerabilities
  3. Test scenarios: Auditors create test cases to verify contract behavior

Testing Best Practices

Comprehensive testing is crucial for security:

  1. Unit tests: Test individual functions
  2. Integration tests: Test interactions between contracts
  3. Fuzz testing: Test with random inputs to find edge cases
  4. Invariant testing: Verify that certain properties always hold

Example using Hardhat and Chai:

const { expect } = require("chai");

describe("Token", function() {
  let token;
  let owner;
  let addr1;
  let addr2;

  beforeEach(async function() {
    const Token = await ethers.getContractFactory("Token");
    [owner, addr1, addr2] = await ethers.getSigners();
    token = await Token.deploy();
  });

  it("Should revert when transferring more than balance", async function() {
    await expect(
      token.connect(addr1).transfer(addr2.address, 1)
    ).to.be.revertedWith("Insufficient balance");
  });

  it("Should update balances after transfers", async function() {
    await token.transfer(addr1.address, 100);
    expect(await token.balanceOf(addr1.address)).to.equal(100);

    await token.connect(addr1).transfer(addr2.address, 50);
    expect(await token.balanceOf(addr1.address)).to.equal(50);
    expect(await token.balanceOf(addr2.address)).to.equal(50);
  });
});

8.4 Practical Example: Secure Token Sale Contract

Let's create a secure token sale contract that incorporates the security best practices we've discussed:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/**
 * @title SecureTokenSale
 * @dev A secure token sale contract with rate limiting, whitelist, and signature verification
 */
contract SecureTokenSale is ReentrancyGuard, Ownable {
    using SafeERC20 for IERC20;
    using ECDSA for bytes32;

    // State variables
    IERC20 public token;
    uint256 public price;
    uint256 public minPurchase;
    uint256 public maxPurchase;
    uint256 public saleStart;
    uint256 public saleEnd;
    uint256 public hardCap;
    uint256 public totalSold;

    mapping(address => bool) public whitelist;
    mapping(address => uint256) public purchases;
    mapping(bytes32 => bool) public usedSignatures;

    // Events
    event TokensPurchased(address indexed buyer, uint256 amount, uint256 cost);
    event WhitelistUpdated(address indexed user, bool status);
    event PriceUpdated(uint256 newPrice);
    event SaleDatesUpdated(uint256 newStart, uint256 newEnd);
    event TokensWithdrawn(address indexed to, uint256 amount);
    event EtherWithdrawn(address indexed to, uint256 amount);

    // Custom errors
    error SaleNotActive();
    error PurchaseTooSmall(uint256 sent, uint256 minimum);
    error PurchaseTooLarge(uint256 sent, uint256 maximum);
    error HardCapExceeded(uint256 requested, uint256 remaining);
    error NotWhitelisted(address user);
    error InvalidSignature();
    error SignatureAlreadyUsed();
    error TransferFailed();

    /**
     * @dev Constructor to initialize the token sale
     * @param _token Address of the token being sold
     * @param _price Price of each token in wei
     * @param _minPurchase Minimum purchase amount in wei
     * @param _maxPurchase Maximum purchase amount in wei
     * @param _saleStart Start time of the sale
     * @param _saleEnd End time of the sale
     * @param _hardCap Maximum amount of tokens to sell
     */
    constructor(
        IERC20 _token,
        uint256 _price,
        uint256 _minPurchase,
        uint256 _maxPurchase,
        uint256 _saleStart,
        uint256 _saleEnd,
        uint256 _hardCap
    ) {
        require(address(_token) != address(0), "Token cannot be zero address");
        require(_price > 0, "Price must be greater than zero");
        require(_minPurchase > 0, "Min purchase must be greater than zero");
        require(_maxPurchase >= _minPurchase, "Max purchase must be >= min purchase");
        require(_saleEnd > _saleStart, "End time must be after start time");
        require(_hardCap > 0, "Hard cap must be greater than zero");

        token = _token;
        price = _price;
        minPurchase = _minPurchase;
        maxPurchase = _maxPurchase;
        saleStart = _saleStart;
        saleEnd = _saleEnd;
        hardCap = _hardCap;
    }

    /**
     * @dev Modifier to check if the sale is active
     */
    modifier whenSaleActive() {
        if (block.timestamp < saleStart || block.timestamp > saleEnd) {
            revert SaleNotActive();
        }
        _;
    }

    /**
     * @dev Purchase tokens with ETH
     */
    function purchaseTokens() external payable nonReentrant whenSaleActive {
        // Input validation
        if (msg.value < minPurchase) {
            revert PurchaseTooSmall(msg.value, minPurchase);
        }
        if (msg.value > maxPurchase) {
            revert PurchaseTooLarge(msg.value, maxPurchase);
        }
        if (!whitelist[msg.sender]) {
            revert NotWhitelisted(msg.sender);
        }

        // Calculate token amount
        uint256 tokenAmount = (msg.value * 10**18) / price;

        // Check hard cap
        if (totalSold + tokenAmount > hardCap) {
            revert HardCapExceeded(tokenAmount, hardCap - totalSold);
        }

        // Update state
        totalSold += tokenAmount;
        purchases[msg.sender] += tokenAmount;

        // Transfer tokens
        token.safeTransfer(msg.sender, tokenAmount);

        // Emit event
        emit TokensPurchased(msg.sender, tokenAmount, msg.value);
    }

    /**
     * @dev Purchase tokens with signature (for gas-less whitelisting)
     * @param amount Amount of tokens to purchase
     * @param deadline Expiration time of the signature
     * @param signature Signature from the contract owner
     */
    function purchaseWithSignature(
        uint256 amount,
        uint256 deadline,
        bytes memory signature
    ) external payable nonReentrant whenSaleActive {
        // Check deadline
        require(block.timestamp <= deadline, "Signature expired");

        // Verify signature
        bytes32 messageHash = keccak256(abi.encodePacked(
            msg.sender,
            amount,
            deadline,
            address(this)
        ));
        bytes32 signedHash = keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            messageHash
        ));

        if (usedSignatures[signedHash]) {
            revert SignatureAlreadyUsed();
        }

        address signer = signedHash.recover(signature);
        if (signer != owner()) {
            revert InvalidSignature();
        }

        // Mark signature as used
        usedSignatures[signedHash] = true;

        // Calculate cost
        uint256 cost = (amount * price) / 10**18;
        if (msg.value < cost) {
            revert PurchaseTooSmall(msg.value, cost);
        }

        // Check hard cap
        if (totalSold + amount > hardCap) {
            revert HardCapExceeded(amount, hardCap - totalSold);
        }

        // Update state
        totalSold += amount;
        purchases[msg.sender] += amount;

        // Transfer tokens
        token.safeTransfer(msg.sender, amount);

        // Refund excess ETH
        if (msg.value > cost) {
            (bool success, ) = msg.sender.call{value: msg.value - cost}("");
            if (!success) {
                revert TransferFailed();
            }
        }

        // Emit event
        emit TokensPurchased(msg.sender, amount, cost);
    }

    /**
     * @dev Update the whitelist status for a user
     * @param user Address to update
     * @param status New whitelist status
     */
    function updateWhitelist(address user, bool status) external onlyOwner {
        require(user != address(0), "Cannot whitelist zero address");
        whitelist[user] = status;
        emit WhitelistUpdated(user, status);
    }

    /**
     * @dev Batch update whitelist
     * @param users Array of addresses to update
     * @param statuses Array of whitelist statuses
     */
    function batchUpdateWhitelist(
        address[] calldata users,
        bool[] calldata statuses
    ) external onlyOwner {
        require(users.length == statuses.length, "Array lengths must match");

        for (uint256 i = 0; i < users.length; i++) {
            require(users[i] != address(0), "Cannot whitelist zero address");
            whitelist[users[i]] = statuses[i];
            emit WhitelistUpdated(users[i], statuses[i]);
        }
    }

    /**
     * @dev Update the token price
     * @param _price New price in wei
     */
    function updatePrice(uint256 _price) external onlyOwner {
        require(_price > 0, "Price must be greater than zero");
        price = _price;
        emit PriceUpdated(_price);
    }

    /**
     * @dev Update sale dates
     * @param _saleStart New start time
     * @param _saleEnd New end time
     */
    function updateSaleDates(uint256 _saleStart, uint256 _saleEnd) external onlyOwner {
        require(_saleEnd > _saleStart, "End time must be after start time");
        saleStart = _saleStart;
        saleEnd = _saleEnd;
        emit SaleDatesUpdated(_saleStart, _saleEnd);
    }

    /**
     * @dev Withdraw unsold tokens
     * @param to Address to send tokens to
     * @param amount Amount of tokens to withdraw
     */
    function withdrawTokens(address to, uint256 amount) external onlyOwner {
        require(to != address(0), "Cannot withdraw to zero address");
        token.safeTransfer(to, amount);
        emit TokensWithdrawn(to, amount);
    }

    /**
     * @dev Withdraw collected ETH
     * @param to Address to send ETH to
     * @param amount Amount of ETH to withdraw
     */
    function withdrawEther(address to, uint256 amount) external onlyOwner nonReentrant {
        require(to != address(0), "Cannot withdraw to zero address");
        require(amount <= address(this).balance, "Insufficient balance");

        (bool success, ) = to.call{value: amount}("");
        if (!success) {
            revert TransferFailed();
        }

        emit EtherWithdrawn(to, amount);
    }

    /**
     * @dev Get remaining tokens available for sale
     * @return uint256 Number of tokens available
     */
    function remainingTokens() public view returns (uint256) {
        return hardCap - totalSold;
    }

    /**
     * @dev Check if the sale is active
     * @return bool Whether the sale is active
     */
    function isSaleActive() public view returns (bool) {
        return block.timestamp >= saleStart && block.timestamp <= saleEnd;
    }
}

This token sale contract incorporates numerous security best practices:

  1. Reentrancy protection: Uses OpenZeppelin's ReentrancyGuard
  2. Access control: Uses OpenZeppelin's Ownable
  3. Safe token transfers: Uses SafeERC20
  4. Input validation: Validates all inputs in constructor and functions
  5. Check-Effects-Interactions pattern: Updates state before external calls
  6. Custom errors: Uses custom errors for better debugging and gas efficiency
  7. Rate limiting: Implements min/max purchase amounts
  8. Signature verification: Uses ECDSA for secure signature verification
  9. Signature replay protection: Tracks used signatures
  10. Event emission: Emits events for all important actions
  11. Specific compiler pragma: Uses a specific Solidity version

8.5 Exercise: Secure Escrow Contract

Task: Create a secure escrow contract that holds funds until certain conditions are met, incorporating the security best practices we've discussed.

Solution:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
 * @title SecureEscrow
 * @dev A secure escrow contract for holding funds until conditions are met
 */
contract SecureEscrow is ReentrancyGuard {
    // Escrow states
    enum State { Created, Funded, Completed, Refunded, Disputed }

    struct EscrowAgreement {
        address payable buyer;
        address payable seller;
        address arbiter;
        uint256 amount;
        State state;
        uint256 createdAt;
        uint256 completionDeadline;
    }

    // Mapping from escrow ID to agreement
    mapping(uint256 => EscrowAgreement) public agreements;

    // Counter for escrow IDs
    uint256 private nextEscrowId = 1;

    // Events
    event EscrowCreated(
        uint256 indexed escrowId,
        address indexed buyer,
        address indexed seller,
        address arbiter,
        uint256 amount,
        uint256 completionDeadline
    );
    event EscrowFunded(uint256 indexed escrowId, uint256 amount);
    event EscrowCompleted(uint256 indexed escrowId);
    event EscrowRefunded(uint256 indexed escrowId);
    event DisputeRaised(uint256 indexed escrowId);
    event DisputeResolved(uint256 indexed escrowId, address winner);

    // Custom errors
    error InvalidAddress();
    error InvalidAmount();
    error InvalidDeadline();
    error InvalidState(State current, State expected);
    error NotAuthorized();
    error DeadlineNotReached();
    error TransferFailed();

    /**
     * @dev Create a new escrow agreement
     * @param seller Address of the seller
     * @param arbiter Address of the arbiter
     * @param completionDeadline Deadline for completion
     * @return escrowId ID of the created escrow
     */
    function createEscrow(
        address payable seller,
        address arbiter,
        uint256 completionDeadline
    ) external returns (uint256 escrowId) {
        // Input validation
        if (seller == address(0) || arbiter == address(0)) {
            revert InvalidAddress();
        }
        if (completionDeadline <= block.timestamp) {
            revert InvalidDeadline();
        }

        escrowId = nextEscrowId++;

        agreements[escrowId] = EscrowAgreement({
            buyer: payable(msg.sender),
            seller: seller,
            arbiter: arbiter,
            amount: 0,
            state: State.Created,
            createdAt: block.timestamp,
            completionDeadline: completionDeadline
        });

        emit EscrowCreated(
            escrowId,
            msg.sender,
            seller,
            arbiter,
            0,
            completionDeadline
        );
    }

    /**
     * @dev Fund an escrow agreement
     * @param escrowId ID of the escrow to fund
     */
    function fundEscrow(uint256 escrowId) external payable nonReentrant {
        EscrowAgreement storage agreement = agreements[escrowId];

        // Validate state
        if (agreement.state != State.Created) {
            revert InvalidState(agreement.state, State.Created);
        }

        // Validate sender
        if (msg.sender != agreement.buyer) {
            revert NotAuthorized();
        }

        // Validate amount
        if (msg.value == 0) {
            revert InvalidAmount();
        }

        // Update state
        agreement.amount = msg.value;
        agreement.state = State.Funded;

        emit EscrowFunded(escrowId, msg.value);
    }

    /**
     * @dev Complete an escrow and release funds to the seller
     * @param escrowId ID of the escrow to complete
     */
    function completeEscrow(uint256 escrowId) external nonReentrant {
        EscrowAgreement storage agreement = agreements[escrowId];

        // Validate state
        if (agreement.state != State.Funded) {
            revert InvalidState(agreement.state, State.Funded);
        }

        // Validate sender
        if (msg.sender != agreement.buyer && msg.sender != agreement.arbiter) {
            revert NotAuthorized();
        }

        // Update state before transfer
        agreement.state = State.Completed;

        // Transfer funds to seller
        (bool success, ) = agreement.seller.call{value: agreement.amount}("");
        if (!success) {
            revert TransferFailed();
        }

        emit EscrowCompleted(escrowId);
    }

    /**
     * @dev Refund an escrow to the buyer
     * @param escrowId ID of the escrow to refund
     */
    function refundEscrow(uint256 escrowId) external nonReentrant {
        EscrowAgreement storage agreement = agreements[escrowId];

        // Validate state
        if (agreement.state != State.Funded && agreement.state != State.Disputed) {
            revert InvalidState(agreement.state, State.Funded);
        }

        // Validate sender
        bool isAuthorized = msg.sender == agreement.seller || 
                           msg.sender == agreement.arbiter ||
                           (msg.sender == agreement.buyer && 
                            block.timestamp > agreement.completionDeadline);

        if (!isAuthorized) {
            if (msg.sender == agreement.buyer && block.timestamp <= agreement.completionDeadline) {
                revert DeadlineNotReached();
            } else {
                revert NotAuthorized();
            }
        }

        // Update state before transfer
        agreement.state = State.Refunded;

        // Transfer funds to buyer
        (bool success, ) = agreement.buyer.call{value: agreement.amount}("");
        if (!success) {
            revert TransferFailed();
        }

        emit EscrowRefunded(escrowId);
    }

    /**
     * @dev Raise a dispute for an escrow
     * @param escrowId ID of the escrow to dispute
     */
    function raiseDispute(uint256 escrowId) external {
        EscrowAgreement storage agreement = agreements[escrowId];

        // Validate state
        if (agreement.state != State.Funded) {
            revert InvalidState(agreement.state, State.Funded);
        }

        // Validate sender
        if (msg.sender != agreement.buyer && msg.sender != agreement.seller) {
            revert NotAuthorized();
        }

        // Update state
        agreement.state = State.Disputed;

        emit DisputeRaised(escrowId);
    }

    /**
     * @dev Resolve a dispute
     * @param escrowId ID of the disputed escrow
     * @param winner Address that should receive the funds
     */
    function resolveDispute(uint256 escrowId, address payable winner) external nonReentrant {
        EscrowAgreement storage agreement = agreements[escrowId];

        // Validate state
        if (agreement.state != State.Disputed) {
            revert InvalidState(agreement.state, State.Disputed);
        }

        // Validate sender
        if (msg.sender != agreement.arbiter) {
            revert NotAuthorized();
        }

        // Validate winner
        if (winner != agreement.buyer && winner != agreement.seller) {
            revert InvalidAddress();
        }

        // Update state before transfer
        if (winner == agreement.buyer) {
            agreement.state = State.Refunded;
        } else {
            agreement.state = State.Completed;
        }

        // Transfer funds to winner
        (bool success, ) = winner.call{value: agreement.amount}("");
        if (!success) {
            revert TransferFailed();
        }

        emit DisputeResolved(escrowId, winner);
    }

    /**
     * @dev Get details of an escrow agreement
     * @param escrowId ID of the escrow
     * @return Full escrow agreement details
     */
    function getEscrow(uint256 escrowId) external view returns (EscrowAgreement memory) {
        return agreements[escrowId];
    }

    /**
     * @dev Check if an escrow is past its deadline
     * @param escrowId ID of the escrow
     * @return bool Whether the escrow is past its deadline
     */
    function isPastDeadline(uint256 escrowId) external view returns (bool) {
        return block.timestamp > agreements[escrowId].completionDeadline;
    }
}

This escrow contract incorporates numerous security best practices:

  1. Reentrancy protection: Uses OpenZeppelin's ReentrancyGuard
  2. State management: Uses an enum to track the state of each escrow
  3. Input validation: Validates all inputs in functions
  4. Check-Effects-Interactions pattern: Updates state before external calls
  5. Custom errors: Uses custom errors for better debugging and gas efficiency
  6. Event emission: Emits events for all important actions
  7. Specific compiler pragma: Uses a specific Solidity version
  8. Access control: Checks that only authorized parties can perform actions
  9. Deadline management: Implements deadline checks for time-sensitive operations

Conclusion

Security is paramount in smart contract development. By understanding common vulnerabilities and following best practices, you can significantly reduce the risk of security issues in your contracts.

Key takeaways from this module:

  1. Be aware of common vulnerabilities like reentrancy, integer overflow/underflow, and front-running.
  2. Follow the Check-Effects-Interactions pattern to prevent reentrancy attacks.
  3. Use specific compiler pragmas and validate all inputs.
  4. Leverage security tools and conduct thorough testing.
  5. Consider professional audits for contracts handling significant value.
  6. Use established libraries like OpenZeppelin for common functionality.

In the next module, we'll explore gas optimization techniques to make your contracts more efficient and cost-effective.