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

Charge user in postOp #43

Closed
wants to merge 5 commits into from
Closed
Changes from 1 commit
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
Next Next commit
compiles
Filipp Makarov authored and Filipp Makarov committed Dec 4, 2024
commit 713d31d038e7f80565ab160381d2b08801a55c34
10 changes: 8 additions & 2 deletions contracts/common/BiconomyTokenPaymasterErrors.sol
Original file line number Diff line number Diff line change
@@ -81,8 +81,14 @@ contract BiconomyTokenPaymasterErrors {
*/
error WithdrawalFailed();


/**
* @notice Throws when PM was not able to charge user
*/
error FailedToChargeTokens(address account, address token, uint256 amount, bytes32 userOpHash);
filmakarov marked this conversation as resolved.
Show resolved Hide resolved

/**
* @notice Emitted when ETH is withdrawn from the paymaster
* Throws when account has insufficient token balance to pay for gas
*/
event EthWithdrawn(address indexed recipient, uint256 indexed amount);
error InsufficientTokenBalance(address account, address token, uint256 amount, bytes32 userOpHash);
}
12 changes: 10 additions & 2 deletions contracts/interfaces/IBiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
@@ -8,7 +8,6 @@ interface IBiconomyTokenPaymaster {
enum PaymasterMode {
EXTERNAL, // Price provided by external service. Authenticated using signature from verifyingSigner
INDEPENDENT // Price queried from oracle. No signature needed from external service.

}

// Struct for storing information about the token
@@ -26,7 +25,7 @@ interface IBiconomyTokenPaymaster {
event TokensRefunded(
address indexed userOpSender, address indexed token, uint256 refundAmount, bytes32 indexed userOpHash
);
event PaidGasInTokens(
event PaidGasInTokensIndependent(
address indexed userOpSender,
address indexed token,
uint256 nativeCharge,
@@ -35,6 +34,15 @@ interface IBiconomyTokenPaymaster {
uint256 tokenPrice,
bytes32 indexed userOpHash
);
event PaidGasInTokensExternal(
address indexed userOpSender,
address indexed token,
uint256 tokenAmount,
bytes32 indexed userOpHash
filmakarov marked this conversation as resolved.
Show resolved Hide resolved
);

event EthWithdrawn(address indexed recipient, uint256 indexed amount);

event Received(address indexed sender, uint256 value);
event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor);
event AddedToTokenDirectory(address indexed tokenAddress, IOracle indexed oracle, uint8 decimals);
8 changes: 3 additions & 5 deletions contracts/libraries/TokenPaymasterParserLib.sol
Original file line number Diff line number Diff line change
@@ -31,17 +31,15 @@ library TokenPaymasterParserLib {
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint256 tokenPrice,
uint32 externalPriceMarkup,
uint256 estimatedTokenAmount,
bytes calldata signature
)
{
validUntil = uint48(bytes6(modeSpecificData[:6]));
validAfter = uint48(bytes6(modeSpecificData[6:12]));
tokenAddress = address(bytes20(modeSpecificData[12:32]));
tokenPrice = uint256(bytes32(modeSpecificData[32:64]));
externalPriceMarkup = uint32(bytes4(modeSpecificData[64:68]));
signature = modeSpecificData[68:];
estimatedTokenAmount = uint256(bytes32(modeSpecificData[32:64]));
signature = modeSpecificData[64:];
}

function parseIndependentModeSpecificData(
123 changes: 64 additions & 59 deletions contracts/token/BiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
@@ -391,8 +391,7 @@ contract BiconomyTokenPaymaster is
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint256 tokenPrice,
uint32 externalPriceMarkup
uint256 estimatedTokenAmount
)
public
view
@@ -415,8 +414,7 @@ contract BiconomyTokenPaymaster is
validUntil,
validAfter,
tokenAddress,
tokenPrice,
externalPriceMarkup
estimatedTokenAmount
)
);
}
@@ -479,14 +477,6 @@ contract BiconomyTokenPaymaster is
revert InvalidPaymasterMode();
}

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

