10 lỗi phổ biến trong Solidity

Dưới đây là 10 lỗi phổ biến trong Solidity, ngôn ngữ lập trình dùng để phát triển các smart contract trên nền tảng Ethereum:

1. Tấn công Reentrancy (Reentrancy Attack)

Đây là một lỗi bảo mật nguy hiểm khi một hàm trong smart contract gọi một hàm ngoài mà không kiểm tra lại trạng thái của contract trước khi truy cập vào nó. Điều này có thể dẫn đến việc một đối tượng ngoại tuyến (attacker) tấn công bằng cách gọi lại hàm gây lãng phí tài nguyên hoặc đánh cắp Ether.

Điều này giống như việc bạn đến nhà bạn bè, và khi bạn vào trong, họ lại mở cửa ra và đưa bạn vào nhà hàng xóm. Điều này có thể dẫn đến việc bạn bị nhà hàng xóm chiếm lấy tài sản của bạn hoặc đánh cắp bất kỳ thứ gì.

contract Bank {
    mapping(address => uint) balances;

    function withdraw(uint amount) external {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
    }
}

Cách sửa lỗi:

  • Sử dụng mẫu thiết kế “Withdrawal Pattern” để đảm bảo rằng hàm gửi tiền chỉ được gọi một lần và kiểm tra trạng thái trước khi thực hiện giao dịch.
  • Sử dụng modifier để kiểm tra trạng thái trước và sau khi giao dịch.

Code sửa lỗi:

bool private locked;

modifier noReentrancy() {
    require(!locked, "Reentrancy detected!");
    locked = true;
    _;
    locked = false;
}

function withdraw() external noReentrancy {
    // code for withdrawal
}

2. Tràn/Trượt số nguyên (Integer Overflow/Underflow)

Khi thực hiện các phép toán số học trên số nguyên trong Solidity, việc không kiểm tra sự tràn số có thể dẫn đến kết quả không chính xác hoặc không mong muốn. Lỗi này hay gặp ở các phiên bản Solidity nhỏ hơn 0.8. Điều này có thể được sử lý bằng cách sử dụng thư viện SafeMath hoặc kiểm tra ràng buộc trước khi thực hiện các phép toán.

contract MathOperations {
    function add(uint256 a, uint256 b) external pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a);
        return c;
    }
}

Cách sửa lỗi:

Code sửa lỗi:

import "@openzeppelin/contracts/utils/math/SafeMath.sol";

uint256 private totalBalance;

function addToBalance(uint256 amount) external {
    totalBalance = SafeMath.add(totalBalance, amount);
}

function subtractFromBalance(uint256 amount) external {
    require(amount <= totalBalance, "Insufficient balance");
    totalBalance = SafeMath.sub(totalBalance, amount);
}

3. Tấn công DoS bởi giới hạn Gas Block (DoS by Block Gas Limit)

Solidity có một ràng buộc về gas limit trong mỗi khối. Nếu một hàm trong smart contract mất quá nhiều gas để thực thi, nó có thể dẫn đến lỗi DoS (Denial-of-Service) khi ngăn chặn các giao dịch khác được thực thi trong cùng một khối.

contract GasConsumingContract {
    uint256[] data;

    function consumeGas() external {
        while (gasleft() > 10000) {
            data.push(0);
        }
    }
}

Cách sửa lỗi:

  • Chia nhỏ các tác vụ phức tạp thành các bước nhỏ và ghi lại trạng thái sau mỗi bước để tiết kiệm gas.
  • Sử dụng “gas stipend” để gửi ether cho các hợp đồng khác hoặc giao dịch khác.

4. Lỗi gõ sai (Typosquatting)

Lỗi này xảy ra khi một địa chỉ contract mới được tạo với một tên gần giống với một contract hiện có, nhằm đánh lừa người dùng và lừa đảo tài sản của họ.

contract Bank {
    mapping(address => uint) balances;

    function withdraw(uint amount) external {
        // ...

        // Incorrect contract address
        address attacker = 0x1234567890123456789012345678901234567890;
        attacker.call{value: amount}("");
    }
}

Cách sửa lỗi:

Chú ý kiểm tra kỹ các địa chỉ contract để đảm bảo không có địa chỉ giống nhau hoặc địa chỉ giống với các contract quan trọng khác.

5. Hàm không được bảo vệ (Unprotected Functions)

Một số hàm trong smart contract có thể không được bảo vệ bởi các ràng buộc hoặc kiểm tra đủ để ngăn chặn các hoạt động không mong muốn. Điều này có thể dẫn đến việc truy cập trái phép hoặc sử dụng sai mục đích.

