Transaction Order Dependence attack
etc. ERC-20 approve race condition
TOD attack (Transaction Order Dependence attack) คือการที่ attacker สามารถหาประโยชน์จากการ frontrun transaction ของเราได้
ยกตัวอย่างเช่น
function approve ของ ERC-20
โดยปกติแล้วจะมี flow ดังนี้
0. Alice มี 10 token และมี allowance ให้ Bob เป็น 0
- Alice approve 5 token ให้ bob ทำให้ตอนนี้ Bob มี allowance จาก Alice เป็น 5 token
- Alice เปลี่ยนใจจึง approve 2 token ให้ bob แทน ทำให้ตอนนี้ Bob มี allowance จาก Alice เป็น 2 token
- Bob transferFrom 2 token นั้นไปใช้
ผลลัพธ์
- Alice เหลือ 8 token
- Bob มี 2 token
แต่ attacker สามารถโจมตีได้ด้วย flow ดังนี้
0. Alice มี 10 token และมี allowance ให้ Bob เป็น 0
- Alice approve 5 token ให้ bob ทำให้ตอนนี้ Bob มี allowance จาก Alice เป็น 5 token
- Alice เปลี่ยนใจจึงส่ง transaction เพื่อ approve 2 token ให้ bob แทน
2.5 Bob ทำการ frontrun transaction ของ Alice โดยทำการ transferFrom 5 token ที่เป็น allowance เดิมไปใช้ก่อน จากนั้นจึงทำการ submit transaction approve 2 token ของ Alice
3. Bob transferFrom อีก 2 token นั้นไปใช้
ผลลัพธ์
- Alice เหลือ 3 token
- Bob มี 7 token
จะเห็นได้ว่า Bob สามารถใช้ token มากกว่าที่ Alice ตั้งใจให้ Bob ใช้ได้
โอเค แล้วจะแก้ปัญหานี้ยังไง ?
วิธีแก้มีหลายวิธี แต่เราจะมาพูดถึงวิธีที่ใช้ increase/decrease allowance แทน approve กัน
โอเค ก่อนอื่นมาดู code approve ที่มีช่องโหว่กันก่อน
function approve(address spender, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, value);
return true;
}
เราจะเห็นได้ว่า approve ไม่สนใจค่า allowance เก่า และทำการเขียนค่า allowance ใหม่ทับไปเลย
ดังนั้น ถ้าไม่มีการ frontrun ดึงค่าเก่าออกไปใช้ก่อน
แต่ถ้ามีก็จะทำให้เกิดการ multiple spending ได้
แล้ว increaseAllowance / decreaseAllowance แก้ปัญหานี้ยังไง ?
มาดู code กัน
function decreaseAllowance(address spender, uint256 requestedDecrease) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance < requestedDecrease) {
revert ERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
}
unchecked {
_approve(owner, spender, currentAllowance - requestedDecrease);
}
return true;
}
จาก code การเปลี่ยน allowance จะไม่ได้เป็นการทับค่าเก่าไปเลย
แต่เป็นการลดค่าลง โดยอิงจากค่าเก่า
ดังนั้น ถ้ามีการ frontrun ดึงเอาค่าเก่าออกไปใช้ก่อน
แล้วค่อยปล่อยให้มีการเปลี่ยน allowance ภายหลัง
จะไม่สามารถเอาค่าที่ถอนไปตอน frontrun กลับมาใช้ได้อีก
เนื่องจากค่าเก่านั้นไม่ได้ถูกรวมในตอน decreaseAllowance นั่นเอง
โอเค มาลองดูตัวอย่างกันดีกว่า
0. Alice มี 10 token และมี allowance ให้ Bob เป็น 0
- Alice approve 5 token ให้ bob ทำให้ตอนนี้ Bob มี allowance จาก Alice เป็น 5 token
- Alice เปลี่ยนใจจึงส่ง transaction เพื่อ decrease allowance ลงมา 1 token (เสมือนว่า Alice เปลี่ยนมา approve 4 token แทน)
3. Bob ทำการ frontrun transaction ของ Alice โดยทำการ transferFrom 1 token ที่เป็น allowance เดิมไปใช้ก่อน = ทำให้ตอนนี้ Bob เหลือ allowance = 5–1 = 4 token
4. จากนั้นจึงทำการ submit transaction decrease allowance ลงมา 1 token ทำให้ตอนนี้ Bob มี allowance = 4–1 = 3 token
3. Bob transferFrom allowance ที่เหลือไปใช้ (เหลือ allowance 3 token)
ผลลัพธ์
จากการที่ Alice ทำการ decreaseAllowance จาก 5 เป็น 4 token
และ Bob พยายาม frontrun
- Alice เหลือ 6 token
- Bob เหลือ 4 token
สรุปแล้วคือ Bob ไม่สามารถหากำไรจากการ frontrun ได้แล้ว
เพราะ frontrun ไปก็ไม่ได้ token มาเพิ่มนั่นเอง