Skip to content

Commit

Permalink
initial setup
Browse files Browse the repository at this point in the history
  • Loading branch information
livingrockrises committed Mar 19, 2024
1 parent 811b335 commit 4212551
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 79 deletions.
151 changes: 151 additions & 0 deletions contracts/base/BasePaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

/* solhint-disable reason-string */

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import { IPaymaster } from "account-abstraction/contracts/interfaces/IPaymaster.sol";
import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol";
import "account-abstraction/contracts/core/UserOperationLib.sol";
/**
* Helper class for creating a paymaster.
* provides helper methods for staking.
* Validates that the postOp is called only by the entryPoint.
*/
abstract contract BasePaymaster is IPaymaster, Ownable {
IEntryPoint public immutable entryPoint;

uint256 internal constant PAYMASTER_VALIDATION_GAS_OFFSET = UserOperationLib.PAYMASTER_VALIDATION_GAS_OFFSET;
uint256 internal constant PAYMASTER_POSTOP_GAS_OFFSET = UserOperationLib.PAYMASTER_POSTOP_GAS_OFFSET;
uint256 internal constant PAYMASTER_DATA_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET;

constructor(IEntryPoint _entryPoint) Ownable(msg.sender) {
_validateEntryPointInterface(_entryPoint);
entryPoint = _entryPoint;
}

//sanity check: make sure this EntryPoint was compiled against the same
// IEntryPoint of this paymaster
function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual {
require(IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), "IEntryPoint interface mismatch");
}

/// @inheritdoc IPaymaster
function validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external override returns (bytes memory context, uint256 validationData) {
_requireFromEntryPoint();
return _validatePaymasterUserOp(userOp, userOpHash, maxCost);
}

/**
* Validate a user operation.
* @param userOp - The user operation.
* @param userOpHash - The hash of the user operation.
* @param maxCost - The maximum cost of the user operation.
*/
function _validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) internal virtual returns (bytes memory context, uint256 validationData);

/// @inheritdoc IPaymaster
function postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost,
uint256 actualUserOpFeePerGas
) external override {
_requireFromEntryPoint();
_postOp(mode, context, actualGasCost, actualUserOpFeePerGas);
}

/**
* Post-operation handler.
* (verified to be called only through the entryPoint)
* @dev If subclass returns a non-empty context from validatePaymasterUserOp,
* it must also implement this method.
* @param mode - Enum with the following options:
* opSucceeded - User operation succeeded.
* opReverted - User op reverted. The paymaster still has to pay for gas.
* postOpReverted - never passed in a call to postOp().
* @param context - The context value returned by validatePaymasterUserOp
* @param actualGasCost - Actual gas used so far (without this postOp call).
* @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas
* and maxPriorityFee (and basefee)
* It is not the same as tx.gasprice, which is what the bundler pays.
*/
function _postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost,
uint256 actualUserOpFeePerGas
) internal virtual {
(mode, context, actualGasCost, actualUserOpFeePerGas); // unused params
// subclass must override this method if validatePaymasterUserOp returns a context
revert("must override");
}

/**
* Add a deposit for this paymaster, used for paying for transaction fees.
*/
function deposit() public payable {
entryPoint.depositTo{value: msg.value}(address(this));
}

/**
* Withdraw value from the deposit.
* @param withdrawAddress - Target to send to.
* @param amount - Amount to withdraw.
*/
function withdrawTo(
address payable withdrawAddress,
uint256 amount
) public onlyOwner {
entryPoint.withdrawTo(withdrawAddress, amount);
}

/**
* Add stake for this paymaster.
* This method can also carry eth value to add to the current stake.
* @param unstakeDelaySec - The unstake delay for this paymaster. Can only be increased.
*/
function addStake(uint32 unstakeDelaySec) external payable onlyOwner {
entryPoint.addStake{value: msg.value}(unstakeDelaySec);
}

/**
* Return current paymaster's deposit on the entryPoint.
*/
function getDeposit() public view returns (uint256) {
return entryPoint.balanceOf(address(this));
}

/**
* Unlock the stake, in order to withdraw it.
* The paymaster can't serve requests once unlocked, until it calls addStake again
*/
function unlockStake() external onlyOwner {
entryPoint.unlockStake();
}

/**
* Withdraw the entire paymaster's stake.
* stake must be unlocked first (and then wait for the unstakeDelay to be over)
* @param withdrawAddress - The address to send withdrawn value.
*/
function withdrawStake(address payable withdrawAddress) external onlyOwner {
entryPoint.withdrawStake(withdrawAddress);
}

/**
* Validate the call is made from a valid entrypoint
*/
function _requireFromEntryPoint() internal virtual {
require(msg.sender == address(entryPoint), "Sender not EntryPoint");
}
}
97 changes: 97 additions & 0 deletions contracts/references/SampleVerifyingPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

/* solhint-disable reason-string */
/* solhint-disable no-inline-assembly */

import "account-abstraction/contracts/core/BasePaymaster.sol";
import "account-abstraction/contracts/core/UserOperationLib.sol";
import "account-abstraction/contracts/core/Helpers.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

