Platform: Code4rena
Start Date: 02/06/2023
Pot Size: $100,000 USDC
Total HM: 15
Participants: 75
Period: 7 days
Judge: Picodes
Total Solo HM: 5
Id: 249
League: ETH
Rank: 17/75
Findings: 4
Award: $1,836.70
🌟 Selected for report: 0
🚀 Solo Findings: 0
102.2712 USDC - $102.27
https://github.com/code-423n4/2023-06-stader/blob/main/contracts/Auction.sol#L14 https://github.com/code-423n4/2023-06-stader/blob/main/contracts/OperatorRewardsCollector.sol#L12 https://github.com/code-423n4/2023-06-stader/blob/main/contracts/SocializingPool.sol#L17 https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderOracle.sol#L17
Referenced contracts are unable to be paused as intended
Referenced contracts inherit PausableUpgradeable and implement the whenNotPaused modifier, but don't implement any method to actually pause or unpause the contract. PausableUpgradeable.sol only implements internal functions, which requires external or public functions to be implemented to wrap them. Since nothing like this has been implemented, the entire pausing system is rendered useless.
Manual review
Create simple external pause and unpause functions that can be called by owner
Access Control
#0 - c4-judge
2023-06-10T10:44:59Z
Picodes marked the issue as duplicate of #383
#1 - c4-judge
2023-07-02T09:44:27Z
Picodes marked the issue as satisfactory
857.9344 USDC - $857.93
Admin will never be able to update pool address
function updatePoolAddress(uint8 _poolId, address _newPoolAddress) external override onlyExistingPoolId(_poolId) onlyRole(DEFAULT_ADMIN_ROLE) { UtilLib.checkNonZeroAddress(_newPoolAddress); verifyNewPool(_poolId, _newPoolAddress); poolAddressById[_poolId] = _newPoolAddress; emit PoolAddressUpdated(_poolId, _newPoolAddress); }
The function has two conflicting checks: modifier onlyExistingPoolId
and function verifyNewPool
. In first it requires isExistingPoolId()
to return true, in second it requires to return false:
modifier onlyExistingPoolId(uint8 _poolId) { if (!isExistingPoolId(_poolId)) { revert PoolIdNotPresent(); } _; } function verifyNewPool(uint8 _poolId, address _poolAddress) internal view { if ( INodeRegistry(IStaderPoolBase(_poolAddress).getNodeRegistry()).POOL_ID() != _poolId || isExistingPoolId(_poolId) ) { revert ExistingOrMismatchingPoolId(); } }
Manual Review
Remove call to function verifyNewPool()
in updatePoolAddress()
Invalid Validation
#0 - c4-judge
2023-06-10T14:22:11Z
Picodes marked the issue as duplicate of #341
#1 - c4-judge
2023-07-02T10:03:57Z
Picodes marked the issue as satisfactory
🌟 Selected for report: 0xWaitress
Also found by: Josiah, LaScaloneta, RaymondFam, T1MOH, peanuts
857.9344 USDC - $857.93
The current implementation does not encourage bidders to submit bids at any time other than the very last second. It results in
The purpose of the auction is to evaluate the asset and sell it at a fair price. But now it's a gas war to put your transaction as the last one in the block.
As you can see it does not extend lotItem.endBlock
if bid was submitted too close to the end
function addBid(uint256 lotId) external payable override whenNotPaused { // reject payments of 0 ETH if (msg.value == 0) revert InSufficientETH(); LotItem storage lotItem = lots[lotId]; if (block.number > lotItem.endBlock) revert AuctionEnded(); uint256 totalUserBid = lotItem.bids[msg.sender] + msg.value; if (totalUserBid < lotItem.highestBidAmount + bidIncrement) revert InSufficientBid(); lotItem.highestBidder = msg.sender; lotItem.highestBidAmount = totalUserBid; lotItem.bids[msg.sender] = totalUserBid; emit BidPlaced(lotId, msg.sender, totalUserBid); }
Manual Review
For example extend auction by 30 minutes if the bid was submitted <30 minutes before the end.
Make sure to refactor bidIncrement
logic, because in current implementation it is constant 0.005ETH, therefore creates issue of infinite extending of auction at low cost (4 days for 1 ETH).
MEV
#0 - c4-judge
2023-06-10T14:40:09Z
Picodes marked the issue as duplicate of #70
#1 - c4-judge
2023-07-03T13:24:51Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-07-03T13:32:06Z
Picodes changed the severity to 2 (Med Risk)
🌟 Selected for report: RaymondFam
Also found by: 0xWaitress, 0xhacksmithh, ChrisTina, DadeKuma, LaScaloneta, Rolezn, SAAJ, Sathish9098, T1MOH, bin2chen, btk, catellatech, ernestognw, fatherOfBlocks, hals, hunter_w3b, jaraxxus, matrix_0wl, mgf15, naman1778, niser93, solsaver, turvy_fuzz
18.5651 USDC - $18.57
Remove the check that the auction has ended. It blocks users' funds and doesn't make any sense
function withdrawUnselectedBid(uint256 lotId) external override nonReentrant { LotItem storage lotItem = lots[lotId]; if (block.number <= lotItem.endBlock) revert AuctionNotEnded(); //@audit if (msg.sender == lotItem.highestBidder) revert BidWasSuccessful(); ... }
createLot()
that msg.sender is SDCollateral.solAdd this check to prevent users from interacting with system contract and following losing of SD tokens. Because ETH and SD tokens will be transferred to StaderTreasury and StaderStakePoolsManager.sol
function createLot(uint256 _sdAmount) external override whenNotPaused { //SOLUTION require(msg.sender == staderConfig.getSDCollateral()); lots[nextLot].startBlock = block.number; lots[nextLot].endBlock = block.number + duration; lots[nextLot].sdAmount = _sdAmount; LotItem storage lotItem = lots[nextLot]; if (!IERC20(staderConfig.getStaderToken()).transferFrom(msg.sender, address(this), _sdAmount)) { revert SDTransferFailed(); } emit LotCreated(nextLot, lotItem.sdAmount, lotItem.startBlock, lotItem.endBlock, bidIncrement); nextLot++; }
#0 - c4-judge
2023-06-14T07:03:33Z
Picodes marked the issue as grade-b