Smart contract аюулгүй байдал
Smart contract-ийн аюулгүй байдал нь онцгой чухал — deploy хийсний дараа код өөрчлөгдөхгүй тул алдаа мөнгөний алдагдалд хүргэнэ. DeFi протоколуудаас 2016–2023 онуудад хакерууд $5 тэрбум гаруй хулгайлсан.
1. Reentrancy — давтан дуудлагын халдлага
Хамгийн аюултай, алдартай халдлагын нэг. Contract гадагш ETH илгээх үед халдагч дуудлагыг дундуур тасалдуулж, дахин дуудаж болно.
// ⚠️ АЮУЛТАЙ — 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 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 0.8+ дээр автоматаар аюулгүй
uint256 x = type(uint256).max;
x += 1; // Revert хийнэ — overflow!
3. Access control — эрхийн хяналт
Чухал функцүүдийг хамгаалахгүй орхих нь томоохон алдаа.
// ⚠️ АЮУЛТАЙ — хэн ч 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 өгч өмнөөс нь гүйцэтгүүлж болно.
// ⚠️ 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-ийн аюулгүй байдлын хамгийн чухал зарчим:
// 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.
// ✅ Аюулгүй байдлын бүрэн жишээ
// 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-ийг нарийвчлан судлах — халдлагын механизм, жишээ, хамгаалалт.