contract Admin {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function changeOwner(address newOwner) external {
        owner = newOwner;
    }
}

Cách sửa lỗi:

Áp dụng các kiểm soát truy cập như kiểm tra quyền hạn, phân quyền và phải ngừng các hành vi không mong muốn.

Code sửa lỗi

address public owner;

modifier onlyOwner() {
    require(msg.sender == owner, "Unauthorized");
    _;
}

function changeOwner(address newOwner) external onlyOwner {
    owner = newOwner;
    // Perform owner change
}

6. Thiếu kiểm soát truy cập (Lack of Access Control)

Nếu một smart contract không áp dụng các kiểm soát truy cập đúng đắn, như kiểm tra quyền hạn hoặc phân quyền, nó có thể cho phép người dùng không được ủy quyền thực hiện các hành động không mong muốn hoặc tấn công smart contract.

Điều này giống như việc bạn mời mọi người đến nhà để có bữa tiệc và không có ai kiểm soát ai được vào phòng. Bất kỳ ai muốn có thể vào và làm hỏng tiệc của bạn. Bạn sẽ cảm thấy khó chịu và không có sự kiểm soát.

contract Bank {
    mapping(address => uint) balances;

    function withdraw(uint amount) external {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        msg.sender.call{value: amount}("");
    }
}

Cách sửa lỗi:

  • Sử dụng modifier để kiểm tra quyền hạn trước khi thực hiện hành động quan trọng.
  • Sử dụng mẫu thiết kế “Access Control” để áp dụng các phân quyền cho các chức năng quan trọng.

Code sửa lỗi:

address public owner;

modifier onlyOwner() {
    require(msg.sender == owner, "Unauthorized");
    _;
}

function setPrice(uint256 newPrice) external onlyOwner {
    // Set new price
}

7. Thiếu xác thực dữ liệu đầu vào (Lack of Input Validation)

Nếu smart contract không kiểm tra và xác thực dữ liệu đầu vào từ người dùng, nó có thể bị lừa đảo, tấn công bằng các giá trị không hợp lệ hoặc dẫn đến lỗi không mong muốn.

contract Auction {
    uint public highestBid;

    function placeBid(uint amount) external {
        require(amount > highestBid);
        highestBid = amount;
    }
}

Cách sửa lỗi:

  • Kiểm tra và xác thực dữ liệu đầu vào từ người dùng trước khi sử dụng chúng trong hợp đồng.
  • Sử dụng thư viện như OpenZeppelin để kiểm tra tính hợp lệ của dữ liệu.

Code sửa lỗi:

function transferTokens(address to, uint256 amount) external {
    require(to != address(0), "Invalid recipient");
    require(amount > 0, "Invalid amount");
    // Transfer tokens
}

8. Ước lượng giới hạn Gas không chính xác (Gas Limit Estimation)

Solidity không tự động ước lượng lượng gas cần thiết để thực thi một hàm trong smart contract. Do đó, việc không đánh giá chính xác lượng gas có thể dẫn đến việc giao dịch bị hủy hoặc lỗi trong quá trình thực thi.

contract GasConsumingContract {
    uint256[] data;

    function consumeGas() external {
        // Incorrect gas estimation
        for (uint256 i = 0; i < 100000; i++) {
            data.push(i);
        }
    }
}

Cách sửa lỗi:

  • Sử dụng các công cụ mô phỏng và kiểm tra gas trước khi triển khai và thực thi hợp đồng.
  • Cân nhắc sử dụng gas stipend hoặc gửi gas tới các hợp đồng khác nếu cần thiết.

Code sửa lỗi:

function execute() external {
    uint256 gasLimit = gasleft();
    // Perform operations

    require(gasleft() > gasLimit / 2, "Insufficient gas");
    // Continue with remaining operations
}

9. Tấn công Front-Running (Front-Running Attacks)

Đây là một loại tấn công trong đó kẻ tấn công nhận biết một giao dịch trong quá trình xử lý và chèn một giao dịch khác trước khi nó được xác nhận. Điều này có thể làm thay đổi kết quả của giao dịch ban đầu và gây thiệt hại cho người dùng.

contract Exchange {
    mapping(address => uint) balances;

    function trade(address token, uint amount) external {
        // ...

        // Front-running attack
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        // ...
    }
}

Cách sửa lỗi:

Sử dụng các cơ chế chống trước tiến như “commit-reveal” để đảm bảo rằng giao dịch không thể bị thay đổi bởi các tấn công front-running.

bytes32 private hashedValue;

function commit(bytes32 hash) external {
    hashedValue = hash;
}

