Introduction

Bài báo này tập trung vào việc xác định các design pattern thường được sử dụng để giảm thiểu các mối đe dọa bảo mật.

Background

Có thể xem Ethereum như là một transaction-based state machine bởi vì state của nó được cập nhật sau mỗi transaction.

Development Aspects

Smart contract nên được sử dụng cho các ứng dụng cần tính phi tập trung, xác thực và thực thi công khai.

Do Ethereum và Solidity phát triển rất nhanh nên developer luôn phải đối mặt với các sự chuyển đổi về tính năng và bảo mật.

Security Patterns

Sau đây là một số các security pattern được liệt kê ở trong bài báo:

Checks-Effects-Interaction

Info

Vấn đề: khi contract A gọi đến một contract B, nó sẽ bàn giao quyền kiểm soát cho contract B. Khi đó, contract B có thể gọi lại contract A để thay đổi state hoặc thay đổi luồng thực thi bằng các đoạn code độc hại.

Giải pháp: chỉ gọi đến contract bên ngoài ở bước cuối cùng trong hàm.

Pattern này gồm ba bước theo thứ tự sau:

  • Kiểm tra các điều kiện
  • Thực thi các sự thay đổi
  • Gọi đến contract khác

Ví dụ:

function auctionEnd() public {
	// 1. Checks
	require(now >= auctionEnd);
	require(!ended);
	// 2. Effects
	ended = true;
	// 3. Interaction
	beneficiary.transfer(highestBid);
}

Lý do mà lời gọi đến contract khác nên được đặt ở cuối function là để đảm bảo contract khác không thể thực thi những hành vi nguy hiểm. Cụ thể, trong reentrancy attack, contract được gọi gọi lại contract hiện tại trước khi lời gọi thực thi function trước đó hoàn thành. Việc gọi lại contract hiện tại chính là một trong số những hành vi nguy hiểm đó. Hành vi này có thể dẫn đến việc thay đổi state variable hoặc thực hiện một số hành động (chẳng hạn như gửi tiền) được thực hiện nhiều lần.

Trong ví dụ bên dưới, việc gọi đến contract bên ngoài (lệnh msg.sender.call.value(amount)()) được thực hiện trước khi gán số dư của địa chỉ msg.sender là 0. Kẻ xấu có thể viết một contract để liên tục gọi đến hàm withdrawBalance trước khi số dư được gán bằng 0 nhằm rút hết tiền từ contract. Đây chính là reentrancy attack.

mapping (address => uint) balances; 
    
function withdrawBalance() public { 
	uint amount = balances[msg.sender]; 
	require(msg.sender.call.value(amount)()); // caller's code is executed and can re-enter withdrawBalance again 
	balances[msg.sender] = 0; // INSECURE - user's balance must be reset before the external call 
}

Emergency Stop (Circuit Breaker)

Info

Vấn đề: contract sau khi deploy thì được thực thi một cách tự động ở trên Ethereum network và không có cách nào để ngừng quá trình thực thi khi có bug hoặc vấn đề bảo mật xảy ra.

Giải pháp: thêm vào cơ chế cho phép một tổ chức được xác thực vô hiệu hóa một số hàm nhạy cảm.

Một kịch bản được khuyến khích sử dụng là: khi có bug xảy ra, tất cả các hàm quan trọng đều bị ngừng hoạt động trừ hàm rút tiền.

Ví dụ:

pragma solidity ^0.8.24;
 
import "../authorization/Ownership.sol";
 
contract EmergencyStop is Owned {
    bool public contractStopped = false;
    
    modifier haltInEmergency() {
        if (!contractStopped) 
        _;
    }
    modifier enableInEmergency() {
        if (contractStopped)
        _;
    }
 
    function toggleContractStopped() public onlyOwner {
        contractStopped = !contractStopped;
    }
 
    function deposit() public payable haltInEmergency {
        // some code
    }
 
    function withdraw() public view enableInEmergency {
        // some code
    }
}

Trong ví dụ trên, state variale contractStopped cho biết contract có ngừng hoạt động hay chưa. Có hai function modifierhaltInEmergencyenableInEmergency phụ thuộc và state variable này. Cụ thể, haltInEmergency sẽ cho phép hàm deposit được thực thi bất cứ khi nào contract đang hiệu lực và enableInEmergency sẽ cho phép hàm withdraw được thực thi bất cứ khi nào contract bị vô hiệu hóa.

Việc toggle giá trị của biến contractStopped chỉ có thể được thực hiện bởi chủ sở hữu contract. Điều này được thể hiện thông qua modifier onlyOwner.

Speed Bump

Info

Vấn đề: việc một lượng lớn các tổ chức thực thi các tác vụ nhạy cảm tại cùng một thời điểm có thể làm smart contract bị chậm (tương tự với DDoS).

Giải pháp: kéo dài thời gian thực thi các tác vụ nhạy cảm.

Việc kéo dài thời gian giúp chúng ta có nhiều thời gian hơn để đối phó với cuộc tấn công. Một ví dụ trong đời sống là “bank run”. Đây là một hiện tượng xảy ra khi có một lượng lớn các người dùng rút tiền khỏi ngân hàng trong cùng một thời điểm bởi vì sự quan ngại về khả năng thanh toán của ngân hàng. Ngân hàng thường đối phó bằng cách trì hoãn, dừng lại hoặc giới hạn lượng tiền rút ra.

Ví dụ:

