bug คำนวณ reward ผิด ของ PoolTogether
มาอ่าน audit report แบบไวๆ กันครับ
โดย bug นี้เกิดขึ้นที่การแจก reward ให้กับ bot ที่มาช่วยทำการหยิบเลขสุ่มให้กับ PoolTogether
โดย PoolTogether เป็น platform lottery ดังนั้น จึงต้องหยิบเลขสุ่มหาเลขที่ชนะ
ซึ่งใน version 5 นี้ทาง PoolTogether ก็ได้เปิดให้ bot ภายนอกเข้ามาช่วยในกระบวนการสุ่มเลข
โดย task ที่ bot ต้องทำ จะประกอบไปด้วย
- ส่ง tx เพื่อ request เลขสุ่มจาก service random onchain
- เมื่อเลขสุ่มนั้นถูก 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 ให้ bot ที่ทำ task request สุ่มเลข
- ก้อน 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);
}
โดยเราจะเห็นว่า กองรางวัล จะประกอบด้วย
- reserve รางวัลใน prizePool ปัจจุบัน
- 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 อย่างคือ
- นำเลขที่สุ่มได้ไป closeDraw ที่ contract PrizePool เพื่อหาผู้ชนะ Lottery
- ให้รางวัลแก่ 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 นี้ก็ประมาณนี้ครับ