Skip to content

Commit

Permalink
price+markup
Browse files Browse the repository at this point in the history
  • Loading branch information
Filipp Makarov authored and Filipp Makarov committed Dec 5, 2024
1 parent b45fc80 commit 0d72fa4
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 130 deletions.
5 changes: 0 additions & 5 deletions contracts/common/BiconomyTokenPaymasterErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,4 @@ contract BiconomyTokenPaymasterErrors {
* @notice Throws when PM was not able to charge user
*/
error FailedToChargeTokens(address account, address token, uint256 amount, bytes32 userOpHash);

/**
* Throws when account has insufficient token balance to pay for gas
*/
error InsufficientTokenBalance(address account, address token, uint256 amount, bytes32 userOpHash);
}
10 changes: 2 additions & 8 deletions contracts/interfaces/IBiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface IBiconomyTokenPaymaster {
event UpdatedFeeCollector(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor);
event UpdatedPriceExpiryDuration(uint256 indexed oldValue, uint256 indexed newValue);

event PaidGasInTokensIndependent(
event PaidGasInTokens(
address indexed userOpSender,
address indexed token,
uint256 gasCostBeforePostOpAndPenalty,
Expand All @@ -32,13 +32,7 @@ interface IBiconomyTokenPaymaster {
uint256 tokenPrice,
bytes32 userOpHash
);
event PaidGasInTokensExternal(
address indexed userOpSender,
address indexed token,
uint256 tokenAmount,
bytes32 userOpHash
);


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

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

function parseIndependentModeSpecificData(
Expand Down
156 changes: 61 additions & 95 deletions contracts/token/BiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { IOracle } from "../interfaces/oracles/IOracle.sol";
import { TokenPaymasterParserLib } from "../libraries/TokenPaymasterParserLib.sol";
import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol";
import { ECDSA as ECDSA_solady } from "solady/utils/ECDSA.sol";
import "account-abstraction/core/Helpers.sol";
import { Uniswapper, IV3SwapRouter } from "./swaps/Uniswapper.sol";
import "account-abstraction/core/Helpers.sol";

/**
* @title BiconomyTokenPaymaster
Expand Down Expand Up @@ -391,7 +391,8 @@ contract BiconomyTokenPaymaster is
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint256 estimatedTokenAmount
uint256 tokenPrice,
uint32 appliedPriceMarkup
)
public
view
Expand All @@ -414,7 +415,8 @@ contract BiconomyTokenPaymaster is
validUntil,
validAfter,
tokenAddress,
estimatedTokenAmount
tokenPrice,
appliedPriceMarkup
)
);
}
Expand Down Expand Up @@ -477,6 +479,11 @@ contract BiconomyTokenPaymaster is
revert InvalidPaymasterMode();
}

uint256 maxPenalty = (
( uint128(uint256(userOp.accountGasLimits))
+ uint128(bytes16(userOp.paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET:_PAYMASTER_DATA_OFFSET]))
) * 10 ) / 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
Expand All @@ -485,7 +492,8 @@ contract BiconomyTokenPaymaster is
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint256 estimatedTokenAmount,
uint256 tokenPrice,
uint32 externalPriceMarkup,
bytes memory signature
) = modeSpecificData.parseExternalModeSpecificData();

Expand All @@ -495,7 +503,7 @@ contract BiconomyTokenPaymaster is

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

if(IERC20(tokenAddress).balanceOf(userOp.sender) < estimatedTokenAmount) {
revert InsufficientTokenBalance(userOp.sender, tokenAddress, estimatedTokenAmount, userOpHash);
}

context = abi.encodePacked(
PaymasterMode.EXTERNAL,
abi.encode(
userOp.sender,
tokenAddress,
estimatedTokenAmount,
userOpHash
)
context = abi.encode(
userOp.sender,
tokenAddress,
maxPenalty,
tokenPrice,
externalPriceMarkup,
userOpHash
);
validationData = _packValidationData(false, validUntil, validAfter);

/// INDEPENDENT MODE
} else if (mode == PaymasterMode.INDEPENDENT) {

address tokenAddress = modeSpecificData.parseIndependentModeSpecificData();

// 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);

if(tokenPrice == 0) {
revert TokenNotSupported();
}
uint256 tokenAmount;
uint32 priceMarkup = independentTokenDirectory[tokenAddress].priceMarkup;

{
// Calculate token amount to precharge
uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp);
tokenAmount = ((maxCost + maxPenalty + (unaccountedGas * maxFeePerGas)) * priceMarkup * tokenPrice)
/ (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR);
}

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

context = abi.encodePacked(
PaymasterMode.INDEPENDENT,
abi.encode(
userOp.sender,
tokenAddress,
maxPenalty,
tokenPrice,
priceMarkup,
userOpHash
)
);
context = abi.encode(
userOp.sender,
tokenAddress,
maxPenalty,
uint256(0), // pass 0, so we can check the price in _postOp and be 4337 compliant
independentTokenDirectory[tokenAddress].priceMarkup,
userOpHash
);
validationData = 0; // Validation success and price is valid indefinetly
}
}
Expand All @@ -588,45 +561,38 @@ contract BiconomyTokenPaymaster is
internal
override
{
PaymasterMode pmMode = PaymasterMode(uint8(context[0]));
if (pmMode == 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 (pmMode == 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);
(
address userOpSender,
address tokenAddress,
uint256 maxPenalty,
uint256 tokenPrice,
uint32 appliedPriceMarkup,
bytes32 userOpHash
) = abi.decode(context, (address, address, uint256, uint256, uint32, bytes32));

// If tokenPrice is 0, it means it was not set in the validatePaymasterUserOp => independent mode
// So we need to get the price of the token from the oracle now
if(tokenPrice == 0) {
tokenPrice = _getPrice(tokenAddress);
// if tokenPrice is still 0, it means the token is not supported
if(tokenPrice == 0) {
revert TokenNotSupported();
}
}

// 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 PaidGasInTokens(userOpSender, tokenAddress, actualGasCost, tokenAmount, appliedPriceMarkup, tokenPrice, userOpHash);
} else {
revert FailedToChargeTokens(userOpSender, tokenAddress, tokenAmount, userOpHash);
}
}

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

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

Expand All @@ -353,7 +355,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.estimatedTokenAmount);
paymaster.getHash(userOp, pmData.validUntil, pmData.validAfter, pmData.tokenAddress, pmData.tokenPrice, pmData.appliedPriceMarkup);

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

function parseIndependentModeSpecificData(bytes calldata modeSpecificData) external pure returns (address tokenAddress) {
Expand Down
4 changes: 2 additions & 2 deletions test/unit/concrete/TestTokenPaymaster.Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ contract TestTokenPaymasterBase is TestBase {
deal(address(usdc), address(ALICE_ACCOUNT), 100e6);

vm.startPrank(PAYMASTER_OWNER.addr);
tokenPaymaster.setUnaccountedGas(40_000);
tokenPaymaster.setUnaccountedGas(80_000);
vm.stopPrank();

uint256 initialBundlerBalance = BUNDLER.addr.balance;
Expand Down Expand Up @@ -113,7 +113,7 @@ contract TestTokenPaymasterBase is TestBase {
ops[0] = userOp;

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

uint256 customGasPrice = 3e6;
startPrank(BUNDLER.addr);
Expand Down
Loading

0 comments on commit 0d72fa4

Please sign in to comment.