contract SpeedBump {
    struct Withdrawal {
        uint256 amount;
        uint256 requestedAt;
    }
    mapping(address => uint256) private balances;
    mapping(address => Withdrawal) private withdrawals;
    uint256 constant WAIT_PERIOD = 7 days;
 
    function deposit() public payable {
        if (!(withdrawals[msg.sender].amount > 0))
            balances[msg.sender] += msg.value;
    }
 
    function requestWithdrawal() public {
        if (balances[msg.sender] > 0) {
            uint256 amountToWithdraw = balances[msg.sender];
            balances[msg.sender] = 0;
            withdrawals[msg.sender] = Withdrawal({
                amount: amountToWithdraw,
                requestedAt: now
            });
        }
    }
 
    function withdraw() public {
        if (
            withdrawals[msg.sender].amount > 0 &&
            now > withdrawals[msg.sender].requestedAt + WAIT_PERIOD
        ) {
            uint256 amount = withdrawals[msg.sender].amount;
            withdrawals[msg.sender].amount = 0;
            msg.sender.transfer(amount);
        }
    }
}

Phân tích ví dụ trên:

  • Struct Withdrawal được dùng để thể hiện một yêu cầu rút tiền.
  • Có hai mapping là balances (thể hiện cho số dư của các người dùng) và withdrawals (thể hiện cho các yêu cầu rút tiền của các người dùng).
  • Có một hằng số là WAIT_PERIOD với giá trị là 7 ngày. Đây chính là khoảng thời gian delay giữa hai lần rút tiền.
  • Hàm deposit cho phép gửi tiền vào contract khi người dùng đang không có yêu cầu rút tiền nào.
  • Hàm requestWithdrawal cho phép tạo yêu cầu rút tiền với điều kiện là số dư của người dùng phải lớn hơn 0. Nếu điều kiện được thỏa mãn, số dư của người dùng sẽ được gán bằng 0 và sẽ có một yêu cầu rút tiền tương ứng với tài khoản của người dùng được tạo ra.
  • Hàm withdraw cho phép rút tiền nếu người dùng có yêu cầu rút tiền và đã đến thời điểm rút tiền (sau thời điểm yêu cầu rút tiền 7 ngày). Nếu điều kiện được thỏa mãn, lượng tiền trong yêu cầu rút tiền sẽ được gán bằng 0 và người dùng sẽ nhận được lượng tiền mà họ mong muốn rút.

Rate Limit

Info

Vấn đề: việc thực thi một hàm nào đó quá nhiều lần có thể làm giảm hiệu năng của smart contract.

Giải pháp: quy định tần suất mà một hàm có thể được thực thi.

Ví dụ:

contract RateLimit {
    uint256 enabledAt = now;
    
    modifier enabledEvery(uint256 t) {
        if (now >= enabledAt) {
            enabledAt = now + t;
            _;
        }
    }
 
    function f() public enabledEvery(1 minutes) {
        // some code
    }
}

Trong ví dụ trên, modifier enabledEvery giúp giới hạn lại việc thực thi của một hàm bất kỳ với tần suất là 1 lần thực thi mỗi 1 phút.

Mutex

Info

Vấn đề: các reentrancy attack có thể thao túng state của contract và chiếm quyền kiểm soát của luồng thực thi.

Giải pháp: sử dụng mutex để ngăn chặn việc re-entering của contract bên ngoài.

Mutex là một cơ chế đồng bộ nhằm giới hạn lại các truy cập đồng thời vào tài nguyên.

Ví dụ áp dụng mutex:

contract Mutex {
    bool locked;
    
    modifier noReentrancy() {
        require(!locked);
        locked = true;
        _;
        locked = false;
    }
 
    // f is protected by a mutex, thus reentrant calls
    // from within msg.sender.call cannot call f again
    function f() public noReentrancy returns (uint256) {
        require(msg.sender.call());
        return 1;
    }
}

Modifier noReentrancy ở trong ví dụ trên giúp đảm bảo một hàm bất kỳ sẽ không được gọi lại khi nó chưa được thực thi xong bằng cách sử dụng cờ locked.

Balance Limit

Info

Vấn đề: contract sẽ luôn có rủi ro bị tấn công thông qua một bug hoặc một vấn đề nào đó của platform. Điều này có thể gây ra thất thoát tiền được lưu trữ trong contract.

Giải pháp: giới hạn lại lượng tiền tối đa mà một contract nắm giữ.

Hiện thực pattern này bằng cách từ chối các lệnh chuyển tiền vào contract khi lượng tiền mà contract đang nắm giữ vượt quá một hạn ngạch nào đó. Cách tiếp cận này có thể không ngăn chặn được các lệnh chuyển tiền ép buộc chẳng hạn như lời gọi hàm selfdestruct(address) hoặc nhận tiền reward tự việc mining.

Ví dụ:

contract LimitBalance {
    uint256 public limit;
 
    function LimitBalance(uint256 value) public {
        limit = value;
    }
 
    modifier limitedPayable() {
        require(this.balance <= limit);
        _;
    }
 
    function deposit() public payable limitedPayable {
        // some code
    }
}

Trong ví dụ trên, modifier limitedPayable sẽ được áp dụng cho các hàm có modifier là payable để đảm bảo rằng số dư của contract không vượt quá giá trị của limit.

list
from outgoing([[Smart Contracts - Security Patterns in the Ethereum Ecosystem and Solidity]])
sort file.ctime asc

Resources