diff --git a/contracts/auction-handler/FastLaneAuctionHandler.sol b/contracts/auction-handler/FastLaneAuctionHandler.sol index 38e1aae..b3268f9 100644 --- a/contracts/auction-handler/FastLaneAuctionHandler.sol +++ b/contracts/auction-handler/FastLaneAuctionHandler.sol @@ -42,7 +42,7 @@ abstract contract FastLaneAuctionHandlerEvents { ); event RelayWithdrawStuckERC20(address indexed receiver, address indexed token, uint256 amount); - event RelayWithdrawStuckNativeToken(address indexed receiver, uint256 amount); + event RelayProcessingExcessBalance(address indexed receiver, uint256 amount); event RelayProcessingPaidValidator(address indexed validator, uint256 validatorPayment, address indexed initiator); @@ -133,7 +133,7 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { uint256 internal constant VALIDATOR_REFUND_SCALE = 10_000; // 1 = 0.01% /// @notice The default refund share for validators - int256 internal constant DEFAULT_VALIDATOR_REFUND_SHARE = 5000; // 50% + int256 internal constant DEFAULT_VALIDATOR_REFUND_SHARE = 5_000; // 50% /// @notice Mapping to Validator Data Struct mapping(address => ValidatorData) internal validatorsDataMap; @@ -155,6 +155,12 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { /// @notice Map[validator] = % payment to validator in a bid with refund mapping(address => int256) private validatorsRefundShareMap; + uint256 internal constant VALIDATOR_SHARE_BASE = 1_000_000; + uint256 internal constant STAKE_RATIO = 50_000; // 5% + + // TODO: update to point to the correct address + address internal excessBalanceRecipient = address(0); + bytes32 private constant UNLOCKED = bytes32(uint256(1)); bytes32 private constant LOCKED = bytes32(uint256(2)); @@ -249,16 +255,15 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { /// @notice Submits a fast bid /// @dev Will not revert /// @param fastGasPrice Bonus gasPrice rate that Searcher commits to pay to validator for gas used by searcher's call - /// @param executeOnLoss Boolean flag that enables Searcher calls to execute even if they lost the auction. + /// @param executeOnLoss Boolean flag that enables Searcher calls to execute even if they lost the auction. /// @param searcherToAddress Searcher contract address to be called on its `fastLaneCall` function. /// @param searcherCallData callData to be passed to `_searcherToAddress.fastLaneCall(fastPrice,msg.sender,callData)` function submitFastBid( uint256 fastGasPrice, // surplus gasprice commited to be paid at the end of execution bool executeOnLoss, // If true, execute even if searcher lost auction address searcherToAddress, - bytes calldata searcherCallData + bytes calldata searcherCallData ) external payable onlyEOA nonReentrant { - if (searcherToAddress == address(this) || searcherToAddress == msg.sender) revert RelaySearcherWrongParams(); PGAData memory existing_bid = fulfilledPGAMap[block.number]; @@ -268,30 +273,31 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { bool alreadyPaid = existing_bid.paid; // NOTE: These checks help mitigate the damage to searchers caused by relay error and adversarial validators by reverting - // early if the transactions are not sequenced pursuant to auction rules. + // early if the transactions are not sequenced pursuant to auction rules. - // Do not execute if a fastBid tx with a lower gasPrice was executed prior to this tx in the same block. - // NOTE: This edge case should only be achieveable via validator manipulation or erratic searcher nonce management + // Do not execute if a fastBid tx with a lower gasPrice was executed prior to this tx in the same block. + // NOTE: This edge case should only be achieveable via validator manipulation or erratic searcher nonce management if (lowestGasPrice != 0 && lowestGasPrice < tx.gasprice) { - emit RelayInvestigateOutcome(block.coinbase, msg.sender, block.number, lowestFastPrice, fastGasPrice, lowestGasPrice, tx.gasprice); - - // Do not execute if a fastBid tx with a lower bid amount was executed prior to this tx in the same block. + emit RelayInvestigateOutcome( + block.coinbase, msg.sender, block.number, lowestFastPrice, fastGasPrice, lowestGasPrice, tx.gasprice + ); + + // Do not execute if a fastBid tx with a lower bid amount was executed prior to this tx in the same block. } else if (lowestTotalPrice != 0 && lowestTotalPrice <= fastGasPrice + tx.gasprice) { - emit RelayInvestigateOutcome(block.coinbase, msg.sender, block.number, lowestFastPrice, fastGasPrice, lowestGasPrice, tx.gasprice); - - // Execute the tx if there are no issues w/ ordering. - // Execute the tx if the searcher enabled executeOnLoss or if the searcher won - } else if (executeOnLoss || !alreadyPaid) { + emit RelayInvestigateOutcome( + block.coinbase, msg.sender, block.number, lowestFastPrice, fastGasPrice, lowestGasPrice, tx.gasprice + ); + // Execute the tx if there are no issues w/ ordering. + // Execute the tx if the searcher enabled executeOnLoss or if the searcher won + } else if (executeOnLoss || !alreadyPaid) { // Use a try/catch pattern so that tx.gasprice and bidAmount can be saved to verify that - // proper transaction ordering is being followed. - try this.fastBidWrapper{value: msg.value}( - msg.sender, fastGasPrice, searcherToAddress, searcherCallData - ) returns (uint256 bidAmount) { - + // proper transaction ordering is being followed. + try this.fastBidWrapper{value: msg.value}(msg.sender, fastGasPrice, searcherToAddress, searcherCallData) + returns (uint256 bidAmount) { // Mark this auction as being complete to provide quicker reverts for subsequent searchers fulfilledPGAMap[block.number] = PGAData({ - lowestGasPrice: uint64(tx.gasprice), + lowestGasPrice: uint64(tx.gasprice), lowestFastPrice: uint64(fastGasPrice), lowestTotalPrice: uint64(fastGasPrice + tx.gasprice), paid: true @@ -300,27 +306,22 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { emit RelayFastBid(msg.sender, block.coinbase, true, bidAmount, searcherToAddress); return; // return early so that we don't refund the searcher's msg.value - } catch { // Update the auction to provide quicker reverts for subsequent searchers fulfilledPGAMap[block.number] = PGAData({ - lowestGasPrice: uint64(tx.gasprice), + lowestGasPrice: uint64(tx.gasprice), lowestFastPrice: uint64(fastGasPrice), lowestTotalPrice: uint64(fastGasPrice + tx.gasprice), paid: alreadyPaid // carry forward any previous wins in the block }); emit RelayFastBid(msg.sender, block.coinbase, false, 0, searcherToAddress); - } } if (msg.value > 0) { - // Refund the searcher any msg.value for failed txs. - SafeTransferLib.safeTransferETH( - msg.sender, - msg.value - ); + // Refund the searcher any msg.value for failed txs. + SafeTransferLib.safeTransferETH(msg.sender, msg.value); } } @@ -431,8 +432,10 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { _bidAmount = address(this).balance - balanceBefore; } - validatorsBalanceMap[block.coinbase] += _bidAmount; - validatorsTotal += _bidAmount; + uint256 amtPayableToValidator = _calculateValidatorShare(_bidAmount, STAKE_RATIO); + + validatorsBalanceMap[block.coinbase] += amtPayableToValidator; + validatorsTotal += amtPayableToValidator; return _bidAmount; } @@ -459,8 +462,10 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { } } - validatorsBalanceMap[block.coinbase] += _bidAmount; - validatorsTotal += _bidAmount; + uint256 amtPayableToValidator = _calculateValidatorShare(_bidAmount, STAKE_RATIO); + + validatorsBalanceMap[block.coinbase] += amtPayableToValidator; + validatorsTotal += amtPayableToValidator; return _bidAmount; } @@ -486,10 +491,11 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { // Calculate the split of payment uint256 validatorShare = (getValidatorRefundShare(block.coinbase) * bidAmount) / VALIDATOR_REFUND_SCALE; uint256 refundAmount = bidAmount - validatorShare; // subtract to ensure no overflow + uint256 amtPayableToValidator = _calculateValidatorShare(validatorShare, STAKE_RATIO); // Update balance and make payment - validatorsBalanceMap[block.coinbase] += validatorShare; - validatorsTotal += validatorShare; + validatorsBalanceMap[block.coinbase] += amtPayableToValidator; + validatorsTotal += amtPayableToValidator; payable(refundAddress).transfer(refundAmount); emit RelayFlashBidWithRefund( @@ -504,6 +510,18 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { ); } + /// @notice Internal, calculates shares + /// @param _amount Amount to calculates cuts from + /// @param _share Share bps + /// @return validatorCut Validator cut + function _calculateValidatorShare(uint256 _amount, uint256 _share) internal pure returns (uint256 validatorCut) { + validatorCut = (_amount * (VALIDATOR_SHARE_BASE - _share)) / VALIDATOR_SHARE_BASE; + } + + function getCurrentStakeRatio() public view returns (uint256) { + return STAKE_RATIO; + } + receive() external payable {} fallback() external payable {} @@ -514,23 +532,6 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { * |__________________________________ */ - /// @notice Syncs stuck matic to calling validator - /// @dev In the event something went really wrong / vuln report - function syncStuckNativeToken() external onlyActiveValidators nonReentrant { - uint256 _expectedBalance = validatorsTotal; - uint256 _currentBalance = address(this).balance; - if (_currentBalance >= _expectedBalance) { - address _validator = getValidator(); - - uint256 _surplus = _currentBalance - _expectedBalance; - - validatorsBalanceMap[_validator] += _surplus; - validatorsTotal += _surplus; - - emit RelayWithdrawStuckNativeToken(_validator, _surplus); - } - } - /// @notice Withdraws stuck ERC20 /// @dev In the event people send ERC20 instead of Matic we can send them back /// @param _tokenAddress Address of the stuck token @@ -544,6 +545,14 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { } } + /// @notice Updates the excess balance recipient address + /// @dev Callable by the current recipient only + /// @param newRecipient New recipient address + function updateExcessBalanceRecipient(address newRecipient) external { + require(msg.sender == excessBalanceRecipient, "Caller is not the current excess balance recipient"); + excessBalanceRecipient = newRecipient; + } + /** * | * | Validator Functions | @@ -563,6 +572,12 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents { validatorsBalanceMap[_validator] = 1; validatorsDataMap[_validator].blockOfLastWithdraw = uint64(block.number); SafeTransferLib.safeTransferETH(validatorPayee(_validator), payableBalance); + uint256 totalBalance = address(this).balance; + uint256 excessBalance = totalBalance - validatorsTotal; + if (excessBalance > 0) { + SafeTransferLib.safeTransferETH(excessBalanceRecipient, excessBalance); + emit RelayProcessingExcessBalance(excessBalanceRecipient, excessBalance); + } emit RelayProcessingPaidValidator(_validator, payableBalance, msg.sender); return payableBalance; } diff --git a/contracts/interfaces/IFastLaneAuctionHandler.sol b/contracts/interfaces/IFastLaneAuctionHandler.sol index ff634f7..f0096d2 100644 --- a/contracts/interfaces/IFastLaneAuctionHandler.sol +++ b/contracts/interfaces/IFastLaneAuctionHandler.sol @@ -54,7 +54,7 @@ interface IFastLaneAuctionHandler { ); event RelayValidatorPayeeUpdated(address validator, address payee, address indexed initiator); event RelayWithdrawStuckERC20(address indexed receiver, address indexed token, uint256 amount); - event RelayWithdrawStuckNativeToken(address indexed receiver, uint256 amount); + event RelayProcessingExcessBalance(address indexed receiver, uint256 amount); function clearValidatorPayee() external; function collectFees() external returns (uint256); @@ -85,9 +85,12 @@ interface IFastLaneAuctionHandler { address searcherToAddress, bytes memory searcherCallData ) external payable; - function submitFastBid(uint256 fastGasPrice, bool executeOnLoss, address searcherToAddress, bytes memory searcherCallData) - external - payable; + function submitFastBid( + uint256 fastGasPrice, + bool executeOnLoss, + address searcherToAddress, + bytes memory searcherCallData + ) external payable; function submitFlashBid( uint256 bidAmount, bytes32 oppTxHash, @@ -107,5 +110,6 @@ interface IFastLaneAuctionHandler { function validatorsBalanceMap(address) external view returns (uint256); function validatorsRefundShareMap(address) external view returns (uint256); function validatorsTotal() external view returns (uint256); + function withdrawStakeShare(address _recipient, uint256 _amount) external; function withdrawStuckERC20(address _tokenAddress) external; } diff --git a/test/PFL_AuctionHandler.t.sol b/test/PFL_AuctionHandler.t.sol index fee3b0e..1396cc7 100644 --- a/test/PFL_AuctionHandler.t.sol +++ b/test/PFL_AuctionHandler.t.sol @@ -354,9 +354,12 @@ contract PFLAuctionHandlerTest is PFLHelper, FastLaneAuctionHandlerEvents, Test function testCollectFees() public { vm.deal(SEARCHER_ADDRESS1, 100 ether); + address EXCESS_RECIPIENT = address(0); uint256 bidAmount = 2 ether; - uint256 expectedValidatorPayout = bidAmount - 1; + uint256 validatorCut = (bidAmount * (1_000_000 - 50_000)) / 1_000_000; + uint256 excessBalance = bidAmount - validatorCut; + uint256 expectedValidatorPayout = validatorCut - 1; bytes32 oppTx = bytes32("tx1"); bytes memory searcherUnusedData = abi.encodeWithSignature("unused()"); @@ -368,13 +371,20 @@ contract PFLAuctionHandlerTest is PFLHelper, FastLaneAuctionHandlerEvents, Test uint256 snap = vm.snapshot(); // As V1 pay itself + uint256 excessRecipientBalanceBefore = EXCESS_RECIPIENT.balance; uint256 balanceBefore = VALIDATOR1.balance; vm.expectEmit(true, true, true, true); emit RelayProcessingPaidValidator(VALIDATOR1, expectedValidatorPayout, VALIDATOR1); + emit RelayProcessingExcessBalance(address(0), excessBalance); vm.prank(VALIDATOR1); + uint256 returnedAmountPaid = PFR.collectFees(); uint256 actualAmountPaid = VALIDATOR1.balance - balanceBefore; + uint256 excessRecipientBalanceAfter = EXCESS_RECIPIENT.balance; + + // Excess balance was sent to the excess recipient + assertEq(excessRecipientBalanceAfter - excessRecipientBalanceBefore, excessBalance); // Validator actually got paid as expected assertEq(returnedAmountPaid, expectedValidatorPayout); @@ -665,35 +675,6 @@ contract PFLAuctionHandlerTest is PFLHelper, FastLaneAuctionHandlerEvents, Test assertEq(PFR.getValidatorPayee(VALIDATOR1), PAYEE1); } - function testSyncNativeTokenCanOnlyBeCalledByValidators() public { - _donateOneWeiToValidatorBalance(); - uint256 stuckNativeAmount = 1 ether; - vm.prank(USER); - address(PFR).call{value: stuckNativeAmount}(""); - - vm.prank(USER); - vm.expectRevert(FastLaneAuctionHandlerEvents.RelayNotActiveValidator.selector); - PFR.syncStuckNativeToken(); - - uint256 validatorBalanceBefore = PFR.getValidatorBalance(VALIDATOR1); - vm.prank(VALIDATOR1); - PFR.syncStuckNativeToken(); - uint256 validatorBalanceAfter = PFR.getValidatorBalance(VALIDATOR1); - assertEq(validatorBalanceAfter - validatorBalanceBefore, stuckNativeAmount); - } - - function testSyncNativeTokenDoesNotIncreaseBalanceIfNoExcess() public { - _donateOneWeiToValidatorBalance(); - uint256 auctionContractBalanceBefore = address(PFR).balance; - uint256 validatorBalanceBefore = PFR.getValidatorBalance(VALIDATOR1); - vm.prank(VALIDATOR1); - PFR.syncStuckNativeToken(); - uint256 auctionContractBalanceAfter = address(PFR).balance; - uint256 validatorBalanceAfter = PFR.getValidatorBalance(VALIDATOR1); - assertEq(validatorBalanceBefore, validatorBalanceAfter); - assertEq(auctionContractBalanceBefore, auctionContractBalanceAfter); - } - function testWithdrawStuckERC20CanOnlyBeCalledByValidators() public { _donateOneWeiToValidatorBalance(); uint256 stuckERC20Amount = 1 ether;