/**
* A sample paymaster that uses external service to decide whether to pay for the UserOp.
* The paymaster trusts an external signer to sign the transaction.
* The calling user must pass the UserOp to that external signer first, which performs
* whatever off-chain verification before signing the UserOp.
* Note that this signature is NOT a replacement for the account-specific signature:
* - the paymaster checks a signature to agree to PAY for GAS.
* - the account checks a signature to prove identity and account ownership.
*/
contract VerifyingPaymaster is BasePaymaster {

using UserOperationLib for PackedUserOperation;

address public immutable verifyingSigner;

uint256 private constant VALID_TIMESTAMP_OFFSET = PAYMASTER_DATA_OFFSET;

uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 64;

constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) {
verifyingSigner = _verifyingSigner;
}

/**
* return the hash we're going to sign off-chain (and validate on-chain)
* this method is called by the off-chain service, to sign the request.
* it is called on-chain from the validatePaymasterUserOp, to validate the signature.
* note that this signature covers all fields of the UserOperation, except the "paymasterAndData",
* which will carry the signature itself.
*/
function getHash(PackedUserOperation calldata userOp, uint48 validUntil, uint48 validAfter)
public view returns (bytes32) {
//can't use userOp.hash(), since it contains also the paymasterAndData itself.
address sender = userOp.getSender();
return
keccak256(
abi.encode(
sender,
userOp.nonce,
keccak256(userOp.initCode),
keccak256(userOp.callData),
userOp.accountGasLimits,
uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_DATA_OFFSET])),
userOp.preVerificationGas,
userOp.gasFees,
block.chainid,
address(this),
validUntil,
validAfter
)
);
}

/**
* verify our external signer signed this request.
* the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params
* paymasterAndData[:20] : address(this)
* paymasterAndData[20:84] : abi.encode(validUntil, validAfter)
* paymasterAndData[84:] : signature
*/
function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund)
internal view override returns (bytes memory context, uint256 validationData) {
(requiredPreFund);

(uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData);
//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"
require(signature.length == 64 || signature.length == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData");
bytes32 hash = MessageHashUtils.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter));

//don't revert on signature failure: return SIG_VALIDATION_FAILED
if (verifyingSigner != ECDSA.recover(hash, signature)) {
return ("", _packValidationData(true, validUntil, validAfter));
}

//no need for other on-chain validation: entire UserOp should have been checked
// by the external service prior to signing it.
return ("", _packValidationData(false, validUntil, validAfter));
}

function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns (uint48 validUntil, uint48 validAfter, bytes calldata signature) {
(validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET :], (uint48, uint48));
signature = paymasterAndData[SIGNATURE_OFFSET :];
}
}
12 changes: 12 additions & 0 deletions contracts/sponsorship/SponsorshipPaymasterWithPremium.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

/* solhint-disable reason-string */

import "../base/BasePaymaster.sol";
import "account-abstraction/contracts/core/UserOperationLib.sol";
import "account-abstraction/contracts/core/Helpers.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import { SignatureCheckerLib } from "solady/src/utils/SignatureCheckerLib.sol";
import { ECDSA as ECDSA_solady } from "solady/src/utils/ECDSA.sol";
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ const config: HardhatUserConfig = {
},
},
docgen: {
projectName: "Biconomy",
projectDescription: "Sc-Template Description",
projectName: "Biconomy Paymasters",
projectDescription: "Account Abstraction (v0.7.0) Paymasters",
},
};

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"ethers": "^6.11.1",
"forge-std": "github:foundry-rs/forge-std#v1.7.6",
"modulekit": "github:rhinestonewtf/modulekit",
"solady": "github:vectorized/solady",
"account-abstraction": "github:eth-infinitism/account-abstraction#develop",
"hardhat-gas-reporter": "^1.0.10",
"hardhat-storage-layout": "^0.1.7",
"prettier": "^3.2.5",
Expand Down
2 changes: 1 addition & 1 deletion scripts/foundry/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.23 <0.9.0;

import { Foo } from "../../contracts/Foo.sol";
import { Foo } from "../../contracts/test/Foo.sol";

import { BaseScript } from "./Base.s.sol";

Expand Down
56 changes: 0 additions & 56 deletions test/foundry/Foo.t.sol

This file was deleted.

2 changes: 1 addition & 1 deletion test/foundry/Lock.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity >=0.8.24 <0.9.0;

import { PRBTest } from "@prb/test/src/PRBTest.sol";
import { Lock } from "../../contracts/Lock.sol";
import { Lock } from "../../contracts/test/Lock.sol";
import { StdCheats } from "forge-std/src/StdCheats.sol";

contract LockTest is PRBTest, StdCheats {
Expand Down
26 changes: 26 additions & 0 deletions test/foundry/mocks/Counter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

contract Counter {
uint256 private _number;

function incrementNumber() public {
_number++;
}

function decrementNumber() public {
_number--;
}

function getNumber() public view returns (uint256) {
return _number;
}

function revertOperation() public pure {
revert("Counter: Revert operation");
}

function test_() public pure {
// This function is used to ignore file in coverage report
}
}
Loading

0 comments on commit 4212551

Please sign in to comment.