ETH илгээх (send, transfer, call)
Contract-аас ETH илгээх гурван арга байдаг: transfer, send, call. Тус бүр өөрийн давуу болон сул талтай. Орчин үеийн Solidity-д call ашиглахыг зөвлөдөг.
transfer — хялбар боловч хязгаартай
payable(recipient).transfer(amount);
- Gas хязгаар: 2300 gas (зөвхөн үйл явдал бичихэд хүрэлцэнэ)
- Амжилтгүй бол автоматаар revert хийнэ
- Return утга байхгүй
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TransferExample {
function sendWithTransfer(address payable recipient) public payable {
require(msg.value > 0, "ETH илгээх шаардлагатай");
// Амжилтгүй бол автоматаар буцаагдана
recipient.transfer(msg.value);
}
}
Сул тал: Хүлээн авагч contract нь receive() функцдоо нарийн логик байвал 2300 gas хүрэлцэхгүй бөгөөд гүйлгээ бүтэлгүйтэнэ.
send — гар аргаар шалгах шаардлагатай
bool success = payable(recipient).send(amount);
- Gas хязгаар: 2300 gas (
transfer-тэй адил) - Амжилтгүй бол
falseбуцаана — revert хийхгүй - Return утгыг заавал шалгах ёстой
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SendExample {
function sendWithSend(address payable recipient) public payable {
require(msg.value > 0, "ETH илгээх шаардлагатай");
bool success = recipient.send(msg.value);
require(success, "ETH илгээхэд алдаа гарлаа"); // Заавал шалгана
}
}
Сул тал: transfer-тэй ижил 2300 gas хязгаартай, гэхдээ алдааг гар аргаар шалгах шаардлагатай тул алдаа гаргахад хялбар.
call — зөвлөмж болгох арга
(bool success, bytes memory data) = payable(recipient).call{value: amount}("");
- Gas хязгаар: бүх боломжит gas дамжуулна
- Return утга:
(bool success, bytes memory data) - Хамгийн уян хатан, орчин үеийн арга
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CallExample {
function sendWithCall(address payable recipient) public payable {
require(msg.value > 0, "ETH илгээх шаардлагатай");
(bool success, ) = recipient.call{value: msg.value}("");
require(success, "ETH илгээхэд алдаа гарлаа");
}
}
Яагаад call ашиглах нь дээр вэ?
2019 оны Istanbul hard fork-оор gas зардал өөрчлөгдсөний дараа transfer болон send-ийн 2300 gas хязгаар хүрэлцэхгүй тохиолдол гарч эхэлсэн. Тиймээс Ethereum Foundation call ашиглахыг зөвлөж байна.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SafeWallet {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Үлдэгдэл хүрэлцэхгүй");
// ✅ Checks-Effects-Interactions загвар
// 1. Checks — нөхцөл шалгах (дээр хийсэн)
// 2. Effects — төлөв өөрчлөх
balances[msg.sender] -= amount;
// 3. Interactions — гадаад дуудлага
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "ETH илгээхэд алдаа гарлаа");
}
}
Гурван аргын харьцуулалт
| | transfer | send | call |
| ----------------- | ---------- | ------------- | ------------- |
| Gas хязгаар | 2300 | 2300 | Бүгд |
| Амжилтгүй бол | Revert | false буцаана | false буцаана |
| Зөвлөмж | ❌ | ❌ | ✅ |
| Reentrancy эрсдэл | Бага | Бага | Өндөр* |
*call ашиглахдаа Checks-Effects-Interactions загварыг заавал дагах ёстой.
Reentrancy — анхаарах эрсдэл
call ашиглахад хүлээн авагч contract нь дуудлагын дундуур таны contract руу дахин дуудлага хийж болно. Үүнийг reentrancy attack гэнэ.
// ⚠️ АЮУЛТАЙ — reentrancy-д өртөмтгий жишээ
contract VulnerableWallet {
mapping(address => uint256) public balances;
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0, "Үлдэгдэл байхгүй");
// ❌ Муу дараалал: эхлээд ETH илгээж, дараа нь тооцоо шинэчилнэ
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Илгээхэд алдаа");
balances[msg.sender] = 0; // Хэт оройтсон!
}
}
Засах арга: төлөв өөрчлөлтийг гадаад дуудлагаас өмнө хийх. Reentrancy attack-ийг дэлгэрэнгүй 26-р хичээлд судална.
Дараагийн хичээлд:
ERC-20 token стандартыг танилцуулна.