Skip to content

Commit

Permalink
Merge pull request #27 from bcnmy/fix/chainlight-005-withdraw-delay-a…
Browse files Browse the repository at this point in the history
…nd-min-deposit

Fix Chainlight 005 // Introduce withdrawal delay and min deposit
  • Loading branch information
livingrockrises authored Oct 23, 2024
2 parents 708b1ff + 36fb443 commit 21525d8
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 119 deletions.
30 changes: 30 additions & 0 deletions contracts/common/BiconomySponsorshipPaymasterErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ contract BiconomySponsorshipPaymasterErrors {
*/
error CanNotWithdrawZeroAmount();

/**
* @notice Throws when no request has been submitted
*/
error NoRequestSubmitted();

/**
* @notice Throws when trying unaccountedGas is too high
*/
Expand All @@ -81,4 +86,29 @@ contract BiconomySponsorshipPaymasterErrors {
* @notice Throws when postOp gas limit is too low
*/
error PostOpGasLimitTooLow();

/**
* @notice Thrown when deposit is too low to reach minDeposit
*/
error LowDeposit();

/**
* @notice Thrown when trying to withdraw more than the balance
*/
error InsufficientFundsInGasTank();

/**
* @notice Thrown when trying to execute withdrawal request before delay has passed
*/
error RequestNotClearedYet(uint256 clearanceTime);

/**
* @notice Thrown when trying to directly withdraw instead of submitting a request
*/
error SubmitRequestInstead();

/**
* @notice Thrown when the array lengths are not equal
*/
error InvalidArrayLengths();
}
11 changes: 11 additions & 0 deletions contracts/interfaces/IBiconomySponsorshipPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { PackedUserOperation } from "account-abstraction/core/UserOperationLib.sol";

interface IBiconomySponsorshipPaymaster {
struct WithdrawalRequest {
uint256 amount;
address to;
uint256 requestSubmittedTimestamp;
}

event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue);
event FixedPriceMarkupChanged(uint256 indexed oldValue, uint256 indexed newValue);
event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor);
Expand All @@ -14,6 +20,11 @@ interface IBiconomySponsorshipPaymaster {
event GasBalanceDeducted(address indexed _paymasterId, uint256 indexed _charge, uint256 indexed _premium);
event Received(address indexed sender, uint256 value);
event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor);
event WithdrawalRequestSubmitted(address withdrawAddress, uint256 amount);
event WithdrawalRequestCancelledFor(address paymasterId);
event TrustedPaymasterIdSet(address indexed paymasterId, bool isTrusted);
event EthWithdrawn(address indexed recipient, uint256 indexed amount);
event MinDepositChanged(uint256 indexed oldValue, uint256 indexed newValue);

function depositFor(address paymasterId) external payable;

Expand Down
186 changes: 141 additions & 45 deletions contracts/sponsorship/BiconomySponsorshipPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ contract BiconomySponsorshipPaymaster is
address public verifyingSigner;
address public feeCollector;
uint256 public unaccountedGas;
uint256 public paymasterIdWithdrawalDelay;
uint256 public minDeposit;

// Denominator to prevent precision errors when applying price markup
uint256 private constant _PRICE_DENOMINATOR = 1e6;
Expand All @@ -52,13 +54,17 @@ contract BiconomySponsorshipPaymaster is
uint256 private constant _UNACCOUNTED_GAS_LIMIT = 100_000;

mapping(address => uint256) public paymasterIdBalances;
mapping(address => bool) internal _trustedPaymasterIds;
mapping(address paymasterId => WithdrawalRequest request) internal _requests;

constructor(
address owner,
IEntryPoint entryPointArg,
address verifyingSignerArg,
address feeCollectorArg,
uint256 unaccountedGasArg
uint256 unaccountedGasArg,
uint256 paymasterIdWithdrawalDelayArg,
uint256 minDepositArg
)
BasePaymaster(owner, entryPointArg)
{
Expand All @@ -68,6 +74,8 @@ contract BiconomySponsorshipPaymaster is
}
feeCollector = feeCollectorArg;
unaccountedGas = unaccountedGasArg;
paymasterIdWithdrawalDelay = paymasterIdWithdrawalDelayArg;
minDeposit = minDepositArg;
}