function reveal(uint256 value) external {
    require(keccak256(abi.encodePacked(value)) == hashedValue, "Invalid value");
    // Perform action with revealed value
}

10. Không tương thích phiên bản Solidity (Solidity Version Compatibility)

Solidity có các phiên bản khác nhau và có thể có sự không tương thích giữa các phiên bản. Việc sử dụng phiên bản không tương thích có thể dẫn đến lỗi không mong muốn trong quá trình triển khai và thực thi smart contract.

pragma solidity ^0.8.0;

contract MyContract {
    uint256 public myNumber;

    function setNumber(uint256 number) external {
        myNumber = number;
    }
}

Cách sửa lỗi:

  • Kiểm tra và đảm bảo rằng các phiên bản Solidity được sử dụng trong các phần mềm và hợp đồng tương thích với nhau.
  • Theo dõi các bản cập nhật và bản vá lỗi từ nhà phát triển Solidity và cộng đồng để sử dụng phiên bản mới nhất và ổn định nhất.

11. Các lỗi khác

1. Không kiểm tra hợp lệ của dữ liệu đầu vào

Mã sau đây cho phép bất kỳ ai gửi bất kỳ số tiền nào vào hợp đồng thông minh:

function deposit(uint256 amount) public payable {
  // Không kiểm tra hợp lệ của dữ liệu đầu vào
  // Người dùng có thể gửi bất kỳ số tiền nào, bao gồm số âm
}

Một cuộc tấn công có thể được thực hiện bằng cách gửi một số tiền âm vào hợp đồng thông minh. Điều này có thể khiến hợp đồng thông minh bị mất tiền.

2. Không sử dụng các ràng buộc ký

Mã sau đây cho phép bất kỳ ai ký bất kỳ giao dịch nào với hợp đồng thông minh:

function transfer(address to, uint256 amount) public {
  // Không sử dụng các ràng buộc ký
  // Bất kỳ ai cũng có thể ký bất kỳ giao dịch nào
}

Một cuộc tấn công có thể được thực hiện bằng cách ký một giao dịch chuyển tiền cho kẻ tấn công. Điều này có thể khiến nạn nhân mất tiền.

3. Không sử dụng các ràng buộc tài nguyên

Mã sau đây cho phép bất kỳ ai thực hiện bất kỳ số lần giao dịch nào với hợp đồng thông minh:

function call() public { // Không sử dụng các ràng buộc tài nguyên // Bất kỳ ai cũng có thể thực hiện bất kỳ số lần giao dịch nào với hợp đồng thông minh }

Một cuộc tấn công có thể được thực hiện bằng cách thực hiện nhiều giao dịch với hợp đồng thông minh. Điều này có thể khiến hợp đồng thông minh bị quá tải hoặc thậm chí bị lỗi.

Để tránh các cuộc tấn công này, điều quan trọng là phải kiểm tra quyền, hợp lệ của dữ liệu đầu vào, các ràng buộc ký, các ràng buộc thời gian và các ràng buộc tài nguyên khi viết mã Solidity.

Dưới đây là một số mẹo cụ thể để giúp bạn tránh các cuộc tấn công:

  • Kiểm tra quyền: Sử dụng các ràng buộc quyền để đảm bảo rằng chỉ những người dùng có quyền thích hợp mới có thể thực hiện các hành động nhất định.
  • Kiểm tra hợp lệ của dữ liệu đầu vào: Sử dụng các ràng buộc hợp lệ để đảm bảo rằng dữ liệu đầu vào hợp lệ trước khi thực hiện bất kỳ hành động nào.
  • Sử dụng các ràng buộc ký: Sử dụng các ràng buộc ký để đảm bảo rằng chỉ những người dùng đã ký giao dịch mới có thể thực hiện các hành động.
  • Sử dụng các ràng buộc tài nguyên: Sử dụng các ràng buộc tài nguyên để hạn chế số lần một hành động nhất định có thể được thực hiện.

Bằng cách tuân theo các mẹo này, bạn có thể giúp bảo vệ hợp đồng thông minh của mình khỏi các cuộc tấn công.

Để tránh các lỗi này, lập trình viên cần có kiến thức vững về Solidity, nắm vững các nguyên tắc bảo mật và tuân thủ các tiêu chuẩn phát triển an toàn như Smart Contract Best Practices từ Ethereum và OpenZeppelin. Kiểm tra và kiểm thử kỹ lưỡng cũng là những bước quan trọng để đảm bảo tính bảo mật và tin cậy của smart contract.

Vietnam Pham – Click Digital

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Click Digital
Hello! How can I help you today? ^^ Chào bạn, bạn cần gì?