Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 68 additions & 53 deletions contracts/auction-handler/FastLaneAuctionHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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));

Expand Down Expand Up @@ -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];
Expand All @@ -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
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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(
Expand All @@ -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 {}
Expand All @@ -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
Expand All @@ -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 |
Expand All @@ -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;
}
Expand Down
12 changes: 8 additions & 4 deletions contracts/interfaces/IFastLaneAuctionHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
41 changes: 11 additions & 30 deletions test/PFL_AuctionHandler.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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()");

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down