Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Excess balance handling #27

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
70 changes: 45 additions & 25 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 @@ -154,6 +154,12 @@ contract FastLaneAuctionHandler is FastLaneAuctionHandlerEvents {

/// @notice Map[validator] = % payment to validator in a bid with refund
mapping(address => int256) private validatorsRefundShareMap;

uint24 internal constant VALIDATOR_SHARE_BASE = 1_000_000;
uint24 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 @@ -431,8 +437,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 +467,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 +496,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 +515,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, uint24 _share) internal pure returns (uint256 validatorCut) {
validatorCut = (_amount * (VALIDATOR_SHARE_BASE - _share)) / VALIDATOR_SHARE_BASE;
}

function getCurrentStakeRatio() public view returns (uint24) {
return STAKE_RATIO;
}

receive() external payable {}

fallback() external payable {}
Expand All @@ -514,23 +537,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 +550,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 +577,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
3 changes: 2 additions & 1 deletion 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 @@ -107,5 +107,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
Loading