Skip to content

Commit

Permalink
tests and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
ShivaanshK committed Sep 10, 2024
1 parent 86e2c75 commit 735f2a6
Show file tree
Hide file tree
Showing 6 changed files with 470 additions and 14 deletions.
4 changes: 2 additions & 2 deletions contracts/interfaces/IBiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ interface IBiconomyTokenPaymaster {
event UpdatedVerifyingSigner(address indexed oldSigner, address indexed newSigner, address indexed actor);
event UpdatedFeeCollector(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor);
event UpdatedPriceExpiryDuration(uint256 indexed oldValue, uint256 indexed newValue);
event TokensRefunded(address indexed userOpSender, uint256 refundAmount, bytes32 indexed userOpHash);
event TokensRefunded(address indexed userOpSender, address indexed token, uint256 refundAmount, bytes32 indexed userOpHash);
event PaidGasInTokens(
address indexed userOpSender, uint256 charge, uint256 dynamicAdjustment, bytes32 indexed userOpHash
address indexed userOpSender, address indexed token, uint256 nativeCharge, uint256 tokenCharge, uint256 dynamicAdjustment, bytes32 indexed userOpHash
);
event Received(address indexed sender, uint256 value);
event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.s
import "@account-abstraction/contracts/core/UserOperationLib.sol";

// A helper library to parse paymaster and data
library PaymasterParser {
library TokenPaymasterParserLib {
// Start offset of mode in PND
uint256 private constant PAYMASTER_MODE_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET;

Expand All @@ -15,10 +15,8 @@ library PaymasterParser {
returns (IBiconomyTokenPaymaster.PaymasterMode mode, bytes memory modeSpecificData)
{
unchecked {
mode = IBiconomyTokenPaymaster.PaymasterMode(
uint8(bytes1(paymasterAndData[PAYMASTER_MODE_OFFSET:PAYMASTER_MODE_OFFSET + 8]))
);
modeSpecificData = paymasterAndData[PAYMASTER_MODE_OFFSET + 8:];
mode = IBiconomyTokenPaymaster.PaymasterMode(uint8(bytes1(paymasterAndData[PAYMASTER_MODE_OFFSET])));
modeSpecificData = paymasterAndData[PAYMASTER_MODE_OFFSET + 1:];
}
}

Expand Down
16 changes: 9 additions & 7 deletions contracts/token/BiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { BasePaymaster } from "../base/BasePaymaster.sol";
import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol";
import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol";
import { IOracle } from "../interfaces/oracles/IOracle.sol";
import { PaymasterParser } from "../libraries/PaymasterParser.sol";
import { TokenPaymasterParserLib } from "../libraries/TokenPaymasterParserLib.sol";
import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol";
import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol";
import "@account-abstraction/contracts/core/Helpers.sol";
Expand Down Expand Up @@ -40,7 +40,7 @@ contract BiconomyTokenPaymaster is
IBiconomyTokenPaymaster
{
using UserOperationLib for PackedUserOperation;
using PaymasterParser for bytes;
using TokenPaymasterParserLib for bytes;
using SignatureCheckerLib for address;

// State variables
Expand Down Expand Up @@ -423,9 +423,8 @@ contract BiconomyTokenPaymaster is
// Transfer full amount to this address. Unused amount will be refunded in postOP
SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount);

context = abi.encode(
userOp.sender, tokenAddress, tokenAmount, tokenPrice, uint256(externalDynamicAdjustment), userOpHash
);
context =
abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, externalDynamicAdjustment, userOpHash);
validationData = _packValidationData(false, validUntil, validAfter);
} else if (mode == PaymasterMode.INDEPENDENT) {
// Use only oracles for the token specified in modeSpecificData
Expand Down Expand Up @@ -488,16 +487,19 @@ contract BiconomyTokenPaymaster is
if (prechargedAmount > actualTokenAmount) {
uint256 refundAmount = prechargedAmount - actualTokenAmount;
SafeTransferLib.safeTransfer(tokenAddress, userOpSender, refundAmount);
emit TokensRefunded(userOpSender, refundAmount, userOpHash);
emit TokensRefunded(userOpSender, tokenAddress, refundAmount, userOpHash);
}

// Emit an event for post-operation completion (optional)
emit PaidGasInTokens(userOpSender, actualGasCost, appliedDynamicAdjustment, userOpHash);
emit PaidGasInTokens(
userOpSender, tokenAddress, actualGasCost, actualTokenAmount, appliedDynamicAdjustment, userOpHash
);
}

function _withdrawERC20(IERC20 token, address target, uint256 amount) private {
if (target == address(0)) revert CanNotWithdrawToZeroAddress();
SafeTransferLib.safeTransfer(address(token), target, amount);
emit TokensWithdrawn(address(token), target, amount, msg.sender);
}

/// @notice Fetches the latest token price.
Expand Down
83 changes: 83 additions & 0 deletions test/base/TestBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol";

import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol";

import {
BiconomyTokenPaymaster,
IBiconomyTokenPaymaster,
BiconomyTokenPaymasterErrors,
IOracle
} from "../../../contracts/token/BiconomyTokenPaymaster.sol";

abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors {
address constant ENTRYPOINT_ADDRESS = address(0x0000000071727De22E5E9d8BAf0edAc6f37da032);

Expand Down Expand Up @@ -207,6 +214,70 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors {
);
}

/// @notice Generates and signs the paymaster data for a user operation.
/// @dev This function prepares the `paymasterAndData` field for a `PackedUserOperation` with the correct signature.
/// @param userOp The user operation to be signed.
/// @param signer The wallet that will sign the paymaster hash.
/// @param paymaster The paymaster contract.
/// @return finalPmData Full Pm Data.
/// @return signature Pm Signature on Data.
function generateAndSignTokenPaymasterData(
PackedUserOperation memory userOp,
Vm.Wallet memory signer,
BiconomyTokenPaymaster paymaster,
uint128 paymasterValGasLimit,
uint128 paymasterPostOpGasLimit,
IBiconomyTokenPaymaster.PaymasterMode mode,
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint128 tokenPrice,
uint32 externalDynamicAdjustment
)
internal
view
returns (bytes memory finalPmData, bytes memory signature)
{
// Initial paymaster data with zero signature
bytes memory initialPmData = abi.encodePacked(
address(paymaster),
paymasterValGasLimit,
paymasterPostOpGasLimit,
uint8(mode),
validUntil,
validAfter,
tokenAddress,
tokenPrice,
externalDynamicAdjustment,
new bytes(65) // Zero signature
);

// Update user operation with initial paymaster data
userOp.paymasterAndData = initialPmData;

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

// Sign the hash
signature = signMessage(signer, paymasterHash);
require(signature.length == 65, "Invalid Paymaster Signature length");

// Final paymaster data with the actual signature
finalPmData = abi.encodePacked(
address(paymaster),
paymasterValGasLimit,
paymasterPostOpGasLimit,
uint8(mode),
validUntil,
validAfter,
tokenAddress,
tokenPrice,
externalDynamicAdjustment,
signature
);
}

function excludeLastNBytes(bytes memory data, uint256 n) internal pure returns (bytes memory) {
require(data.length > n, "Input data is too short");
bytes memory result = new bytes(data.length - n);
Expand Down Expand Up @@ -268,4 +339,16 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors {
// paymaster)
assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18);
}

function _toSingletonArray(address addr) internal pure returns (address[] memory) {
address[] memory array = new address[](1);
array[0] = addr;
return array;
}

function _toSingletonArray(IOracle oracle) internal pure returns (IOracle[] memory) {
IOracle[] memory array = new IOracle[](1);
array[0] = oracle;
return array;
}
}
80 changes: 80 additions & 0 deletions test/mocks/MockOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "contracts/interfaces/oracles/IOracle.sol";

contract MockOracle is IOracle {
int256 public price;
uint8 public priceDecimals;
uint256 public updatedAtDelay;

constructor(int256 _initialPrice, uint8 _decimals) {
price = _initialPrice;
priceDecimals = _decimals;
updatedAtDelay = 0;
}

/**
* @dev Allows setting a new price manually for testing purposes.
* @param _price The new price to be set.
*/
function setPrice(int256 _price) external {
price = _price;
}

/**
* @dev Allows setting the delay for the `updatedAt` timestamp.
* @param _updatedAtDelay The delay in seconds to simulate a stale price.
*/
function setUpdatedAtDelay(uint256 _updatedAtDelay) external {
updatedAtDelay = _updatedAtDelay;
}

/**
* @dev Returns the number of decimals for the oracle price feed.
*/
function decimals() external view override returns (uint8) {
return priceDecimals;
}

/**
* @dev Mocks a random price within a given range.
* @param minPrice The minimum price range (inclusive).
* @param maxPrice The maximum price range (inclusive).
*/
function setRandomPrice(int256 minPrice, int256 maxPrice) external {
require(minPrice <= maxPrice, "Min price must be less than or equal to max price");

// Generate a random price within the range [minPrice, maxPrice]
price = minPrice + int256(uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty))) % uint256(maxPrice - minPrice + 1));
}

/**
* @dev Returns mocked data for the latest round of the price feed.
* @return _roundId The round ID.
* @return answer The current price.
* @return startedAt The timestamp when the round started.
* @return _updatedAt The timestamp when the round was last updated.
* @return answeredInRound The round ID in which the answer was computed.
*/
function latestRoundData()
external
view
override
returns (
uint80 _roundId,
int256 answer,
uint256 startedAt,
uint256 _updatedAt,
uint80 answeredInRound
)
{
return (
73786976294838215802, // Mock round ID
price, // The current price
block.timestamp, // Simulate round started at the current block timestamp
block.timestamp - updatedAtDelay, // Simulate price last updated with delay
73786976294838215802 // Mock round ID for answeredInRound
);
}
}
Loading

0 comments on commit 735f2a6

Please sign in to comment.