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: 6/75
Findings: 4
Award: $3,465.78
π Selected for report: 1
π Solo Findings: 0
π Selected for report: Co0nan
Also found by: 0xWaitress, Co0nan
2118.3566 USDC - $2,118.36
a fixed amount of validator's SD collateral would be slashed even if it's collateral is slightly less than the penalty
the amount of poolThreshold.minThreshold
would be slashed anyway regardless of the difference/deficit at which the validator's collateral differ from it's penalty.
settleFunds
function settleFunds() external override { ... if (operatorShare < penaltyAmount) { ISDCollateral(staderConfig.getSDCollateral()).slashValidatorSD(validatorId, poolId); penaltyAmount = operatorShare; }
and in the slashValidatorSD
function slashValidatorSD(uint256 _validatorId, uint8 _poolId) external override nonReentrant { address operator = UtilLib.getOperatorForValidSender(_poolId, _validatorId, msg.sender, staderConfig); isPoolThresholdValid(_poolId); PoolThresholdInfo storage poolThreshold = poolThresholdbyPoolId[_poolId]; uint256 sdToSlash = convertETHToSD(poolThreshold.minThreshold); slashSD(operator, sdToSlash); }
pass the difference to slash to the slashValidatorSD function and slash accordingly (even with some penalty multiplier).
Other
#0 - Picodes
2023-06-14T18:58:13Z
It looks like this is by design
#1 - c4-judge
2023-06-14T19:01:59Z
Picodes marked the issue as duplicate of #165
#2 - c4-judge
2023-07-03T12:08:20Z
Picodes marked the issue as duplicate of #292
#3 - c4-judge
2023-07-03T12:08:35Z
Picodes marked the issue as satisfactory
π Selected for report: 0xWaitress
Also found by: Josiah, LaScaloneta, RaymondFam, T1MOH, peanuts
1115.3148 USDC - $1,115.31
https://github.com/code-423n4/2023-06-stader/blob/main/contracts/Auction.sol#L48-L50
no bidder has incentive to bid the Auction except doing last-minute MEV due to fixed endBlock
The auction of SD Token has a fixed endBlock. bidder(s) would like to get SD Token with least ETH and they are all incentivized to just bid at the last block, leading to loss of protocol principle (during the auction).
function createLot(uint256 _sdAmount) external override whenNotPaused { lots[nextLot].startBlock = block.number; lots[nextLot].endBlock = block.number + duration; lots[nextLot].sdAmount = _sdAmount;
Genearally, auction with fixed endtime has the known vulnerability of being bidden at the last block, essentially the validator/MEVer who has the ability to slip in transaction at that block has the highest likelihood to get the bid. This basically give them advantage, and would lead to the auction to end at lower price.
Extend the final endBlock at each bid. This can be activated at the end of 1h for example to ensure the highest bidder can take the auction on a fairly manner.
MEV
#0 - c4-judge
2023-06-10T14:40:01Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-06-18T14:17:55Z
manoj9april marked the issue as sponsor acknowledged
#2 - manoj9april
2023-06-18T14:20:36Z
MEV bots are welcome to be bidders.
#3 - c4-sponsor
2023-06-20T07:27:37Z
manoj9april marked the issue as sponsor disputed
#4 - Picodes
2023-07-02T11:01:45Z
This is a valid vulnerability in my opinion. MEV are of course welcome to bid but the problem is that the current auction system doesn't allow for a proper price discovery. From a game theory standpoint, no one has any interest in revealing their bid before the end of the auction, and when bidding you tend to not reveal your max bid but try to guess the "second-highest bid". Therefore the current design can quickly lead to a suboptimal final price as someone may have increased its bid given more time.
Note as well that it is currently relatively cheap to censor a transaction for a few blocks by bribing block builders. So with the current design value will may easily be lost trying to censor other bidders or bribing miners to be included.
#5 - c4-judge
2023-07-02T21:21:40Z
Picodes marked the issue as selected for report
#6 - manoj9april
2023-07-05T09:25:02Z
Yes, We agree with your point. But for now we would prefer go with current simpler approach and improve based your recommendation in later phases.
I think this should be considered as low severity.
#7 - c4-sponsor
2023-07-05T09:25:56Z
manoj9april marked the issue as sponsor acknowledged
#8 - c4-sponsor
2023-07-05T09:26:02Z
manoj9april marked the issue as disagree with severity
π 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
210.4947 USDC - $210.49
uint256 TOTAL_STAKED_ETH = staderConfig.getStakedEthPerNode(); uint256 collateralETH = getCollateralETH(_poolId); uint256 usersETH = TOTAL_STAKED_ETH - collateralETH; uint256 protocolFeeBps = getProtocolFee(_poolId); uint256 operatorFeeBps = getOperatorFee(_poolId); uint256 _userShareBeforeCommision = (_totalRewards * usersETH) / TOTAL_STAKED_ETH; protocolShare = (protocolFeeBps * _userShareBeforeCommision) / staderConfig.getTotalFee(); >>>>>can be skipped if collateralETH is 0 operatorShare = (_totalRewards * collateralETH) / TOTAL_STAKED_ETH; operatorShare += (operatorFeeBps * _userShareBeforeCommision) / staderConfig.getTotalFee(); userShare = _totalRewards - protocolShare - operatorShare;
function getQueuedValidatorCountByPool(uint8 _poolId) external view override onlyExistingPoolId(_poolId) returns (uint256) { address nodeRegistry = getNodeRegistry(_poolId); return INodeRegistry(nodeRegistry).getTotalQueuedValidatorCount(); } /// @inheritdoc IPoolUtils function getActiveValidatorCountByPool(uint8 _poolId) public view override onlyExistingPoolId(_poolId) returns (uint256) { address nodeRegistry = getNodeRegistry(_poolId); return INodeRegistry(nodeRegistry).getTotalActiveValidatorCount(); } /// @inheritdoc IPoolUtils function getSocializingPoolAddress(uint8 _poolId) public view override onlyExistingPoolId(_poolId) returns (address) { return IStaderPoolBase(poolAddressById[_poolId]).getSocializingPoolAddress(); }
function extractNonBidSD(uint256 lotId) external override { LotItem storage lotItem = lots[lotId]; if (block.number <= lotItem.endBlock) revert AuctionNotEnded(); if (lotItem.highestBidAmount > 0) revert LotWasAuctioned(); if (lotItem.sdAmount == 0) revert AlreadyClaimed(); uint256 _sdAmount = lotItem.sdAmount; lotItem.sdAmount = 0; if (!IERC20(staderConfig.getStaderToken()).transfer(staderConfig.getStaderTreasury(), _sdAmount)) { revert SDTransferFailed(); } emit UnsuccessfulSDAuctionExtracted(lotId, _sdAmount, staderConfig.getStaderTreasury()); }
function claim() external whenNotPaused { address operator = msg.sender; uint256 amount = balances[operator]; balances[operator] -= amount; address operatorRewardsAddr = UtilLib.getOperatorRewardAddress(msg.sender, staderConfig); UtilLib.sendValue(operatorRewardsAddr, amount); emit Claimed(operatorRewardsAddr, amount); }
``solidity // mapping of whitelisted permissioned node operator mapping(address => bool) public override permissionList;
7. user can only specify a full ETH as expectedAmount, but sometimes they might want to specify a decimal amount but now is not working since minEThRequiredToFinalizeRequest is dividing by 10 ** 18 ```solidity UserWithdrawInfo memory userWithdrawInfo = userWithdrawRequests[requestId]; uint256 requiredEth = userWithdrawInfo.ethExpected; uint256 lockedEthX = userWithdrawInfo.ethXAmount; uint256 minEThRequiredToFinalizeRequest = Math.min(requiredEth, (lockedEthX * exchangeRate) / DECIMALS);
type(uint256).max
. But if a new auctionContract is updated, the allowance of the old auctionContract cannot be revoked./// @notice for max approval to auction contract for spending SD tokens function maxApproveSD() external override { UtilLib.onlyManagerRole(msg.sender, staderConfig); address auctionContract = staderConfig.getAuctionContract(); UtilLib.checkNonZeroAddress(auctionContract); IERC20(staderConfig.getStaderToken()).approve(auctionContract, type(uint256).max); }
function enableSafeMode() external override { UtilLib.onlyManagerRole(msg.sender, staderConfig); safeMode = true; emit SafeModeEnabled(); } function disableSafeMode() external override onlyRole(DEFAULT_ADMIN_ROLE) { safeMode = false; emit SafeModeDisabled(); }
add a status check before setting the safeMode to the designed value.
#0 - c4-judge
2023-06-14T07:08:56Z
Picodes marked the issue as grade-a
#1 - c4-sponsor
2023-06-21T13:11:28Z
manoj9april marked the issue as sponsor acknowledged
π Selected for report: JCN
Also found by: 0x70C9, 0xSmartContract, 0xWaitress, 0xhacksmithh, DavidGiladi, K42, LaScaloneta, Rageur, Raihan, SAAJ, SAQ, SM3_SS, Sathish9098, Tomio, bigtone, c3phas, ernestognw, etherhood, koxuan, matrix_0wl, mgf15, naman1778, niser93, petrichor, piyushshukla, sebghatullah, shamsulhaq123
21.6219 USDC - $21.62
function getQueuedValidatorCountByPool(uint8 _poolId) external view override onlyExistingPoolId(_poolId) returns (uint256) { address nodeRegistry = getNodeRegistry(_poolId); return INodeRegistry(nodeRegistry).getTotalQueuedValidatorCount(); } /// @inheritdoc IPoolUtils function getActiveValidatorCountByPool(uint8 _poolId) public view override onlyExistingPoolId(_poolId) returns (uint256) { address nodeRegistry = getNodeRegistry(_poolId); return INodeRegistry(nodeRegistry).getTotalActiveValidatorCount(); } /// @inheritdoc IPoolUtils function getSocializingPoolAddress(uint8 _poolId) public view override onlyExistingPoolId(_poolId) returns (address) { return IStaderPoolBase(poolAddressById[_poolId]).getSocializingPoolAddress(); }
2.calculateMEVTHeftPenality just need a length return from rate oracle
/// @inheritdoc IPenalty function calculateMEVTheftPenalty(bytes32 _pubkeyRoot) public override returns (uint256) { // Retrieve the epochs in which the validator violated the fee recipient change rule. uint256[] memory violatedEpochs = IRatedV1(ratedOracleAddress).getViolationsForValidator(_pubkeyRoot); // each strike attracts `mevTheftPenaltyPerStrike` penalty return violatedEpochs.length * mevTheftPenaltyPerStrike; }
add a function in rateOracle that only returns the length of the violatedEpochs.
#0 - c4-judge
2023-06-14T05:58:20Z
Picodes marked the issue as grade-b