bug คำนวณ reward ผิด ของ PoolTogether

Nattawat Songsom
3 min readSep 21, 2023

--

มาอ่าน audit report แบบไวๆ กันครับ

โดย bug นี้เกิดขึ้นที่การแจก reward ให้กับ bot ที่มาช่วยทำการหยิบเลขสุ่มให้กับ PoolTogether

โดย PoolTogether เป็น platform lottery ดังนั้น จึงต้องหยิบเลขสุ่มหาเลขที่ชนะ

ซึ่งใน version 5 นี้ทาง PoolTogether ก็ได้เปิดให้ bot ภายนอกเข้ามาช่วยในกระบวนการสุ่มเลข

โดย task ที่ bot ต้องทำ จะประกอบไปด้วย

  1. ส่ง tx เพื่อ request เลขสุ่มจาก service random onchain
  2. เมื่อเลขสุ่มนั้นถูก fulfill แล้ว ตัว bot จะต้องยิงอีก tx เพื่อ bridge เลขที่สุ่มมาไปให้ smart contract ที่ทำการหยิบ lottery บน L2

โดยเป็นไปตามภาพ

โอเค ซึ่ง bot ที่ทำข้อ 1 (ส่ง tx เพื่อ request เลขสุ่มจาก service random onchain หรือก็คือเรียก function startRngRequest)

และข้อ 2 (เมื่อเลขสุ่มนั้นถูก fulfill แล้ว ตัว bot จะต้องยิงอีก tx เพื่อ bridge เลขที่สุ่มมาไปให้ smart contract ที่ทำการหยิบ lottery บน L2 หรือก็คือ เรียก function relay) เนี่ย

ไม่จำเป็นต้องเป็นคนเดียวกัน

และ ทาง PrizePool จะทำการให้รางวัลแยกเป็น 2 ก้อน คือ

  1. ก้อน 1 ให้ bot ที่ทำ task request สุ่มเลข
  2. ก้อน 2 ให้ bot ที่ทำ task ทำเรื่องส่งเลขมายัง L2

แยกกัน

ยังไงก็ตาม bot จะรู้ได้ยังไงว่าจะได้ reward เท่าไหร่ ถ้าทำ task สำเร็จ

โดยทาง RngRelayAuction จะมี function ให้ bot ดู reward ที่จะได้ ถ้าทำ task สำเร็จตาม code

  /// @notice Computes the actual rewards that will be distributed to the recipients using the current Prize Pool reserve.
/// @param __auctionResults The auction results to use for calculation
/// @return rewards The rewards that will be distributed
function computeRewards(AuctionResult[] calldata __auctionResults) external returns (uint256[] memory) {
uint256 totalReserve = prizePool.reserve() + prizePool.reserveForOpenDraw();
return _computeRewards(__auctionResults, totalReserve);
}

โดยเราจะเห็นว่า กองรางวัล จะประกอบด้วย

  1. reserve รางวัลใน prizePool ปัจจุบัน
  2. reserve รางวัลใน prizePool ของรอบนี้ ที่ยังไม่ได้ไปรวมกับ reserve กองปัจจุบันในข้อ 1

โดยตัว prizePool จะมีกลไกในการนำ token มาเติมใน reserve เรื่อยๆ เป็นรอบๆ ทำให้ตอนคิดรางวัล ต้องเอาก้อนที่จะเอามารวมในรอบนี้มาคิดด้วยนั่นเอง

ดังที่เห็นใน code ของ function reserveForOpenDraw ของ contract PrizePool จะมีการหา computeNewDistribution ของรอบปัจจุบัน นั่นเอง

  /// @notice Returns the amount of tokens that will be added to the reserve when the open draw closes.
/// @dev Intended for Draw manager to use after the draw has ended but not yet been closed.
/// @return The amount of prize tokens that will be added to the reserve
function reserveForOpenDraw() external view returns (uint256) {
uint8 _numTiers = numberOfTiers;
uint8 _nextNumberOfTiers = _numTiers;

if (lastClosedDrawId != 0) {
_nextNumberOfTiers = _computeNextNumberOfTiers(_numTiers);
}

(, uint104 newReserve, ) = _computeNewDistributions(
_numTiers,
_nextNumberOfTiers,
uint96(_contributionsForDraw(lastClosedDrawId + 1))
);

return newReserve;
}

เอาละ นี่ไม่ใช่จุดที่ bug

ถ้าเรามาดู code ของ function rngComplete ของ contract RngRelayAuction ที่มี หน้าที่ 2 อย่างคือ

  1. นำเลขที่สุ่มได้ไป closeDraw ที่ contract PrizePool เพื่อหาผู้ชนะ Lottery
  2. ให้รางวัลแก่ bot ที่มาช่วยในการสุ่มเลขครั้งนี้

ตาม code

uint32 drawId = prizePool.closeDraw(_randomNumber);

uint256 futureReserve = prizePool.reserve() + prizePool.reserveForOpenDraw();
uint256[] memory _rewards = RewardLib.rewards(auctionResults, futureReserve);

โอเค มาเริ่มกันที่บรรทัดแรก คือการไปเรียก function closeDraw ไปดู code ของ closeDraw กัน

เราจะพบว่าตอน closeDraw มีการรวม reserve ของรางวัลในรอบนี้ เข้าสู่ reserve กองกลาง

    uint8 numTiers = numberOfTiers;
UD60x18 _prizeTokenPerShare = fromUD34x4toUD60x18(prizeTokenPerShare);
(
uint16 closedDrawId,
uint104 newReserve,
UD60x18 newPrizeTokenPerShare
) = _computeNewDistributions(
numTiers,
_nextNumberOfTiers,
_prizeTokenPerShare,
_prizeTokenLiquidity
);
...
_reserve += newReserve;

แต่เดี๋ยวนะ ใน rngComplete ยังคิดรางวัลแบบ function computeReward อยู่เลย (คิดรางวัลในเคสที่ reserve รางวัลรอบนี้ ยังไม่ไปรวมกับ reserve กองกลาง)

uint256 futureReserve = prizePool.reserve() + prizePool.reserveForOpenDraw();
uint256[] memory _rewards = RewardLib.rewards(auctionResults, futureReserve);

แสดงว่าตอนนี้ reserveForOpenDraw ที่ rngComplete เอามาใช้ เป็น รางวัลของรอบถัดไป ไปซะแล้ว (เพราะรอบปัจจุบัน เราเคลียร์ไปแล้วใน closeDraw)

โดยวิธีแก้ก็คือ การเอาแค่ reserve กองกลางมาเป็น reward ได้เลย เพราะรางวัลทั้งหมดของรอบนี้ ถูกรวมยอดกันในตอน closeDraw แล้ว

diff --git a/src/RngRelayAuction.sol b/src/RngRelayAuction.sol
index 8085169..cf3c210 100644
--- a/src/RngRelayAuction.sol
+++ b/src/RngRelayAuction.sol
@@ -153,8 +153,8 @@ contract RngRelayAuction is IRngAuctionRelayListener, IAuction {

uint32 drawId = prizePool.closeDraw(_randomNumber);

- uint256 futureReserve = prizePool.reserve() + prizePool.reserveForOpenDraw();
- uint256[] memory _rewards = RewardLib.rewards(auctionResults, futureReserve);
+ uint256 reserve = prizePool.reserve();
+ uint256[] memory _rewards = RewardLib.rewards(auctionResults, reserve);

emit RngSequenceCompleted(
_sequenceId,

โอเค bug นี้ก็ประมาณนี้ครับ

Referrences

--

--

No responses yet