Solidity / Smart contract аюулгүй байдал

Smart contract аюулгүй байдал

Smart contract-ийн аюулгүй байдал нь онцгой чухал — deploy хийсний дараа код өөрчлөгдөхгүй тул алдаа мөнгөний алдагдалд хүргэнэ. DeFi протоколуудаас 2016–2023 онуудад хакерууд $5 тэрбум гаруй хулгайлсан.

1. Reentrancy — давтан дуудлагын халдлага

Хамгийн аюултай, алдартай халдлагын нэг. Contract гадагш ETH илгээх үед халдагч дуудлагыг дундуур тасалдуулж, дахин дуудаж болно.

solidity
// ⚠️ АЮУЛТАЙ — reentrancy-д өртөмтгий
contract VulnerableBank {
    mapping(address => uint256) public balances;

    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0);

        // ❌ Эхлээд ETH илгээнэ — халдагч энд тасалдуулна
        (bool success,) = msg.sender.call{value: amount}("");
        require(success);

        balances[msg.sender] = 0; // Хэт оройтсон!
    }
}

Засах арга: Checks-Effects-Interactions загвар (доор дэлгэрэнгүй).

2. Integer overflow ба underflow

Solidity 0.8-аас өмнө дугаарын хязгаараас хэтрэх нь алдаа гаргадаггүй байсан:

solidity
// Solidity 0.7 ба түүнээс өмнө
uint8 x = 255;
x += 1; // x = 0 болдог байсан! (overflow)

uint8 y = 0;
y -= 1; // y = 255 болдог байсан! (underflow)

Solidity 0.8+: Автоматаар шалгаж revert хийдэг тул аюулгүй. Хуучин код ашиглаж байгаа бол SafeMath library хэрэглэнэ.

solidity
// Solidity 0.8+ дээр автоматаар аюулгүй
uint256 x = type(uint256).max;
x += 1; // Revert хийнэ — overflow!

3. Access control — эрхийн хяналт

Чухал функцүүдийг хамгаалахгүй орхих нь томоохон алдаа.

solidity
// ⚠️ АЮУЛТАЙ — хэн ч mint хийж болно
contract BadToken {
    mapping(address => uint256) public balances;

    function mint(address to, uint256 amount) public {
        // ❌ Хандалтын хяналт байхгүй!
        balances[to] += amount;
    }
}

// ✅ ЗӨВ — зөвхөн эзэмшигч mint хийнэ
contract GoodToken {
    mapping(address => uint256) public balances;
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Зөвхөн эзэмшигч");
        _;
    }

    function mint(address to, uint256 amount) public onlyOwner {
        balances[to] += amount;
    }
}

OpenZeppelin-ийн Ownable болон AccessControl contract-уудыг ашиглахыг зөвлөнө.

4. Front-running — гүйлгээний дарааллын халдлага

Ethereum дээр гүйлгээ mempool-д орсны дараа бүх хүн харж болно. Халдагч өндөр gas fee өгч өмнөөс нь гүйцэтгүүлж болно.

solidity
// ⚠️ Front-running-д өртөмтгий — шагналын тоглоом
contract GuessGame {
    bytes32 public secretHash;

    function guess(string memory answer) public {
        // Халдагч хариуг mempool-д харж, өндөр gas-тай дахин илгээнэ
        if (keccak256(abi.encodePacked(answer)) == secretHash) {
            payable(msg.sender).transfer(address(this).balance);
        }
    }
}

Хамгаалах арга: Commit-reveal схем — эхлээд hash илгээж, дараа нь нээнэ.

Checks-Effects-Interactions загвар

Smart contract-ийн аюулгүй байдлын хамгийн чухал зарчим:

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

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

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

    function withdraw(uint256 amount) public {
        // ✅ 1. CHECKS — бүх нөхцөл шалгах
        require(amount > 0, "Дүн 0-ээс их байх ёстой");
        require(balances[msg.sender] >= amount, "Үлдэгдэл хүрэлцэхгүй");

        // ✅ 2. EFFECTS — төлөв өөрчлөх (гадагш дуудлагаас өмнө!)
        balances[msg.sender] -= amount;

        // ✅ 3. INTERACTIONS — гадагш дуудлага (хамгийн сүүлд)
        (bool success,) = payable(msg.sender).call{value: amount}("");
        require(success, "ETH илгээхэд алдаа");
    }
}

Яагаад энэ дараалал чухал вэ?

Effects (төлөв өөрчлөлт) нь Interactions (гадаад дуудлага)-аас өмнө байвал халдагч дахин дуудлага хийхэд балансыг аль хэдийн 0 болсон байна — хамгаалагдсан.

Аюулгүй байдлын шилдэг туршлагууд

OpenZeppelin ашиглах: Audit хийгдсэн, баталгаажсан хэрэгжүүлэлт ашиглах нь аюулгүй.

Modifier ашиглах: Давтагдах шалгалтыг modifier болгох.

Үйл явдал бүртгэх: Бүх чухал үйлдлийг emit хийж, audit trail үүсгэх.

Тест бичих: Hardhat эсвэл Foundry ашиглан бүрэн тест бичих.

Audit хийлгэх: Үнэ цэнтэй contract-уудыг мэргэжлийн аудитад өгөх.

Аажим deploy хийх: Эхлээд testnet, дараа нь mainnet.

solidity
// ✅ Аюулгүй байдлын бүрэн жишээ
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SecureVault is ReentrancyGuard, Ownable {
    mapping(address => uint256) public balances;
    uint256 public constant MAX_DEPOSIT = 100 ether;

    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);

    constructor() Ownable(msg.sender) {}

    function deposit() public payable nonReentrant {
        require(msg.value > 0, "Дүн 0-ээс их байх ёстой");
        require(msg.value <= MAX_DEPOSIT, "Дээд хэмжээнээс хэтэрлээ");

        balances[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) public nonReentrant {
        // Checks
        require(amount > 0, "Дүн 0-ээс их байх ёстой");
        require(balances[msg.sender] >= amount, "Үлдэгдэл хүрэлцэхгүй");

        // Effects
        balances[msg.sender] -= amount;
        emit Withdrawn(msg.sender, amount);

        // Interactions
        (bool success,) = payable(msg.sender).call{value: amount}("");
        require(success, "ETH илгээхэд алдаа");
    }
}

Дараагийн хичээлд:

Reentrancy attack-ийг нарийвчлан судлах — халдлагын механизм, жишээ, хамгаалалт.