if (mode == PaymasterMode.EXTERNAL) {
// Use the price and other params specified in modeSpecificData by the verifyingSigner
// Useful for supporting tokens which don't have oracle support
@@ -495,8 +485,7 @@ contract BiconomyTokenPaymaster is
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint256 tokenPrice, // Note: what backend should pass is nativeTokenPriceInUsd/tokenPriceInUsd * 10^token decimals
uint32 externalPriceMarkup,
uint256 estimatedTokenAmount,
bytes memory signature
) = modeSpecificData.parseExternalModeSpecificData();

@@ -506,7 +495,7 @@ contract BiconomyTokenPaymaster is

bool validSig = verifyingSigner.isValidSignatureNow(
ECDSA_solady.toEthSignedMessageHash(
getHash(userOp, validUntil, validAfter, tokenAddress, tokenPrice, externalPriceMarkup)
getHash(userOp, validUntil, validAfter, tokenAddress, estimatedTokenAmount)
),
signature
);
@@ -516,38 +505,33 @@ contract BiconomyTokenPaymaster is
return ("", _packValidationData(true, validUntil, validAfter));
}

if (externalPriceMarkup > _MAX_PRICE_MARKUP || externalPriceMarkup < _PRICE_DENOMINATOR) {
revert InvalidPriceMarkup();
}


uint256 tokenAmount;
{
uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp);
tokenAmount = ((maxCost + maxPenalty + (unaccountedGas * maxFeePerGas)) * externalPriceMarkup * tokenPrice)
/ (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR);
if(IERC20(tokenAddress).balanceOf(userOp.sender) < estimatedTokenAmount) {
revert InsufficientTokenBalance(userOp.sender, tokenAddress, estimatedTokenAmount, userOpHash);
}

// Transfer full amount to this address. Unused amount will be refunded in postOP
SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount);

// deduct max penalty from the token amount we pass to the postOp
// so we don't refund it at postOp
context = abi.encode(
mode,
userOp.sender,
tokenAddress,
tokenAmount-((maxPenalty*tokenPrice*externalPriceMarkup)/(_NATIVE_TOKEN_DECIMALS*_PRICE_DENOMINATOR)),
tokenPrice,
externalPriceMarkup,
estimatedTokenAmount,
userOpHash
);
validationData = _packValidationData(false, validUntil, validAfter);

/// INDEPENDENT MODE
} else if (mode == PaymasterMode.INDEPENDENT) {
// Use only oracles for the token specified in modeSpecificData
if (modeSpecificData.length != 20) {
revert InvalidTokenAddress();
}

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

// Get address for token used to pay
address tokenAddress = modeSpecificData.parseIndependentModeSpecificData();
uint256 tokenPrice = _getPrice(tokenAddress);
@@ -566,13 +550,17 @@ contract BiconomyTokenPaymaster is
}

// Transfer full amount to this address. Unused amount will be refunded in postOP
SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount);
// SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount);
if(IERC20(tokenAddress).balanceOf(userOp.sender) < tokenAmount) {
revert InsufficientTokenBalance(userOp.sender, tokenAddress, tokenAmount, userOpHash);
}

