diff --git a/src/AuctionManagerGasOptimized.sol b/src/AuctionManagerGasOptimized.sol new file mode 100644 index 000000000..e21d81517 --- /dev/null +++ b/src/AuctionManagerGasOptimized.sol @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "./interfaces/IAuctionManager.sol"; +import "./interfaces/INodeOperatorManager.sol"; +import "./interfaces/IProtocolRevenueManager.sol"; +import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +contract AuctionManagerGasOptimized is + Initializable, + IAuctionManager, + PausableUpgradeable, + OwnableUpgradeable, + ReentrancyGuardUpgradeable, + UUPSUpgradeable +{ + //-------------------------------------------------------------------------------------- + //--------------------------------- STATE-VARIABLES ---------------------------------- + //-------------------------------------------------------------------------------------- + + uint128 public whitelistBidAmount; + uint64 public minBidAmount; + uint64 public maxBidAmount; + uint256 public numberOfBids; + uint256 public numberOfActiveBids; + + INodeOperatorManager public nodeOperatorManager; + IProtocolRevenueManager public DEPRECATED_protocolRevenueManager; + + address public stakingManagerContractAddress; + bool public whitelistEnabled; + + mapping(uint256 => Bid) public bids; + + address public DEPRECATED_admin; + + // new state variables for phase 2 + address public membershipManagerContractAddress; + uint128 public accumulatedRevenue; + uint128 public accumulatedRevenueThreshold; + + mapping(address => bool) public admins; + + uint256 public bidIdsBeforeGasOptimization; + mapping(uint256 bidIndex => BatchedBid bids) public batchedBids; + mapping(uint256 bidIndex => address operator) public operatorBidIndexMap; + + //-------------------------------------------------------------------------------------- + //------------------------------------- EVENTS --------------------------------------- + //-------------------------------------------------------------------------------------- + + event BidCreated(address indexed bidder, uint256 amountPerBid, uint256 bidIdIndex, uint64 ipfsStartIndex, uint8 numBids); + event BidCancelled(uint256 indexed bidId); + event BidReEnteredAuction(uint256 indexed bidId); + event WhitelistDisabled(bool whitelistStatus); + event WhitelistEnabled(bool whitelistStatus); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + //-------------------------------------------------------------------------------------- + //---------------------------- STATE-CHANGING FUNCTIONS ------------------------------ + //-------------------------------------------------------------------------------------- + + /// @notice Initialize to set variables on deployment + function initialize( + address _nodeOperatorManagerContract + ) external initializer { + require(_nodeOperatorManagerContract != address(0), "No Zero Addresses"); + + whitelistBidAmount = 0.001 ether; + minBidAmount = 0.01 ether; + maxBidAmount = 5 ether; + numberOfBids = 1; + whitelistEnabled = true; + + nodeOperatorManager = INodeOperatorManager(_nodeOperatorManagerContract); + + __Pausable_init(); + __Ownable_init(); + __UUPSUpgradeable_init(); + __ReentrancyGuard_init(); + } + + function initializeOnUpgrade(address _membershipManagerContractAddress, uint128 _accumulatedRevenueThreshold, address _etherFiAdminContractAddress, address _nodeOperatorManagerAddress) external onlyOwner { + require(_membershipManagerContractAddress != address(0) && _etherFiAdminContractAddress != address(0) && _nodeOperatorManagerAddress != address(0), "No Zero Addresses"); + membershipManagerContractAddress = _membershipManagerContractAddress; + nodeOperatorManager = INodeOperatorManager(_nodeOperatorManagerAddress); + accumulatedRevenue = 0; + accumulatedRevenueThreshold = _accumulatedRevenueThreshold; + admins[_etherFiAdminContractAddress] = true; + } + + function initializeOnUpgradeVersion2() external onlyOwner() { + bidIdsBeforeGasOptimization = numberOfBids; + numberOfBids = 256 * ((numberOfBids + 256 - 1) / 256); // offset + } + + /// @notice Creates bid(s) for the right to run a validator node when ETH is deposited + /// @param _bidSize the number of bids that the node operator would like to create + /// @param _bidAmountPerBid the ether value of each bid that is created + /// @return bidId Batched Bid ID + function createBid( + uint256 _bidSize, + uint256 _bidAmountPerBid + ) external payable whenNotPaused nonReentrant returns (uint256[] memory) { + require(_bidSize > 0 && _bidSize < 217, "Invalid bid size"); + if (whitelistEnabled) { + require( + nodeOperatorManager.isWhitelisted(msg.sender), + "Only whitelisted addresses" + ); + require( + msg.value == _bidSize * _bidAmountPerBid && + _bidAmountPerBid >= whitelistBidAmount && + _bidAmountPerBid <= maxBidAmount, + "Incorrect bid value" + ); + } else { + if ( + nodeOperatorManager.isWhitelisted(msg.sender) + ) { + require( + msg.value == _bidSize * _bidAmountPerBid && + _bidAmountPerBid >= whitelistBidAmount && + _bidAmountPerBid <= maxBidAmount, + "Incorrect bid value" + ); + } else { + require( + msg.value == _bidSize * _bidAmountPerBid && + _bidAmountPerBid >= minBidAmount && + _bidAmountPerBid <= maxBidAmount, + "Incorrect bid value" + ); + } + } + uint64 keysRemaining = nodeOperatorManager.getNumKeysRemaining(msg.sender); + require(_bidSize <= keysRemaining, "Insufficient public keys"); + + uint256 batchedBidId = numberOfBids / 256; + uint64 ipfsStartIndex = nodeOperatorManager.batchFetchNextKeyIndex(msg.sender, _bidSize); + + uint216 bitset = type(uint216).max >> (216 - _bidSize); + batchedBids[batchedBidId] = BatchedBid({ + numBids: uint8(_bidSize), + amountPerBidInGwei: uint32(_bidAmountPerBid / 1 gwei), + availableBidsBitset: bitset + }); + + numberOfBids += 256; + numberOfActiveBids += _bidSize; + operatorBidIndexMap[batchedBidId] = msg.sender; + + emit BidCreated(msg.sender, _bidAmountPerBid, batchedBidId, ipfsStartIndex, uint8(_bidSize)); + + uint256[] memory returnBatchedBidId = new uint256[](1); + returnBatchedBidId[0] = batchedBidId; + return returnBatchedBidId; + } + + /// @notice Cancels bids in a batch by calling the 'cancelBid' function multiple times + /// @dev Calls an internal function to perform the cancel + /// @param _bidIds the ID's of the bids to cancel + function cancelBidBatch(uint256[] calldata _bidIds) external whenNotPaused { + for (uint256 i = 0; i < _bidIds.length; i++) { + _cancelBid(_bidIds[i]); + } + } + + /// @notice Cancels a specified bid by de-activating it + /// @dev Calls an internal function to perform the cancel + /// @param _bidId the ID of the bid to cancel + function cancelBid(uint256 _bidId) public whenNotPaused { + _cancelBid(_bidId); + } + + function _available(uint256 bidPosition, uint216 bitset) internal pure returns (bool) { + return ((1 << bidPosition) & bitset) != 0; + } + + /// @notice Updates the details of the bid which has been used in a stake match + /// @dev Called by batchDepositWithBidIds() in StakingManager.sol + /// @param _bidId the ID of the bid being removed from the auction (since it has been selected) + function updateSelectedBidInformation( + uint256 _bidId + ) external onlyStakingManagerContract { + if (_bidId <= bidIdsBeforeGasOptimization) { + Bid storage bid = bids[_bidId]; + require(bid.isActive, "The bid is not active"); + bid.isActive = false; + } else { + uint256 bidPosition = _bidId % 256; + // batchedBidId = _bidId / 256 + BatchedBid storage batchedBid = batchedBids[_bidId / 256]; + require(_available(bidPosition, batchedBid.availableBidsBitset), "The bid is not active"); + batchedBid.availableBidsBitset &= ~(uint216(1 << bidPosition)); + } + + numberOfActiveBids--; + } + + /// @notice Lets a bid that was matched to a cancelled stake re-enter the auction + /// @param _bidId the ID of the bid which was matched to the cancelled stake. + function reEnterAuction( + uint256 _bidId + ) external onlyStakingManagerContract { + if (_bidId <= bidIdsBeforeGasOptimization) { + Bid storage bid = bids[_bidId]; + require(!bid.isActive, "Bid already active"); + bid.isActive = true; + } else { + uint256 bidPosition = _bidId % 256; + // batchedBidId = _bidId / 256 + BatchedBid storage batchedBid = batchedBids[_bidId / 256]; + + require(!_available(bidPosition, batchedBid.availableBidsBitset), "Bid already active"); + batchedBid.availableBidsBitset |= uint216(1 << bidPosition); + } + + numberOfActiveBids++; + emit BidReEnteredAuction(_bidId); + } + + /// @notice Transfer the auction fee received from the node operator to the membership NFT contract when above the threshold + /// @dev Called by registerValidator() in StakingManager.sol + /// @param _bidId the ID of the validator + function processAuctionFeeTransfer( + uint256 _bidId + ) external onlyStakingManagerContract { + uint256 amount; + if (_bidId <= bidIdsBeforeGasOptimization) { + amount = bids[_bidId].amount; + } else { + amount = uint256(batchedBids[_bidId / 256].amountPerBidInGwei) * 1 gwei; + } + + uint256 newAccumulatedRevenue = accumulatedRevenue + amount; + if (newAccumulatedRevenue >= accumulatedRevenueThreshold) { + accumulatedRevenue = 0; + (bool sent, ) = membershipManagerContractAddress.call{value: newAccumulatedRevenue}(""); + require(sent, "Failed to send Ether"); + } else { + accumulatedRevenue = uint128(newAccumulatedRevenue); + } + } + + function transferAccumulatedRevenue() external onlyAdmin { + uint256 transferAmount = accumulatedRevenue; + accumulatedRevenue = 0; + (bool sent, ) = membershipManagerContractAddress.call{value: transferAmount}(""); + require(sent, "Failed to send Ether"); + } + + /// @notice Disables the whitelisting phase of the bidding + /// @dev Allows both regular users and whitelisted users to bid + function disableWhitelist() public onlyAdmin { + whitelistEnabled = false; + emit WhitelistDisabled(whitelistEnabled); + } + + /// @notice Enables the whitelisting phase of the bidding + /// @dev Only users who are on a whitelist can bid + function enableWhitelist() public onlyAdmin { + whitelistEnabled = true; + emit WhitelistEnabled(whitelistEnabled); + } + + //Pauses the contract + function pauseContract() external onlyAdmin { + _pause(); + } + + //Unpauses the contract + function unPauseContract() external onlyAdmin { + _unpause(); + } + + //-------------------------------------------------------------------------------------- + //------------------------------- INTERNAL FUNCTIONS -------------------------------- + //-------------------------------------------------------------------------------------- + + function _cancelBid(uint256 _bidId) internal { + if (_bidId <= bidIdsBeforeGasOptimization) { + Bid storage bid = bids[_bidId]; + require(bid.bidderAddress == msg.sender, "Invalid bid"); + require(bid.isActive, "Bid already cancelled"); + + bid.isActive = false; + + (bool sent, ) = msg.sender.call{value: bid.amount}(""); + require(sent, "Failed to send Ether"); + } else { + uint256 batchId = _bidId / 256; + uint256 bidPosition = _bidId % 256; + BatchedBid storage batchedBid = batchedBids[batchId]; + + require(operatorBidIndexMap[batchId] == msg.sender, "Invalid bid"); + require(_available(bidPosition, batchedBid.availableBidsBitset), "Bid already cancelled"); + + batchedBid.availableBidsBitset &= ~(uint216(1 << bidPosition)); + + uint256 amount = uint256(batchedBid.amountPerBidInGwei) * 1 gwei; + (bool sent, ) = msg.sender.call{value: amount}(""); + require(sent, "Failed to send Ether"); + } + + numberOfActiveBids--; + emit BidCancelled(_bidId); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + //-------------------------------------------------------------------------------------- + //-------------------------------------- GETTER -------------------------------------- + //-------------------------------------------------------------------------------------- + + /// @notice Fetches the address of the user who placed a bid for a specific bid ID + /// @dev Needed for registerValidator() function in Staking Contract as well as function in the EtherFiNodeManager.sol + /// @return the address of the user who placed (owns) the bid + function getBidOwner(uint256 _bidId) external view returns (address) { + if (_bidId <= bidIdsBeforeGasOptimization) return bids[_bidId].bidderAddress; + + uint256 bucket = _bidId / 256; + uint256 subIndex = _bidId % 256; + if (subIndex >= batchedBids[bucket].numBids) return address(0); + + return operatorBidIndexMap[bucket]; + } + + /// @notice Fetches if a selected bid is currently active + /// @dev Needed for batchDepositWithBidIds() function in Staking Contract + /// @return the boolean value of the active flag in bids + function isBidActive(uint256 _bidId) external view returns (bool) { + if (_bidId <= bidIdsBeforeGasOptimization) return bids[_bidId].isActive; + + uint256 batchedBidIndex = _bidId / 256; + uint256 bidPosition = _bidId % 256; + BatchedBid memory batchedBid = batchedBids[batchedBidIndex]; + + // bid ID outside of accepted range for this aggregate bid + if (bidPosition >= batchedBid.numBids) return false; + + return _available(bidPosition, batchedBid.availableBidsBitset); + } + + /// @notice Fetches the address of the implementation contract currently being used by the proxy + /// @return the address of the currently used implementation contract + function getImplementation() external view returns (address) { + return _getImplementation(); + } + + //-------------------------------------------------------------------------------------- + //-------------------------------------- SETTER -------------------------------------- + //-------------------------------------------------------------------------------------- + + /// @notice Sets the staking managers contract address in the current contract + /// @param _stakingManagerContractAddress new stakingManagerContract address + function setStakingManagerContractAddress( + address _stakingManagerContractAddress + ) external onlyOwner { + require(address(stakingManagerContractAddress) == address(0), "Address already set"); + require(_stakingManagerContractAddress != address(0), "No zero addresses"); + stakingManagerContractAddress = _stakingManagerContractAddress; + } + + /// @notice Updates the minimum bid price for a non-whitelisted bidder + /// @param _newMinBidAmount the new amount to set the minimum bid price as + function setMinBidPrice(uint64 _newMinBidAmount) external onlyAdmin { + require(_newMinBidAmount < maxBidAmount, "Min bid exceeds max bid"); + require(_newMinBidAmount >= whitelistBidAmount, "Min bid less than whitelist bid amount"); + minBidAmount = _newMinBidAmount; + } + + /// @notice Updates the maximum bid price for both whitelisted and non-whitelisted bidders + /// @param _newMaxBidAmount the new amount to set the maximum bid price as + function setMaxBidPrice(uint64 _newMaxBidAmount) external onlyAdmin { + require(_newMaxBidAmount > minBidAmount, "Min bid exceeds max bid"); + maxBidAmount = _newMaxBidAmount; + } + + /// @notice Updates the accumulated revenue threshold that will trigger a transfer to MembershipNFT contract + /// @param _newThreshold the new threshold to set + function setAccumulatedRevenueThreshold(uint128 _newThreshold) external onlyAdmin { + accumulatedRevenueThreshold = _newThreshold; + } + + /// @notice Updates the minimum bid price for a whitelisted address + /// @param _newAmount the new amount to set the minimum bid price as + function updateWhitelistMinBidAmount( + uint128 _newAmount + ) external onlyOwner { + require(_newAmount < minBidAmount && _newAmount > 0, "Invalid Amount"); + whitelistBidAmount = _newAmount; + } + + function updateNodeOperatorManager(address _address) external onlyOwner { + nodeOperatorManager = INodeOperatorManager( + _address + ); + } + + /// @notice Updates the address of the admin + /// @param _address the new address to set as admin + function updateAdmin(address _address, bool _isAdmin) external onlyOwner { + require(_address != address(0), "Cannot be address zero"); + admins[_address] = _isAdmin; + } + + //-------------------------------------------------------------------------------------- + //----------------------------------- MODIFIERS -------------------------------------- + //-------------------------------------------------------------------------------------- + + modifier onlyStakingManagerContract() { + require(msg.sender == stakingManagerContractAddress, "Only staking manager contract function"); + _; + } + + modifier onlyAdmin() { + require(admins[msg.sender], "Caller is not the admin"); + _; + } +} diff --git a/src/NodeOperatorManager.sol b/src/NodeOperatorManager.sol index b91162bb5..1f7d56d43 100644 --- a/src/NodeOperatorManager.sol +++ b/src/NodeOperatorManager.sol @@ -126,6 +126,25 @@ contract NodeOperatorManager is INodeOperatorManager, Initializable, UUPSUpgrade return ipfsIndex; } + /// @notice Fetches the next key they have available to use + /// @param _user the user to fetch the key for + /// @return The ipfs index available for the validators + function batchFetchNextKeyIndex( + address _user, + uint256 _size + ) external onlyAuctionManagerContract returns (uint64) { + KeyData storage keyData = addressToOperatorData[_user]; + uint64 totalKeys = keyData.totalKeys; + require( + keyData.keysUsed + _size < totalKeys, + "Insufficient public keys" + ); + + uint64 ipfsIndex = keyData.keysUsed; + keyData.keysUsed += uint64(_size); + return ipfsIndex; + } + /// @notice Approves or un approves an operator to run validators from a specific source of funds /// @dev To allow a permissioned system, we will approve node operators to run validators only for a specific source of funds (EETH / ETHER_FAN) /// Some operators can be approved for both sources and some for only one. Being approved means that when a BNFT player deposits, diff --git a/src/interfaces/IAuctionManager.sol b/src/interfaces/IAuctionManager.sol index 9be753e48..ab507560d 100644 --- a/src/interfaces/IAuctionManager.sol +++ b/src/interfaces/IAuctionManager.sol @@ -8,6 +8,12 @@ interface IAuctionManager { address bidderAddress; bool isActive; } + + struct BatchedBid { + uint8 numBids; + uint32 amountPerBidInGwei; + uint216 availableBidsBitset; + } function initialize(address _nodeOperatorManagerContract) external; diff --git a/src/interfaces/INodeOperatorManager.sol b/src/interfaces/INodeOperatorManager.sol index b669e9bfc..031484b1a 100644 --- a/src/interfaces/INodeOperatorManager.sol +++ b/src/interfaces/INodeOperatorManager.sol @@ -41,6 +41,7 @@ interface INodeOperatorManager { ) external; function fetchNextKeyIndex(address _user) external returns (uint64); + function batchFetchNextKeyIndex(address _user, uint256 _size) external returns (uint64); function isEligibleToRunValidatorsForSourceOfFund(address _operator, LiquidityPool.SourceOfFunds _source) external view returns (bool approved); diff --git a/src/libraries/PopCount.sol b/src/libraries/PopCount.sol new file mode 100644 index 000000000..436b4b924 --- /dev/null +++ b/src/libraries/PopCount.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +library PopCount { + uint256 private constant m1 = 0x5555555555555555555555555555555555555555555555555555555555555555; + uint256 private constant m2 = 0x3333333333333333333333333333333333333333333333333333333333333333; + uint256 private constant m4 = 0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f; + uint256 private constant h01 = 0x0101010101010101010101010101010101010101010101010101010101010101; + + function popcount256A(uint256 x) internal pure returns (uint256 count) { + unchecked{ + for (count=0; x!=0; count++) + x &= x - 1; + } + } + + function popcount256B(uint256 x) internal pure returns (uint256) { + if (x == type(uint256).max) { + return 256; + } + unchecked { + x -= (x >> 1) & m1; //put count of each 2 bits into those 2 bits + x = (x & m2) + ((x >> 2) & m2); //put count of each 4 bits into those 4 bits + x = (x + (x >> 4)) & m4; //put count of each 8 bits into those 8 bits + x = (x * h01) >> 248; //returns left 8 bits of x + (x<<8) + (x<<16) + (x<<24) + ... + } + return x; + } +} diff --git a/test/AuctionManagerGasOptimized.t.sol b/test/AuctionManagerGasOptimized.t.sol new file mode 100644 index 000000000..7004267fa --- /dev/null +++ b/test/AuctionManagerGasOptimized.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "./TestSetup.sol"; +import "../src/AuctionManagerGasOptimized.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; + +contract AuctionManagerGasOptimizedTest is TestSetup { + AuctionManagerGasOptimized auctionGasOptimizedInstance; + + function setUp() public { + setUpTests(); + + uint256 numBidsBefore = auctionInstance.numberOfBids(); + + AuctionManagerGasOptimized auctionManager = new AuctionManagerGasOptimized(); + + vm.prank(owner); + UUPSUpgradeable(address(auctionInstance)).upgradeToAndCall( + address(auctionManager), + abi.encodeWithSelector( + AuctionManagerGasOptimized.initializeOnUpgradeVersion2.selector + ) + ); + + uint256 numBidsAfter = auctionInstance.numberOfBids(); + assertEq(numBidsAfter, 256 * ((numBidsBefore - 1 + 256) / 256)); + + auctionGasOptimizedInstance = AuctionManagerGasOptimized(address(auctionInstance)); + } + + function test_createBidWorksAfterGasOptimization() public { + startHoax(alice); + nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 250); + uint256[] memory batchBidId = auctionInstance.createBid{value: 20 ether}(200, 0.1 ether); + + uint256 batchId = batchBidId[0]; + ( + uint16 numBids, + uint32 amountPerBidInGwei, + uint216 availableBidsBitset + ) = auctionGasOptimizedInstance.batchedBids(batchId); + + uint256 bidId = batchId * 256; + address bidderAddress = auctionGasOptimizedInstance.getBidOwner(bidId); + + assertEq(numBids, 200); + assertEq(availableBidsBitset, 1606938044258990275541962092341162602522202993782792835301375); // 111..(200 times) in binary for 3 active bids + assertEq(amountPerBidInGwei, 0.1 gwei); + assertEq(bidderAddress, alice); + + assertTrue(auctionInstance.isBidActive(bidId)); + assertEq(auctionInstance.getBidOwner(bidId), alice); + } + + function test_CancelBidWorksAfterGasOptimization() public { + vm.prank(alice); + nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 15); + + uint256 amountPerBidInGwei = 0.1 ether; + + startHoax(alice); + uint256[] memory bidIds = auctionInstance.createBid{value: 0.2 ether}(2, amountPerBidInGwei); + uint256 batchId = bidIds[0]; + + uint256 firstBidId = batchId * 256; + uint256 secondBidId = batchId * 256 + 1; + + uint256 balanceBefore = alice.balance; + auctionGasOptimizedInstance.cancelBid(firstBidId); + + address bidderAddress = auctionGasOptimizedInstance.getBidOwner(firstBidId); + + ( , , uint216 availableBidsBitset) = auctionGasOptimizedInstance.batchedBids(batchId); + + assertEq(availableBidsBitset, 2); // 10 in binary - first bid cancelled + assertFalse(auctionGasOptimizedInstance.isBidActive(firstBidId)); + assertTrue(auctionGasOptimizedInstance.isBidActive(secondBidId)); + assertEq(bidderAddress, alice); + assertEq(alice.balance, balanceBefore + 0.1 ether); + } + + function test_BidActivationAfterGasOptimization() public { + vm.prank(alice); + nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 15); + + startHoax(alice); + uint256[] memory batchId = auctionGasOptimizedInstance.createBid{value: 0.1 ether}(1, 0.1 ether); + + uint256[] memory selectedBids = new uint256[](1); + selectedBids[0] = batchId[0] * 256; + + stakingManagerInstance.batchDepositWithBidIds{value: 32 ether}(selectedBids, false); + assertFalse(auctionGasOptimizedInstance.isBidActive(selectedBids[0])); + + stakingManagerInstance.batchCancelDeposit(selectedBids); + assertTrue(auctionGasOptimizedInstance.isBidActive(selectedBids[0])); + } + + function test_BatchBoundaries() public { + vm.prank(alice); + nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 250); + + startHoax(alice); + uint256[] memory bidIds = auctionInstance.createBid{value: 21.6 ether}(216, 0.1 ether); + + uint256 batchId = bidIds[0]; + ( + uint16 numBids, + uint32 amountPerBidInGwei, + uint216 availableBidsBitset + ) = auctionGasOptimizedInstance.batchedBids(batchId); + + assertEq(numBids, 216); + assertEq(availableBidsBitset, 105312291668557186697918027683670432318895095400549111254310977535); // 1111111111..(216 times) in binary + + uint256[] memory moreBids = auctionGasOptimizedInstance.createBid{value: 0.5 ether}(5, 0.1 ether); + + uint256 nextBatchId = moreBids[0]; + (numBids, amountPerBidInGwei, availableBidsBitset) = auctionGasOptimizedInstance.batchedBids(nextBatchId); + + assertEq(numBids, 5); + assertEq(availableBidsBitset, 31); // 11111 in binary + } +} \ No newline at end of file