receive() external payable {
Expand All @@ -82,6 +90,7 @@ contract BiconomySponsorshipPaymaster is
function depositFor(address paymasterId) external payable nonReentrant {
if (paymasterId == address(0)) revert PaymasterIdCanNotBeZero();
if (msg.value == 0) revert DepositCanNotBeZero();
if (paymasterIdBalances[paymasterId] + msg.value < minDeposit) revert LowDeposit();
paymasterIdBalances[paymasterId] += msg.value;
entryPoint.depositTo{ value: msg.value }(address(this));
emit GasDeposited(paymasterId, msg.value);
Expand All @@ -106,6 +115,45 @@ contract BiconomySponsorshipPaymaster is
emit VerifyingSignerChanged(oldSigner, newVerifyingSigner, msg.sender);
}

/**
* @dev Refund balances for multiple paymasterIds
* PM charges more than it should to protect itself.
* This function is used to refund the extra amount
* when the real consumption is known.
* @param paymasterIds The paymasterIds to refund
* @param amounts The amounts to refund
*/
function refundBalances(address[] calldata paymasterIds, uint256[] calldata amounts) external payable onlyOwner {
if (paymasterIds.length != amounts.length) revert InvalidArrayLengths();
for (uint256 i; i < paymasterIds.length; i++) {
paymasterIdBalances[paymasterIds[i]] += amounts[i];
}
}

/**
* @dev Set a new trusted paymasterId.
* Can only be called by the owner of the contract.
* @param paymasterId The paymasterId to be set as trusted.
* @param isTrusted Whether the paymasterId is trusted or not.
*/
function setTrustedPaymasterId(address paymasterId, bool isTrusted) external payable onlyOwner {
if (paymasterId == address(0)) revert PaymasterIdCanNotBeZero();
if (_trustedPaymasterIds[paymasterId] != isTrusted) {
_trustedPaymasterIds[paymasterId] = isTrusted;
emit TrustedPaymasterIdSet(paymasterId, isTrusted);
}
}

/**
* @dev Set a new minimum deposit value.
* Can only be called by the owner of the contract.
* @param newMinDeposit The new minimum deposit value to be set.
*/
function setMinDeposit(uint256 newMinDeposit) external payable onlyOwner {
minDeposit = newMinDeposit;
emit MinDepositChanged(minDeposit, newMinDeposit);
}

/**
* @dev Set a new fee collector address.
* Can only be called by the owner of the contract.
Expand Down Expand Up @@ -153,28 +201,58 @@ contract BiconomySponsorshipPaymaster is
}

/**
* @dev Withdraws the specified amount of gas tokens from the paymaster's balance and transfers them to the
* specified address.
* @param withdrawAddress The address to which the gas tokens should be transferred.
* @param amount The amount of gas tokens to withdraw.
* @dev Submit a withdrawal request for the paymasterId (Dapp Depositor address)
* @param withdrawAddress address to send the funds to
* @param amount amount to withdraw
*/
function withdrawTo(address payable withdrawAddress, uint256 amount) external override nonReentrant {
function submitWithdrawalRequest(address withdrawAddress, uint256 amount) external {
if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress();
if (amount == 0) revert CanNotWithdrawZeroAmount();
uint256 currentBalance = paymasterIdBalances[msg.sender];
if (amount > currentBalance) {
revert InsufficientFunds();
}
paymasterIdBalances[msg.sender] = currentBalance - amount;
entryPoint.withdrawTo(withdrawAddress, amount);
emit GasWithdrawn(msg.sender, withdrawAddress, amount);
if (amount > currentBalance) revert InsufficientFundsInGasTank();
_requests[msg.sender] =
WithdrawalRequest({ amount: amount, to: withdrawAddress, requestSubmittedTimestamp: block.timestamp });
emit WithdrawalRequestSubmitted(withdrawAddress, amount);
}

/**
* @dev Execute a withdrawal request for the paymasterId (Dapp Depositor address)
* Request must be cleared by the withdrawal delay period
* @param paymasterId paymasterId (Dapp Depositor address)
*/
function executeWithdrawalRequest(address paymasterId) external nonReentrant {
WithdrawalRequest memory req = _requests[paymasterId];
if (req.requestSubmittedTimestamp == 0) revert NoRequestSubmitted();
uint256 clearanceTimestamp = req.requestSubmittedTimestamp + _getDelay(paymasterId);
if (block.timestamp < clearanceTimestamp) revert RequestNotClearedYet(clearanceTimestamp);
uint256 currentBalance = paymasterIdBalances[paymasterId];
req.amount = req.amount > currentBalance ? currentBalance : req.amount;
if(req.amount == 0) revert CanNotWithdrawZeroAmount();
paymasterIdBalances[paymasterId] = currentBalance - req.amount;
delete _requests[paymasterId];
entryPoint.withdrawTo(payable(req.to), req.amount);
emit GasWithdrawn(paymasterId, req.to, req.amount);
}

/**
* @dev Cancel a withdrawal request for the paymasterId (Dapp Depositor address)
*/
function cancelWithdrawalRequest() external {
delete _requests[msg.sender];
emit WithdrawalRequestCancelledFor(msg.sender);
}

function withdrawEth(address payable recipient, uint256 amount) external payable onlyOwner nonReentrant {
(bool success,) = recipient.call{ value: amount }("");
if (!success) {
revert WithdrawalFailed();
}
emit EthWithdrawn(recipient, amount);
}

function withdrawTo(address payable withdrawAddress, uint256 amount) external virtual override {
(withdrawAddress, amount);
revert SubmitRequestInstead();
}

/**
Expand Down Expand Up @@ -245,8 +323,10 @@ contract BiconomySponsorshipPaymaster is
validUntil = uint48(bytes6(paymasterAndData[_PAYMASTER_ID_OFFSET + 20:_PAYMASTER_ID_OFFSET + 26]));
validAfter = uint48(bytes6(paymasterAndData[_PAYMASTER_ID_OFFSET + 26:_PAYMASTER_ID_OFFSET + 32]));
priceMarkup = uint32(bytes4(paymasterAndData[_PAYMASTER_ID_OFFSET + 32:_PAYMASTER_ID_OFFSET + 36]));
paymasterValidationGasLimit = uint128(bytes16(paymasterAndData[_PAYMASTER_VALIDATION_GAS_OFFSET:_PAYMASTER_POSTOP_GAS_OFFSET]));
paymasterPostOpGasLimit = uint128(bytes16(paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET : _PAYMASTER_DATA_OFFSET]));
paymasterValidationGasLimit =
uint128(bytes16(paymasterAndData[_PAYMASTER_VALIDATION_GAS_OFFSET:_PAYMASTER_POSTOP_GAS_OFFSET]));
paymasterPostOpGasLimit =
uint128(bytes16(paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET:_PAYMASTER_DATA_OFFSET]));
signature = paymasterAndData[_PAYMASTER_ID_OFFSET + 36:];
}
}
Expand All @@ -264,31 +344,33 @@ contract BiconomySponsorshipPaymaster is
internal
override
{
unchecked {
(address paymasterId, uint32 priceMarkup, uint256 prechargedAmount) =
abi.decode(context, (address, uint32, uint256));

// Include unaccountedGas since EP doesn't include this in actualGasCost
// unaccountedGas = postOpGas + EP overhead gas + estimated penalty
actualGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas);
// Apply the price markup
uint256 adjustedGasCost = (actualGasCost * priceMarkup) / _PRICE_DENOMINATOR;

uint256 premium = adjustedGasCost - actualGasCost;

// Add priceMarkup to fee collector balance
paymasterIdBalances[feeCollector] += premium;

if (prechargedAmount > adjustedGasCost) {
// If overcharged refund the excess
paymasterIdBalances[paymasterId] += (prechargedAmount - adjustedGasCost);
} else {
// deduct what needs to be deducted from paymasterId
paymasterIdBalances[paymasterId] -= (adjustedGasCost - prechargedAmount);
}
// here adjustedGasCost does not account for gasPenalty. prechargedAmount accounts for penalty with maxGasPenalty
emit GasBalanceDeducted(paymasterId, adjustedGasCost, premium);
(address paymasterId, uint32 priceMarkup, uint256 prechargedAmount) =
abi.decode(context, (address, uint32, uint256));

// Include unaccountedGas since EP doesn't include this in actualGasCost
// unaccountedGas = postOpGas + EP overhead gas + estimated penalty
actualGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas);
// Apply the price markup
uint256 adjustedGasCost = (actualGasCost * priceMarkup) / _PRICE_DENOMINATOR;

uint256 premium = adjustedGasCost - actualGasCost;

// Add priceMarkup to fee collector balance
paymasterIdBalances[feeCollector] += premium;

if (prechargedAmount > adjustedGasCost) {
// If overcharged refund the excess
paymasterIdBalances[paymasterId] += (prechargedAmount - adjustedGasCost);
} else {
// deduct what needs to be deducted from paymasterId
paymasterIdBalances[paymasterId] -= (adjustedGasCost - prechargedAmount);
}
// here adjustedGasCost does not account for gasPenalty. prechargedAmount accounts for penalty with maxGasPenalty
emit GasBalanceDeducted(paymasterId, adjustedGasCost, premium);

// here adjustedGasCost does not account for gasPenalty. prechargedAmount accounts for penalty with
// maxGasPenalty
emit GasBalanceDeducted(paymasterId, adjustedGasCost, premium);
}

/**
Expand All @@ -311,10 +393,17 @@ contract BiconomySponsorshipPaymaster is
returns (bytes memory context, uint256 validationData)
{
(userOpHash);
(address paymasterId, uint48 validUntil, uint48 validAfter, uint32 priceMarkup, uint128 paymasterValidationGasLimit, uint128 paymasterPostOpGasLimit, bytes calldata signature) =
parsePaymasterAndData(userOp.paymasterAndData);
(
address paymasterId,
uint48 validUntil,
uint48 validAfter,
uint32 priceMarkup,
uint128 paymasterValidationGasLimit,
uint128 paymasterPostOpGasLimit,
bytes calldata signature
) = parsePaymasterAndData(userOp.paymasterAndData);
(paymasterValidationGasLimit, paymasterPostOpGasLimit);

//ECDSA library supports both 64 and 65-byte long signatures.
// we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and
// not "ECDSA"
Expand Down Expand Up @@ -344,13 +433,15 @@ contract BiconomySponsorshipPaymaster is

// callGasLimit + paymasterPostOpGas
uint256 maxPenalty = (
uint128(uint256(userOp.accountGasLimits)) +
uint128(bytes16(userOp.paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET : _PAYMASTER_DATA_OFFSET]))
) * 10 * userOp.unpackMaxFeePerGas() / 100;
(
uint128(uint256(userOp.accountGasLimits))
+ uint128(bytes16(userOp.paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET:_PAYMASTER_DATA_OFFSET]))
) * 10 * userOp.unpackMaxFeePerGas()
) / 100;

// Deduct the max gas cost.
uint256 effectiveCost =
((requiredPreFund + unaccountedGas * userOp.unpackMaxFeePerGas()) * priceMarkup / _PRICE_DENOMINATOR);
(((requiredPreFund + unaccountedGas * userOp.unpackMaxFeePerGas()) * priceMarkup) / _PRICE_DENOMINATOR);

if (effectiveCost + maxPenalty > paymasterIdBalances[paymasterId]) {
revert InsufficientFundsForPaymasterId();
Expand Down Expand Up @@ -386,6 +477,11 @@ contract BiconomySponsorshipPaymaster is
}
}

function _getDelay(address paymasterId) internal view returns (uint256) {
if (_trustedPaymasterIds[paymasterId]) return 0;
return paymasterIdWithdrawalDelay;
}

function _withdrawERC20(IERC20 token, address target, uint256 amount) private {
if (target == address(0)) revert CanNotWithdrawToZeroAddress();
SafeTransferLib.safeTransfer(address(token), target, amount);
Expand Down
Loading

0 comments on commit 21525d8

Please sign in to comment.