Platform: Code4rena
Start Date: 02/08/2023
Pot Size: $42,000 USDC
Total HM: 13
Participants: 45
Period: 5 days
Judge: hickuphh3
Total Solo HM: 5
Id: 271
League: ETH
Rank: 22/45
Findings: 1
Award: $116.52
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: Aymen0909
Also found by: 0xStalin, 0xbepresent, Arz, D_Auditor, Jorgect, T1MOH, bin2chen, dirk_y, josephdara, ptsanev, rvierdiiev, seerether, shirochan, trachev
116.5185 USDC - $116.52
The rngComplete
function is supposed to be called by the relayer to complete the Rng relay auction and send auction rewards to the recipient, but because the function doesn't have any access control it can be called by anyone, an attacker can call the function before the relayer and give a different _rewardRecipient
and thus he can collect all the rewards and the true auction reward recipient will not get any.
The issue occurs in the rngComplete
function below :
function rngComplete( uint256 _randomNumber, uint256 _rngCompletedAt, address _rewardRecipient, // @audit can set any address uint32 _sequenceId, AuctionResult calldata _rngAuctionResult ) external returns (bytes32) { // @audit should only be callable by rngAuctionRelayer if (_sequenceHasCompleted(_sequenceId)) revert SequenceAlreadyCompleted(); uint64 _auctionElapsedSeconds = uint64( block.timestamp < _rngCompletedAt ? 0 : block.timestamp - _rngCompletedAt ); if (_auctionElapsedSeconds > (_auctionDurationSeconds - 1)) revert AuctionExpired(); // Calculate the reward fraction and set the draw auction results UD2x18 rewardFraction = _fractionalReward(_auctionElapsedSeconds); _auctionResults.rewardFraction = rewardFraction; _auctionResults.recipient = _rewardRecipient; _lastSequenceId = _sequenceId; AuctionResult[] memory auctionResults = new AuctionResult[](2); auctionResults[0] = _rngAuctionResult; auctionResults[1] = AuctionResult({ rewardFraction: rewardFraction, recipient: _rewardRecipient }); uint32 drawId = prizePool.closeDraw(_randomNumber); uint256 futureReserve = prizePool.reserve() + prizePool.reserveForOpenDraw(); uint256[] memory _rewards = RewardLib.rewards(auctionResults, futureReserve); emit RngSequenceCompleted( _sequenceId, drawId, _rewardRecipient, _auctionElapsedSeconds, rewardFraction ); for (uint8 i = 0; i < _rewards.length; i++) { uint104 _reward = uint104(_rewards[i]); if (_reward > 0) { prizePool.withdrawReserve(auctionResults[i].recipient, _reward); emit AuctionRewardDistributed(_sequenceId, auctionResults[i].recipient, i, _reward); } } return bytes32(uint(drawId)); }
As we can see the function does not have any access control (modifier or check on the msg.sender), so any user can call it and you can also notice that the _rewardRecipient
(the address that receives the rewards) is given as argument to the function and there is no check to verify that it is the correct auction reward receiver.
Hence an attacker can call the function before the relayer does, he can thus complete the auction and give another address for _rewardRecipient
which will receive all the rewards.
The result is in the end that the true auction reward recipient will get his reward stolen by other users.
Manual review
Add a check in the rngComplete
function to make sure that only the relayer can call it, the function can be modified as follows :
function rngComplete( uint256 _randomNumber, uint256 _rngCompletedAt, address _rewardRecipient, uint32 _sequenceId, AuctionResult calldata _rngAuctionResult ) external returns (bytes32) { // @audit only called by rngAuctionRelayer if (msg.sender != rngAuctionRelayer) revert NotRelayer(); ... }
Access Control
#0 - c4-pre-sort
2023-08-08T02:29:24Z
raymondfam marked the issue as primary issue
#1 - c4-pre-sort
2023-08-08T05:43:13Z
raymondfam marked the issue as high quality report
#2 - c4-sponsor
2023-08-10T19:21:11Z
asselstine marked the issue as sponsor confirmed
#3 - c4-judge
2023-08-14T02:44:19Z
HickupHH3 marked the issue as selected for report