context =
abi.encode(
mode,
userOp.sender,
tokenAddress,
tokenAmount-((maxPenalty*tokenPrice*priceMarkup)/(_NATIVE_TOKEN_DECIMALS*_PRICE_DENOMINATOR)),
maxPenalty,
tokenPrice,
priceMarkup,
userOpHash
@@ -596,31 +584,48 @@ contract BiconomyTokenPaymaster is
)
internal
override
{
// Decode context data
(
address userOpSender,
address tokenAddress,
uint256 prechargedAmount,
uint256 tokenPrice,
uint32 appliedPriceMarkup,
bytes32 userOpHash
) = abi.decode(context, (address, address, uint256, uint256, uint32, bytes32));

// Calculate the actual cost in tokens based on the actual gas cost and the token price
uint256 actualTokenAmount = (
(actualGasCost + (unaccountedGas * actualUserOpFeePerGas)) * appliedPriceMarkup * tokenPrice
) / (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR);
if (prechargedAmount > actualTokenAmount) {
// If the user was overcharged, refund the excess tokens
uint256 refundAmount = prechargedAmount - actualTokenAmount;
SafeTransferLib.safeTransfer(tokenAddress, userOpSender, refundAmount);
emit TokensRefunded(userOpSender, tokenAddress, refundAmount, userOpHash);
}
{

PaymasterMode mode = PaymasterMode(uint8(context[0]));

emit PaidGasInTokens(
userOpSender, tokenAddress, actualGasCost, actualTokenAmount, appliedPriceMarkup, tokenPrice, userOpHash
);
if (mode == PaymasterMode.EXTERNAL) {
// Decode context data
(
address userOpSender,
address tokenAddress,
uint256 estimatedTokenAmount,
bytes32 userOpHash
) = abi.decode(context[1:], (address, address, uint256, bytes32));

if (SafeTransferLib.trySafeTransferFrom(tokenAddress, userOpSender, address(this), estimatedTokenAmount)) {
emit PaidGasInTokensExternal(userOpSender, tokenAddress, estimatedTokenAmount, userOpHash);
} else {
revert FailedToChargeTokens(userOpSender, tokenAddress, estimatedTokenAmount, userOpHash);
}

} else if (mode == PaymasterMode.INDEPENDENT) {
(
address userOpSender,
address tokenAddress,
uint256 maxPenalty,
uint256 tokenPrice,
uint32 appliedPriceMarkup,
bytes32 userOpHash
) = abi.decode(context[1:], (address, address, uint256, uint256, uint32, bytes32));
// Calculate the amount to charge. unaccountedGas and maxPenalty are used, as we do not know the exact gas spent for postop and actual penalty at this point
// this is obviously overcharge, however, the excess amount can be refunded by backend, when we know the exact gas spent (emitted by EP after executing UserOp)
uint256 tokenAmount = (
(actualGasCost + ((unaccountedGas + maxPenalty)) * actualUserOpFeePerGas)) * appliedPriceMarkup * tokenPrice
/ (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR);

if (SafeTransferLib.trySafeTransferFrom(tokenAddress, userOpSender, address(this), tokenAmount)) {
emit PaidGasInTokensIndependent(
userOpSender, tokenAddress, actualGasCost, tokenAmount, appliedPriceMarkup, tokenPrice, userOpHash
);
} else {
revert FailedToChargeTokens(userOpSender, tokenAddress, tokenAmount, userOpHash);
}
}
}

function _validateTokenInfo(TokenInfo memory tokenInfo) internal view {
11 changes: 4 additions & 7 deletions test/base/TestBase.sol
Original file line number Diff line number Diff line change
@@ -63,8 +63,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors {
uint48 validUntil;
uint48 validAfter;
address tokenAddress;
uint256 tokenPrice;
uint32 externalPriceMarkup;
uint256 estimatedTokenAmount;
}

// Used to buffer user op gas limits
@@ -342,8 +341,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors {
pmData.validUntil,
pmData.validAfter,
pmData.tokenAddress,
pmData.tokenPrice,
pmData.externalPriceMarkup,
pmData.estimatedTokenAmount,
new bytes(65) // Zero signature
);

@@ -352,7 +350,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors {

// Generate hash to be signed
bytes32 paymasterHash =
paymaster.getHash(userOp, pmData.validUntil, pmData.validAfter, pmData.tokenAddress, pmData.tokenPrice, pmData.externalPriceMarkup);
paymaster.getHash(userOp, pmData.validUntil, pmData.validAfter, pmData.tokenAddress, pmData.estimatedTokenAmount);

// Sign the hash
signature = signMessage(signer, paymasterHash);
@@ -367,8 +365,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors {
pmData.validUntil,
pmData.validAfter,
pmData.tokenAddress,
pmData.tokenPrice,
pmData.externalPriceMarkup,
pmData.estimatedTokenAmount,
signature
);
}
3 changes: 1 addition & 2 deletions test/mocks/PaymasterParserLibExposed.sol
Original file line number Diff line number Diff line change
@@ -15,8 +15,7 @@ library PaymasterParserLibExposed {
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint256 tokenPrice,
uint32 externalPriceMarkup,
uint256 estimatedTokenAmount,
bytes calldata signature
) {
return modeSpecificData.parseExternalModeSpecificData();
5 changes: 2 additions & 3 deletions test/mocks/PaymasterParserLibWrapper.sol
Original file line number Diff line number Diff line change
@@ -15,11 +15,10 @@ contract PaymasterParserLibWrapper {
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint256 tokenPrice,
uint32 externalPriceMarkup,
uint256 estimatedTokenAmount,
bytes memory signature
) {
(validUntil, validAfter, tokenAddress, tokenPrice, externalPriceMarkup, signature) = modeSpecificData.parseExternalModeSpecificData();
(validUntil, validAfter, tokenAddress, estimatedTokenAmount, signature) = modeSpecificData.parseExternalModeSpecificData();
}

function parseIndependentModeSpecificData(bytes calldata modeSpecificData) external pure returns (address tokenAddress) {
2 changes: 1 addition & 1 deletion test/unit/concrete/TestTokenPaymaster.Base.t.sol
Original file line number Diff line number Diff line change
@@ -115,7 +115,7 @@ contract TestTokenPaymasterBase is TestBase {
emit IBiconomyTokenPaymaster.TokensRefunded(address(ALICE_ACCOUNT), address(usdc), 0, bytes32(0));

vm.expectEmit(true, true, false, false, address(tokenPaymaster));
emit IBiconomyTokenPaymaster.PaidGasInTokens(address(ALICE_ACCOUNT), address(usdc), 0, 0, 1e6, 0, bytes32(0));
emit IBiconomyTokenPaymaster.PaidGasInTokensIndependent(address(ALICE_ACCOUNT), address(usdc), 0, 0, 1e6, 0, bytes32(0));

uint256 customGasPrice = 3e6;
startPrank(BUNDLER.addr);
15 changes: 5 additions & 10 deletions test/unit/concrete/TestTokenPaymaster.t.sol
Original file line number Diff line number Diff line change
@@ -309,7 +309,6 @@ contract TestTokenPaymaster is TestBase {
vm.stopPrank();

PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE));
uint128 tokenPrice = 1e18; // Assume 1 token = 1 native token = 1 native?

TokenPaymasterData memory pmData = TokenPaymasterData({
paymasterValGasLimit: 3e6,
@@ -318,8 +317,7 @@ contract TestTokenPaymaster is TestBase {
validUntil: uint48(block.timestamp + 1 days),
validAfter: uint48(block.timestamp),
tokenAddress: address(testToken),
tokenPrice: tokenPrice,
externalPriceMarkup: 1e6
estimatedTokenAmount: 999*1e18
});

(bytes memory paymasterAndData,) = generateAndSignTokenPaymasterData(
@@ -354,7 +352,6 @@ contract TestTokenPaymaster is TestBase {
vm.stopPrank();

PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE));
uint128 tokenPrice = 1e18;

TokenPaymasterData memory pmData = TokenPaymasterData({
paymasterValGasLimit: 3e6,
@@ -363,8 +360,7 @@ contract TestTokenPaymaster is TestBase {
validUntil: uint48(block.timestamp + 1 days),
validAfter: uint48(block.timestamp),
tokenAddress: address(testToken),
tokenPrice: tokenPrice,
externalPriceMarkup: 1e6
estimatedTokenAmount: 999*1e18
});

// Create a valid paymasterAndData
@@ -424,8 +420,7 @@ contract TestTokenPaymaster is TestBase {
validUntil: validUntil,
validAfter: validAfter,
tokenAddress: address(testToken),
tokenPrice: tokenPrice,
externalPriceMarkup: externalPriceMarkup
estimatedTokenAmount: 1e18
});

// Generate and sign the token paymaster data
@@ -446,7 +441,7 @@ contract TestTokenPaymaster is TestBase {
emit IBiconomyTokenPaymaster.TokensRefunded(address(ALICE_ACCOUNT), address(testToken), 0, bytes32(0));

vm.expectEmit(true, true, false, false, address(tokenPaymaster));
emit IBiconomyTokenPaymaster.PaidGasInTokens(address(ALICE_ACCOUNT), address(testToken), 0, 0, 1e6, 0, bytes32(0));
emit IBiconomyTokenPaymaster.PaidGasInTokensExternal(address(ALICE_ACCOUNT), address(testToken), 0, bytes32(0));

// Execute the operation
startPrank(BUNDLER.addr);
@@ -503,7 +498,7 @@ contract TestTokenPaymaster is TestBase {
vm.expectEmit(true, true, false, false, address(tokenPaymaster));
emit IBiconomyTokenPaymaster.TokensRefunded(address(ALICE_ACCOUNT), address(testToken), 0, bytes32(0));
vm.expectEmit(true, true, false, false, address(tokenPaymaster));
emit IBiconomyTokenPaymaster.PaidGasInTokens(address(ALICE_ACCOUNT), address(testToken), 0, 0, 1e6, 0, bytes32(0));
emit IBiconomyTokenPaymaster.PaidGasInTokensIndependent(address(ALICE_ACCOUNT), address(testToken), 0, 0, 1e6, 0, bytes32(0));
startPrank(BUNDLER.addr);
uint256 gasValue = gasleft();
ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr));
12 changes: 4 additions & 8 deletions test/unit/concrete/TestTokenPaymasterParserLib.t.sol
Original file line number Diff line number Diff line change
@@ -70,17 +70,15 @@ contract TestTokenPaymasterParserLib is Test {
uint48 expectedValidUntil = uint48(block.timestamp + 1 days);
uint48 expectedValidAfter = uint48(block.timestamp);
address expectedTokenAddress = address(0x1234567890AbcdEF1234567890aBcdef12345678);
uint256 expectedTokenPrice = 1e8;
uint32 expectedExternalPriceMarkup = 1e6;
uint256 expectedEstimatedTokenAmount = 256*1e8;
bytes memory expectedSignature = hex"abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef";

// Construct external mode specific data
bytes memory externalModeSpecificData = abi.encodePacked(
bytes6(abi.encodePacked(expectedValidUntil)),
bytes6(abi.encodePacked(expectedValidAfter)),
bytes20(expectedTokenAddress),
bytes32(abi.encodePacked(expectedTokenPrice)),
bytes4(abi.encodePacked(expectedExternalPriceMarkup)),
bytes32(abi.encodePacked(expectedEstimatedTokenAmount)),
expectedSignature
);

@@ -89,17 +87,15 @@ contract TestTokenPaymasterParserLib is Test {
uint48 parsedValidUntil,
uint48 parsedValidAfter,
address parsedTokenAddress,
uint256 parsedTokenPrice,
uint32 parsedExternalPriceMarkup,
uint256 parsedEstimatedTokenAmount,
bytes memory parsedSignature
) = parser.parseExternalModeSpecificData(externalModeSpecificData);

// Validate the parsed values
assertEq(parsedValidUntil, expectedValidUntil, "ValidUntil should match");
assertEq(parsedValidAfter, expectedValidAfter, "ValidAfter should match");
assertEq(parsedTokenAddress, expectedTokenAddress, "Token address should match");
assertEq(parsedTokenPrice, expectedTokenPrice, "Token price should match");
assertEq(parsedExternalPriceMarkup, expectedExternalPriceMarkup, "Dynamic adjustment should match");
assertEq(parsedEstimatedTokenAmount, expectedEstimatedTokenAmount, "Estimated Token amount should match");
assertEq(parsedSignature, expectedSignature, "Signature should match");
}