From 318e693d8b57dc3225ff38282e03d785e42163e4 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Tue, 24 Dec 2024 14:41:20 -0500 Subject: [PATCH 1/3] optimized auction manager --- src/AuctionManagerGasOptimized.sol | 439 ++++++++++++++++++++++++ src/NodeOperatorManager.sol | 19 + src/interfaces/IAuctionManager.sol | 8 + src/interfaces/INodeOperatorManager.sol | 1 + test/AuctionManagerGasOptimized.t.sol | 118 +++++++ 5 files changed, 585 insertions(+) create mode 100644 src/AuctionManagerGasOptimized.sol create mode 100644 test/AuctionManagerGasOptimized.t.sol diff --git a/src/AuctionManagerGasOptimized.sol b/src/AuctionManagerGasOptimized.sol new file mode 100644 index 000000000..f413d3771 --- /dev/null +++ b/src/AuctionManagerGasOptimized.sol @@ -0,0 +1,439 @@ +// 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 => BatchedBid) public batchedBids; + + //-------------------------------------------------------------------------------------- + //------------------------------------- EVENTS --------------------------------------- + //-------------------------------------------------------------------------------------- + + event BidCreated(address indexed bidder, uint256 amountPerBid, uint256[] bidIdArray, uint64[] ipfsIndexArray); + 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 = 10 * ((numberOfBids + 10 - 1) / 10); // 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 bidIdArray array of the bidIDs that were created + function createBid( + uint256 _bidSize, + uint256 _bidAmountPerBid + ) external payable whenNotPaused nonReentrant returns (uint256[] memory) { + require(_bidSize > 0, "Bid size is too small"); + 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[] memory bidIdArray = new uint256[](_bidSize); + uint64[] memory ipfsIndexArray = new uint64[](_bidSize); + + uint256 numBatchedBids = (_bidSize + 10 - 1) / 10; + uint256 batchedBidId = numberOfBids / 10; + uint64 ipfsStartIndex = nodeOperatorManager.batchFetchNextKeyIndex(msg.sender, _bidSize); + + for (uint256 i = 0; i < _bidSize;) { + unchecked { + bidIdArray[i] = batchedBidId * 10 + i; + ipfsIndexArray[i] = ipfsStartIndex + uint64(1); + ++i; + } + } + + for (uint256 i = 0; i < numBatchedBids;) { + uint256 numBids = Math.min(10, _bidSize - i * 10); + uint256 bidBatchId = batchedBidId + i; + uint16 isActiveBits = uint16((1 << numBids) - 1); + + batchedBids[bidBatchId] = BatchedBid({ + numBids: uint16(numBids), + isActiveBits: isActiveBits, + amountPerBidInGwei: uint32(_bidAmountPerBid / 1 gwei), + bidderPubKeyStartIndex: uint32(ipfsStartIndex + i), + bidderAddress: msg.sender + }); + + unchecked { + ++i; + } + + // The Bid with its bid Id `x` can be accessed + // - if x <= bidIdsBeforeGasOptimization, then bids[x] is the bid + // - otherwise, then batchedBids[x / 10] is all needed to construct the bid info + // - isAcitve = (batchedBids[x / 10].isActiveBits >> (x % 10)) & 1 + // - bidderPubKeyIndex = batchedBids[x / 10].bidderPubKeyStartIndex + (x % 10) + } + numberOfBids += numBatchedBids * 10; + numberOfActiveBids += _bidSize; + + emit BidCreated(msg.sender, _bidAmountPerBid, bidIdArray, ipfsIndexArray); + return bidIdArray; + } + + /// @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); + } + + /// @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 % 10; + // batchedBidId = _bidId / 10 + BatchedBid storage batchedBid = batchedBids[_bidId / 10]; + require(((batchedBid.isActiveBits >> bidPosition) & 1) == 1, "The bid is not active"); + batchedBid.isActiveBits &= ~(uint16(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 % 10; + // batchedBidId = _bidId / 10 + BatchedBid storage batchedBid = batchedBids[_bidId / 10]; + + require(((batchedBid.isActiveBits >> bidPosition) & 1) == 0, "Bid already active"); + batchedBid.isActiveBits |= uint16(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 / 10].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 / 10; + uint256 bidPosition = _bidId % 10; + BatchedBid storage batchedBid = batchedBids[batchId]; + + require(batchedBid.bidderAddress == msg.sender, "Invalid bid"); + require(((batchedBid.isActiveBits >> bidPosition) & 1) == 1, "Bid already cancelled"); + + batchedBid.isActiveBits &= ~(uint16(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; + // batchedBidId = _bidId / 10 + return batchedBids[_bidId / 10].bidderAddress; + } + + /// @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 bidPosition = _bidId % 10; + // batchedBidId = _bidId / 10 + return (batchedBids[_bidId / 10].isActiveBits >> bidPosition) & 1 == 1; + } + + /// @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..6154925b5 100644 --- a/src/interfaces/IAuctionManager.sol +++ b/src/interfaces/IAuctionManager.sol @@ -8,6 +8,14 @@ interface IAuctionManager { address bidderAddress; bool isActive; } + + struct BatchedBid { + uint16 numBids; + uint16 isActiveBits; + uint32 amountPerBidInGwei; + uint32 bidderPubKeyStartIndex; + address bidderAddress; + } 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/test/AuctionManagerGasOptimized.t.sol b/test/AuctionManagerGasOptimized.t.sol new file mode 100644 index 000000000..f464edd43 --- /dev/null +++ b/test/AuctionManagerGasOptimized.t.sol @@ -0,0 +1,118 @@ +// 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, 10 * ((numBidsBefore - 1 + 10) / 10)); + + auctionGasOptimizedInstance = AuctionManagerGasOptimized(address(auctionInstance)); + } + + function test_createBidWorksAfterGasOptimization() public { + vm.prank(alice); + nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 25); + + // Create new bids under batched system + startHoax(alice); + uint256[] memory bidIds = auctionInstance.createBid{value: 0.3 ether}(3, 0.1 ether); + + // Verify batch storage + uint256 batchId = bidIds[0] / 10; + ( + uint16 numBids, + uint16 isActiveBits, + uint32 amountPerBidInGwei, + , + address bidderAddress + ) = auctionGasOptimizedInstance.batchedBids(batchId); + + assertEq(numBids, 3); + assertEq(isActiveBits, 7); // 111 in binary for 3 active bids + assertEq(amountPerBidInGwei, 0.1 gwei); + assertEq(bidderAddress, alice); + + assertTrue(auctionInstance.isBidActive(bidIds[0])); + assertEq(auctionInstance.getBidOwner(bidIds[0]), alice); + } + + function test_CancelBidWorksAfterGasOptimization() public { + vm.prank(alice); + nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 15); + + startHoax(alice); + uint256[] memory bidIds = auctionInstance.createBid{value: 0.2 ether}(2, 0.1 ether); + + uint256 balanceBefore = alice.balance; + auctionGasOptimizedInstance.cancelBid(bidIds[0]); + + uint256 batchId = bidIds[0] / 10; + (, uint16 isActiveBits,,, address bidderAddress) = auctionGasOptimizedInstance.batchedBids(batchId); + + assertEq(isActiveBits, 2); // 10 in binary - first bid cancelled + assertFalse(auctionGasOptimizedInstance.isBidActive(bidIds[0])); + assertTrue(auctionGasOptimizedInstance.isBidActive(bidIds[1])); + 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 bidIds = auctionGasOptimizedInstance.createBid{value: 0.1 ether}(1, 0.1 ether); + + uint256[] memory selectedBids = new uint256[](1); + selectedBids[0] = bidIds[0]; + + stakingManagerInstance.batchDepositWithBidIds{value: 32 ether}(selectedBids, false); + assertFalse(auctionGasOptimizedInstance.isBidActive(bidIds[0])); + + stakingManagerInstance.batchCancelDeposit(selectedBids); + assertTrue(auctionGasOptimizedInstance.isBidActive(bidIds[0])); + } + + function test_BatchBoundaries() public { + vm.prank(alice); + nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 25); + + + startHoax(alice); + uint256[] memory bidIds = auctionInstance.createBid{value: 1 ether}(10, 0.1 ether); + + uint256 batchId = bidIds[0] / 10; + (uint16 numBids, uint16 isActiveBits,,,) = auctionGasOptimizedInstance.batchedBids(batchId); + + assertEq(numBids, 10); + assertEq(isActiveBits, 1023); // 1111111111 in binary + + uint256[] memory moreBids = auctionGasOptimizedInstance.createBid{value: 0.5 ether}(5, 0.1 ether); + + uint256 nextBatchId = moreBids[0] / 10; + (numBids, isActiveBits,,,) = auctionGasOptimizedInstance.batchedBids(nextBatchId); + + assertEq(numBids, 5); + assertEq(isActiveBits, 31); // 11111 in binary + } +} \ No newline at end of file From 10c7151a54601927e044d49d84f7df556bc1b2e1 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Fri, 27 Dec 2024 14:39:01 -0500 Subject: [PATCH 2/3] added optimized version of Auction manager --- src/AuctionManagerGasOptimized.sol | 114 +++++++++++--------------- src/interfaces/IAuctionManager.sol | 6 +- src/libraries/PopCount.sol | 29 +++++++ test/AuctionManagerGasOptimized.t.sol | 85 ++++++++++--------- 4 files changed, 127 insertions(+), 107 deletions(-) create mode 100644 src/libraries/PopCount.sol diff --git a/src/AuctionManagerGasOptimized.sol b/src/AuctionManagerGasOptimized.sol index f413d3771..1211f7889 100644 --- a/src/AuctionManagerGasOptimized.sol +++ b/src/AuctionManagerGasOptimized.sol @@ -46,13 +46,14 @@ contract AuctionManagerGasOptimized is mapping(address => bool) public admins; uint256 public bidIdsBeforeGasOptimization; - mapping(uint256 => BatchedBid) public batchedBids; + mapping(uint256 bidIndex => BatchedBid bids) public batchedBids; + mapping(uint256 bidIndex => address operator) public operatorBidIndexMap; //-------------------------------------------------------------------------------------- //------------------------------------- EVENTS --------------------------------------- //-------------------------------------------------------------------------------------- - event BidCreated(address indexed bidder, uint256 amountPerBid, uint256[] bidIdArray, uint64[] ipfsIndexArray); + 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); @@ -98,18 +99,18 @@ contract AuctionManagerGasOptimized is function initializeOnUpgradeVersion2() external onlyOwner() { bidIdsBeforeGasOptimization = numberOfBids; - numberOfBids = 10 * ((numberOfBids + 10 - 1) / 10); // offset + 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 bidIdArray array of the bidIDs that were created + /// @return bidId Batched Bid ID function createBid( uint256 _bidSize, uint256 _bidAmountPerBid ) external payable whenNotPaused nonReentrant returns (uint256[] memory) { - require(_bidSize > 0, "Bid size is too small"); + require(_bidSize > 0 && _bidSize < 217, "Invalid bid size"); if (whitelistEnabled) { require( nodeOperatorManager.isWhitelisted(msg.sender), @@ -143,49 +144,25 @@ contract AuctionManagerGasOptimized is uint64 keysRemaining = nodeOperatorManager.getNumKeysRemaining(msg.sender); require(_bidSize <= keysRemaining, "Insufficient public keys"); - uint256[] memory bidIdArray = new uint256[](_bidSize); - uint64[] memory ipfsIndexArray = new uint64[](_bidSize); - - uint256 numBatchedBids = (_bidSize + 10 - 1) / 10; - uint256 batchedBidId = numberOfBids / 10; + uint256 batchedBidId = numberOfBids / 256; uint64 ipfsStartIndex = nodeOperatorManager.batchFetchNextKeyIndex(msg.sender, _bidSize); - for (uint256 i = 0; i < _bidSize;) { - unchecked { - bidIdArray[i] = batchedBidId * 10 + i; - ipfsIndexArray[i] = ipfsStartIndex + uint64(1); - ++i; - } - } - - for (uint256 i = 0; i < numBatchedBids;) { - uint256 numBids = Math.min(10, _bidSize - i * 10); - uint256 bidBatchId = batchedBidId + i; - uint16 isActiveBits = uint16((1 << numBids) - 1); - - batchedBids[bidBatchId] = BatchedBid({ - numBids: uint16(numBids), - isActiveBits: isActiveBits, - amountPerBidInGwei: uint32(_bidAmountPerBid / 1 gwei), - bidderPubKeyStartIndex: uint32(ipfsStartIndex + i), - bidderAddress: msg.sender - }); - - unchecked { - ++i; - } + uint216 bitset = type(uint216).max >> (216 - _bidSize); + batchedBids[batchedBidId] = BatchedBid({ + numBids: uint8(_bidSize), + amountPerBidInGwei: uint32(_bidAmountPerBid / 1 gwei), + availableBidsBitset: bitset + }); - // The Bid with its bid Id `x` can be accessed - // - if x <= bidIdsBeforeGasOptimization, then bids[x] is the bid - // - otherwise, then batchedBids[x / 10] is all needed to construct the bid info - // - isAcitve = (batchedBids[x / 10].isActiveBits >> (x % 10)) & 1 - // - bidderPubKeyIndex = batchedBids[x / 10].bidderPubKeyStartIndex + (x % 10) - } - numberOfBids += numBatchedBids * 10; + numberOfBids += 256; numberOfActiveBids += _bidSize; + operatorBidIndexMap[batchedBidId] = msg.sender; - emit BidCreated(msg.sender, _bidAmountPerBid, bidIdArray, ipfsIndexArray); - return bidIdArray; + 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 @@ -215,11 +192,11 @@ contract AuctionManagerGasOptimized is require(bid.isActive, "The bid is not active"); bid.isActive = false; } else { - uint256 bidPosition = _bidId % 10; - // batchedBidId = _bidId / 10 - BatchedBid storage batchedBid = batchedBids[_bidId / 10]; - require(((batchedBid.isActiveBits >> bidPosition) & 1) == 1, "The bid is not active"); - batchedBid.isActiveBits &= ~(uint16(1 << bidPosition)); + uint256 bidPosition = _bidId % 256; + // batchedBidId = _bidId / 256 + BatchedBid storage batchedBid = batchedBids[_bidId / 256]; + require(((1 << bidPosition) & batchedBid.availableBidsBitset) == 1, "The bid is not active"); + batchedBid.availableBidsBitset &= ~(uint216(1 << bidPosition)); } numberOfActiveBids--; @@ -235,12 +212,12 @@ contract AuctionManagerGasOptimized is require(!bid.isActive, "Bid already active"); bid.isActive = true; } else { - uint256 bidPosition = _bidId % 10; - // batchedBidId = _bidId / 10 - BatchedBid storage batchedBid = batchedBids[_bidId / 10]; + uint256 bidPosition = _bidId % 256; + // batchedBidId = _bidId / 256 + BatchedBid storage batchedBid = batchedBids[_bidId / 256]; - require(((batchedBid.isActiveBits >> bidPosition) & 1) == 0, "Bid already active"); - batchedBid.isActiveBits |= uint16(1 << bidPosition); + require(((1 << bidPosition) & batchedBid.availableBidsBitset) == 0, "Bid already active"); + batchedBid.availableBidsBitset |= uint216(1 << bidPosition); } numberOfActiveBids++; @@ -257,7 +234,7 @@ contract AuctionManagerGasOptimized is if (_bidId <= bidIdsBeforeGasOptimization) { amount = bids[_bidId].amount; } else { - amount = uint256(batchedBids[_bidId / 10].amountPerBidInGwei) * 1 gwei; + amount = uint256(batchedBids[_bidId / 256].amountPerBidInGwei) * 1 gwei; } uint256 newAccumulatedRevenue = accumulatedRevenue + amount; @@ -316,14 +293,14 @@ contract AuctionManagerGasOptimized is (bool sent, ) = msg.sender.call{value: bid.amount}(""); require(sent, "Failed to send Ether"); } else { - uint256 batchId = _bidId / 10; - uint256 bidPosition = _bidId % 10; + uint256 batchId = _bidId / 256; + uint256 bidPosition = _bidId % 256; BatchedBid storage batchedBid = batchedBids[batchId]; - require(batchedBid.bidderAddress == msg.sender, "Invalid bid"); - require(((batchedBid.isActiveBits >> bidPosition) & 1) == 1, "Bid already cancelled"); + require(operatorBidIndexMap[batchId] == msg.sender, "Invalid bid"); + require(((1 << bidPosition) & batchedBid.availableBidsBitset) == 1, "Bid already cancelled"); - batchedBid.isActiveBits &= ~(uint16(1 << bidPosition)); + batchedBid.availableBidsBitset &= ~(uint216(1 << bidPosition)); uint256 amount = uint256(batchedBid.amountPerBidInGwei) * 1 gwei; (bool sent, ) = msg.sender.call{value: amount}(""); @@ -345,8 +322,12 @@ contract AuctionManagerGasOptimized is /// @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; - // batchedBidId = _bidId / 10 - return batchedBids[_bidId / 10].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 @@ -355,9 +336,14 @@ contract AuctionManagerGasOptimized is function isBidActive(uint256 _bidId) external view returns (bool) { if (_bidId <= bidIdsBeforeGasOptimization) return bids[_bidId].isActive; - uint256 bidPosition = _bidId % 10; - // batchedBidId = _bidId / 10 - return (batchedBids[_bidId / 10].isActiveBits >> bidPosition) & 1 == 1; + uint256 bucket = _bidId / 256; + uint256 subIndex = _bidId % 256; + BatchedBid memory batchedBid = batchedBids[bucket]; + + // bid ID outside of accepted range for this aggregate bid + if (subIndex >= batchedBid.numBids) return false; + + return ((1 << subIndex) & batchedBid.availableBidsBitset) != 0; } /// @notice Fetches the address of the implementation contract currently being used by the proxy diff --git a/src/interfaces/IAuctionManager.sol b/src/interfaces/IAuctionManager.sol index 6154925b5..ab507560d 100644 --- a/src/interfaces/IAuctionManager.sol +++ b/src/interfaces/IAuctionManager.sol @@ -10,11 +10,9 @@ interface IAuctionManager { } struct BatchedBid { - uint16 numBids; - uint16 isActiveBits; + uint8 numBids; uint32 amountPerBidInGwei; - uint32 bidderPubKeyStartIndex; - address bidderAddress; + uint216 availableBidsBitset; } function initialize(address _nodeOperatorManagerContract) external; 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 index f464edd43..7004267fa 100644 --- a/test/AuctionManagerGasOptimized.t.sol +++ b/test/AuctionManagerGasOptimized.t.sol @@ -24,54 +24,58 @@ contract AuctionManagerGasOptimizedTest is TestSetup { ); uint256 numBidsAfter = auctionInstance.numberOfBids(); - assertEq(numBidsAfter, 10 * ((numBidsBefore - 1 + 10) / 10)); + assertEq(numBidsAfter, 256 * ((numBidsBefore - 1 + 256) / 256)); auctionGasOptimizedInstance = AuctionManagerGasOptimized(address(auctionInstance)); } function test_createBidWorksAfterGasOptimization() public { - vm.prank(alice); - nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 25); - - // Create new bids under batched system startHoax(alice); - uint256[] memory bidIds = auctionInstance.createBid{value: 0.3 ether}(3, 0.1 ether); + nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 250); + uint256[] memory batchBidId = auctionInstance.createBid{value: 20 ether}(200, 0.1 ether); - // Verify batch storage - uint256 batchId = bidIds[0] / 10; + uint256 batchId = batchBidId[0]; ( uint16 numBids, - uint16 isActiveBits, uint32 amountPerBidInGwei, - , - address bidderAddress + uint216 availableBidsBitset ) = auctionGasOptimizedInstance.batchedBids(batchId); + + uint256 bidId = batchId * 256; + address bidderAddress = auctionGasOptimizedInstance.getBidOwner(bidId); - assertEq(numBids, 3); - assertEq(isActiveBits, 7); // 111 in binary for 3 active bids + 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(bidIds[0])); - assertEq(auctionInstance.getBidOwner(bidIds[0]), 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, 0.1 ether); + 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(bidIds[0]); + auctionGasOptimizedInstance.cancelBid(firstBidId); - uint256 batchId = bidIds[0] / 10; - (, uint16 isActiveBits,,, address bidderAddress) = auctionGasOptimizedInstance.batchedBids(batchId); + address bidderAddress = auctionGasOptimizedInstance.getBidOwner(firstBidId); + + ( , , uint216 availableBidsBitset) = auctionGasOptimizedInstance.batchedBids(batchId); - assertEq(isActiveBits, 2); // 10 in binary - first bid cancelled - assertFalse(auctionGasOptimizedInstance.isBidActive(bidIds[0])); - assertTrue(auctionGasOptimizedInstance.isBidActive(bidIds[1])); + 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); } @@ -81,38 +85,41 @@ contract AuctionManagerGasOptimizedTest is TestSetup { nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 15); startHoax(alice); - uint256[] memory bidIds = auctionGasOptimizedInstance.createBid{value: 0.1 ether}(1, 0.1 ether); + uint256[] memory batchId = auctionGasOptimizedInstance.createBid{value: 0.1 ether}(1, 0.1 ether); uint256[] memory selectedBids = new uint256[](1); - selectedBids[0] = bidIds[0]; + selectedBids[0] = batchId[0] * 256; stakingManagerInstance.batchDepositWithBidIds{value: 32 ether}(selectedBids, false); - assertFalse(auctionGasOptimizedInstance.isBidActive(bidIds[0])); + assertFalse(auctionGasOptimizedInstance.isBidActive(selectedBids[0])); stakingManagerInstance.batchCancelDeposit(selectedBids); - assertTrue(auctionGasOptimizedInstance.isBidActive(bidIds[0])); + assertTrue(auctionGasOptimizedInstance.isBidActive(selectedBids[0])); } function test_BatchBoundaries() public { vm.prank(alice); - nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 25); - - + nodeOperatorManagerInstance.registerNodeOperator(_ipfsHash, 250); + startHoax(alice); - uint256[] memory bidIds = auctionInstance.createBid{value: 1 ether}(10, 0.1 ether); - - uint256 batchId = bidIds[0] / 10; - (uint16 numBids, uint16 isActiveBits,,,) = auctionGasOptimizedInstance.batchedBids(batchId); - - assertEq(numBids, 10); - assertEq(isActiveBits, 1023); // 1111111111 in binary + 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] / 10; - (numBids, isActiveBits,,,) = auctionGasOptimizedInstance.batchedBids(nextBatchId); + uint256 nextBatchId = moreBids[0]; + (numBids, amountPerBidInGwei, availableBidsBitset) = auctionGasOptimizedInstance.batchedBids(nextBatchId); assertEq(numBids, 5); - assertEq(isActiveBits, 31); // 11111 in binary + assertEq(availableBidsBitset, 31); // 11111 in binary } } \ No newline at end of file From 95fcf8323c0bb3dc7e4389a5ced95f80b599fbb0 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Fri, 27 Dec 2024 14:58:26 -0500 Subject: [PATCH 3/3] add bid available in a function --- src/AuctionManagerGasOptimized.sol | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/AuctionManagerGasOptimized.sol b/src/AuctionManagerGasOptimized.sol index 1211f7889..e21d81517 100644 --- a/src/AuctionManagerGasOptimized.sol +++ b/src/AuctionManagerGasOptimized.sol @@ -181,6 +181,10 @@ contract AuctionManagerGasOptimized is _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) @@ -195,7 +199,7 @@ contract AuctionManagerGasOptimized is uint256 bidPosition = _bidId % 256; // batchedBidId = _bidId / 256 BatchedBid storage batchedBid = batchedBids[_bidId / 256]; - require(((1 << bidPosition) & batchedBid.availableBidsBitset) == 1, "The bid is not active"); + require(_available(bidPosition, batchedBid.availableBidsBitset), "The bid is not active"); batchedBid.availableBidsBitset &= ~(uint216(1 << bidPosition)); } @@ -216,7 +220,7 @@ contract AuctionManagerGasOptimized is // batchedBidId = _bidId / 256 BatchedBid storage batchedBid = batchedBids[_bidId / 256]; - require(((1 << bidPosition) & batchedBid.availableBidsBitset) == 0, "Bid already active"); + require(!_available(bidPosition, batchedBid.availableBidsBitset), "Bid already active"); batchedBid.availableBidsBitset |= uint216(1 << bidPosition); } @@ -298,7 +302,7 @@ contract AuctionManagerGasOptimized is BatchedBid storage batchedBid = batchedBids[batchId]; require(operatorBidIndexMap[batchId] == msg.sender, "Invalid bid"); - require(((1 << bidPosition) & batchedBid.availableBidsBitset) == 1, "Bid already cancelled"); + require(_available(bidPosition, batchedBid.availableBidsBitset), "Bid already cancelled"); batchedBid.availableBidsBitset &= ~(uint216(1 << bidPosition)); @@ -336,14 +340,14 @@ contract AuctionManagerGasOptimized is function isBidActive(uint256 _bidId) external view returns (bool) { if (_bidId <= bidIdsBeforeGasOptimization) return bids[_bidId].isActive; - uint256 bucket = _bidId / 256; - uint256 subIndex = _bidId % 256; - BatchedBid memory batchedBid = batchedBids[bucket]; + uint256 batchedBidIndex = _bidId / 256; + uint256 bidPosition = _bidId % 256; + BatchedBid memory batchedBid = batchedBids[batchedBidIndex]; // bid ID outside of accepted range for this aggregate bid - if (subIndex >= batchedBid.numBids) return false; + if (bidPosition >= batchedBid.numBids) return false; - return ((1 << subIndex) & batchedBid.availableBidsBitset) != 0; + return _available(bidPosition, batchedBid.availableBidsBitset); } /// @notice Fetches the address of the implementation contract currently being used by the proxy