Note: Secureum security pitfalls & best practices 101 (Part 3)

Nattawat Songsom
3 min readAug 28, 2023

--

Secureum ได้ทำ list ช่องโหว่ ที่พบบ่อยใน smart contract เอาไว้ มาดูต่อกันครับ

Disclaimer:

  1. เป็นการ note ไวๆ ไม่ค่อยมี POC นะครับ
  2. เป็นบทความเพื่อการศึกษา ไม่มีเจตนาร้ายใดๆ

Block values as time proxies

การใช้ block.timestamp (เวลาปัจจุบันของ node miner) หรือ block.number (เลข block ปัจจุบัน สามารถใช้คำนวณเวลาปัจจุบันคร่าวๆได้ เนื่องจากเวลาเฉลี่ยของ block คือ 14 วินาที) ในการกำหนดเงื่อนไขใน smart contract ถือเป็น ani pattern เช่น การเขียนเช็คเงื่อนไขในแบบ code ด้านล่าง

pragma solidity ^0.5.0;

contract TimedCrowdsale {

event Finished();
event notFinished();

// Sale should finish exactly at January 1, 2019
function isSaleFinished() private returns (bool) {
return block.timestamp >= 1546300800;
}

function run() public {
if (isSaleFinished()) {
emit Finished();
} else {
emit notFinished();
}
}

}

ทำไม ?

  1. เนื่องจากสามารถถูกปั่นโดย miner ได้

โดย block.timestamp สามารถถูกแก้ให้เป็นเวลาในอนาคต ในช่วง 15 วินาทีได้

2. ถ้าเกิดการ reorganization ขึ้น อาจทำให้ block time เปลี่ยนได้ เนื่องจากมีการแยก blockchain ออกจากกัน เส้นที่เราไปอยู่อาจจะมีเงื่อนไขของแต่ละ block ที่เปลี่ยนไป

ซึ่งจะกระทบกับการคำนวณเวลาโดยกำหนด block.number จากการคาดเดาว่า block.timestamp จะประมาณ 14 วินาทีด้วยเช่นกัน

การแก้ไข

  1. ใช้เวลาจาก oracle แทน
  2. ถ้ารับได้กับเวลาที่คลาดเคลื่อนไม่เกิน 15 วินาที ได้ และมั่นใจว่า block.timestamp จะไม่คลาดเคลื่อนไปมากกว่านี้จาก block reorganization ทาง Ethereum yellow paper ก็ใช้ท่านี้ไปก่อนได้ ตามที่ yellow paper แนะนำไว้

note: pattern นี้จัดอยู่ใน SWC ด้วย

Integer overflow/underflow

ใน Solidity compiler version ก่อนหน้า 0.8.0 ไม่มีการเช็ค integer overflow/underflow ให้

ซึ่งนี่ก่อให้เกิด logic error ได้

ยกตัวอย่าง code

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

contract Token {

mapping(address => uint) balances;
uint public totalSupply;

constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}

function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}

ถ้าเรามี balanceOf อยู่ 20 เราจะเพิ่ม balance ยังไงให้มากกว่านั้น ?

คำตอบคือ transfer ให้ตัวเอง 21 เหรียญ แบบนี้

จะทำให่ balance เรากลายเป็น max uint ไปเลย

วิธีแก้

  1. ใช้ solidity compiler version 0.8.0 ขึ้นไป แต่ถ้าคิดว่าอยากลดค่าแก๊ส ก็สามารถปิด feature builtin การเช็คนี้ได้ด้วย syntax unchecked
  2. ถ้าต้องใช้ Solidity compiler version ต่ำกว่า 0.8.0 ต้องใช้ library Safemath ของ Openzeppelin
  3. ถ้าอยากเขียน safeMath เอง ให้เทียบผลลัพธ์การบวกลบคูณหารทุกครั้ง ตามภาพ

Divide before multiply

การหารก่อนการคูณจะทำให้ผลลัพธ์เพี้ยน เนื่องจากเศษการหารจะถูกตัดออก

เช่นจาก code

contract A {
function f(uint n) public {
coins = (oldSupply / n) * interest;
}
}

ถ้า

n = 10

oldSupply = 5

interest = 2

จะได้ coins = 0 เนื่องจากตอน 5/10 จะได้ 0.5 ซึ่งจะถูกปัดเป็น 0 เพราะเป็น uint

วิธีแก้

  1. นำการคูณมาทำก่อนการหาร แบบนี้
contract A {
function f(uint n) public {
coins = (oldSupply * interest) / n;
}
}

โดยในเคสนี้ถ้า

n = 10

oldSupply = 5

interest = 2

จะได้ coins = 1

ยังไงก็ตาม การทำแบบนี้ก็ยังมี case ที่เลขถูกปัดจนหายออกไปอยู่ดี เช่น

n = 10

oldSupply = 5

interest = 3

coins ควรจะได้ 1.5 แต่กลายเป็น 1 เพราะโดนปัด

ซึ่งสามารถบรรเทาปัญหานี้ได้โดยวิธีที่ 2

2. ทำให้หน่วยใหญ่ขึ้น เช่น แทนที่จะใช้หน่วยบาท เราก็ใช้หน่วยสตางค์แทน นี้จะทำให้เศษสตางค์ที่หายไปมี impact น้อยลง โดยปกติ smart contract มักจะใช้หน่วย 10¹⁸ ในการเก็บเลขกัน

note:

  1. ช่องโหว่นี้อยู่ใน SWC ด้วย
  2. Slither สามารถตรวจจับช่องโหว่นี้ได้

Referrences

--

--

No responses yet