Skip to content

Commit

Permalink
feat: support latest entrypoint (#13)
Browse files Browse the repository at this point in the history
* feat: support latest entrypoint
feat: remove oracle from paymaster

* fix: add TokenPriceOracle to test contracts
  • Loading branch information
andrewwahid authored Apr 21, 2023
1 parent aafb5a9 commit f7131da
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 97 deletions.
2 changes: 1 addition & 1 deletion @account-abstraction
Submodule @account-abstraction updated 36 files
+ audits/EIP_4337_–_Ethereum_Account_Abstraction_Incremental_Audit_Feb_2023.pdf
+24 −13 contracts/core/BaseAccount.sol
+15 −4 contracts/core/EntryPoint.sol
+16 −0 contracts/core/Helpers.sol
+40 −0 contracts/core/NonceManager.sol
+8 −1 contracts/interfaces/IEntryPoint.sol
+27 −0 contracts/interfaces/INonceManager.sol
+23 −16 contracts/interfaces/UserOperation.sol
+1 −1 contracts/package.json
+2 −17 contracts/samples/SimpleAccount.sol
+61 −0 contracts/samples/callback/TokenCallbackHandler.sol
+9 −1 contracts/samples/gnosis/EIP4337Fallback.sol
+10 −5 contracts/samples/gnosis/EIP4337Manager.sol
+3 −2 contracts/test/MaliciousAccount.sol
+1 −0 deploy/2_deploy_SimpleAccountFactory.ts
+592 −440 deployments/goerli/EntryPoint.json
+1 −0 deployments/mainnet/.chainId
+1,267 −0 deployments/mainnet/EntryPoint.json
+59 −0 deployments/mainnet/solcInputs/cfbebdf1101dd2bc0f310cb0b7d62cb7.json
+1 −0 deployments/mumbai/.chainId
+1 −0 deployments/sepolia/.chainId
+1,267 −0 deployments/sepolia/EntryPoint.json
+284 −0 deployments/sepolia/solcInputs/bf45a54649a091a115919fd9027d13aa.json
+59 −0 deployments/sepolia/solcInputs/cfbebdf1101dd2bc0f310cb0b7d62cb7.json
+305 −0 deployments/sepolia/solcInputs/e7c946e4952bca029c011c50539aa892.json
+33 −21 eip/EIPS/eip-4337.md
+35 −9 gascalc/GasChecker.ts
+2 −2 hardhat.config.ts
+2 −2 package.json
+20 −16 reports/gas-checker.txt
+26 −67 test/UserOp.ts
+90 −8 test/entrypoint.test.ts
+9 −8 test/gnosis.test.ts
+20 −15 test/simple-wallet.test.ts
+1 −2 test/y.bls.test.ts
+23 −27 yarn.lock
2 changes: 1 addition & 1 deletion @safe-contracts
16 changes: 8 additions & 8 deletions contracts/candideWallet/CandideWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ contract CandideWallet is Safe{
//EIP4337 trusted entrypoint
address public entryPoint;

string public constant CANDIDE_VERSION = "0.0.1";
//return value in case of signature failure, with no time-range.
uint256 constant internal SIG_VALIDATION_FAILED = 1;

Expand Down Expand Up @@ -65,9 +66,8 @@ contract CandideWallet is Safe{
uint256 missingAccountFunds) external returns (uint256 validationData){
_requireFromEntryPoint();
validationData = _validateSignature(userOp, userOpHash);
if (userOp.initCode.length == 0) {
_validateAndUpdateNonce(userOp);
}
// mimic normal Safe nonce behaviour: prevent parallel nonces
require(userOp.nonce < type(uint64).max, "account: nonsequential nonce");
_payPrefund(missingAccountFunds);
}

Expand All @@ -79,7 +79,7 @@ contract CandideWallet is Safe{
}

function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
internal returns (uint256 validationData) {
internal view returns (uint256 validationData) {
bytes32 hash = userOpHash.toEthSignedMessageHash();
try this.checkSignatures(
hash,
Expand All @@ -93,10 +93,6 @@ contract CandideWallet is Safe{
return 0;
}

function _validateAndUpdateNonce(UserOperation calldata userOp) internal {
require(nonce++ == userOp.nonce, "account: invalid nonce");
}

function _payPrefund(uint256 missingAccountFunds) internal {
if (missingAccountFunds != 0) {
(bool success,) = payable(msg.sender).call{value : missingAccountFunds, gas : type(uint256).max}("");
Expand Down Expand Up @@ -168,4 +164,8 @@ contract CandideWallet is Safe{
function replaceEntrypoint(address newEntrypoint) public authorized {
entryPoint = newEntrypoint;
}

function getNonce() public view returns (uint256) {
return IEntryPoint(entryPoint).getNonce(address(this), 0);
}
}
91 changes: 22 additions & 69 deletions contracts/paymaster/CandidePaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import "@openzeppelin/contracts/access/Ownable.sol";

import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract CandidePaymaster is BasePaymaster {

Expand All @@ -29,32 +28,19 @@ contract CandidePaymaster is BasePaymaster {
SponsoringMode mode;
uint48 validUntil;
uint256 fee;
uint256 exchangeRate;
bytes signature;
}

//calculated cost of the postOp
uint256 constant public COST_OF_POST = 35000;

address public immutable verifyingSigner;
uint256 constant public COST_OF_POST = 45000;
mapping(IERC20Metadata => uint256) public balances;
//
AggregatorV3Interface private immutable ETH_USD_ORACLE;
AggregatorV3Interface private constant NULL_ORACLE = AggregatorV3Interface(address(0));
mapping(IERC20Metadata => AggregatorV3Interface) public oracles; // Oracles should be against USD, so if the token is UNI the oracle needs to be UNI/USD

constructor(IEntryPoint _entryPoint, address _verifyingSigner, address _ethUsdOracle) BasePaymaster(_entryPoint) {
ETH_USD_ORACLE = AggregatorV3Interface(_ethUsdOracle);
verifyingSigner = _verifyingSigner;
_transferOwnership(verifyingSigner);
}

event UserOperationSponsored(address indexed sender, address indexed token, uint256 cost);

/**
* owner of the paymaster should add supported tokens
*/
function addToken(IERC20Metadata token, AggregatorV3Interface tokenPriceOracle) external onlyOwner {
require(oracles[token] == NULL_ORACLE, "CP04: Token already set");
oracles[token] = tokenPriceOracle;
constructor(IEntryPoint _entryPoint, address _owner) BasePaymaster(_entryPoint) {
_transferOwnership(_owner);
}

/**
Expand All @@ -64,7 +50,7 @@ contract CandidePaymaster is BasePaymaster {
* @param amount amount to withdraw
*/
function withdrawTokensTo(IERC20Metadata token, address target, uint256 amount) public {
require(verifyingSigner == msg.sender, "CP00: only verifyingSigner can withdraw tokens");
require(owner() == msg.sender, "CP00: only owner can withdraw tokens");
balances[token] -= amount;
token.safeTransfer(target, amount);
}
Expand Down Expand Up @@ -99,7 +85,8 @@ contract CandidePaymaster is BasePaymaster {
address(paymasterData.token),
paymasterData.mode,
paymasterData.validUntil,
paymasterData.fee
paymasterData.fee,
paymasterData.exchangeRate
));
}

Expand All @@ -109,44 +96,9 @@ contract CandidePaymaster is BasePaymaster {
SponsoringMode mode = SponsoringMode(uint8(bytes1(paymasterAndData[40:41])));
uint48 validUntil = uint48(bytes6(paymasterAndData[41:47]));
uint256 fee = uint256(bytes32(paymasterAndData[47:79]));
bytes memory signature = bytes(paymasterAndData[79:]);
return PaymasterData(token, mode, validUntil, fee, signature);
}

function getDerivedValue(
IERC20Metadata _token,
uint256 _ethBought
) public view returns (uint256) {
uint8 _decimals = 18; // this can be hardcoded because we're always deriving a TOKEN / ETH price which is always 18 decimals
int256 decimals = int256(10 ** uint256(_decimals));
//
AggregatorV3Interface tokenOracle = oracles[_token];
require(tokenOracle != NULL_ORACLE, "CP00: unsupported token");
//
(, int256 tokenPrice, , , ) = tokenOracle.latestRoundData();
uint8 tokenOracleDecimals = tokenOracle.decimals();
tokenPrice = scalePrice(tokenPrice, tokenOracleDecimals, _decimals);
//
(, int256 ethPrice, , , ) = ETH_USD_ORACLE.latestRoundData();
uint8 ethOracleDecimals = ETH_USD_ORACLE.decimals();
ethPrice = scalePrice(ethPrice, ethOracleDecimals, _decimals);
//
int256 price = (tokenPrice * decimals) / ethPrice;
uint8 tokenDecimals = _token.decimals();
return (_ethBought * (10**tokenDecimals)) / uint256(price);
}

function scalePrice(
int256 _price,
uint8 _priceDecimals,
uint8 _decimals
) internal pure returns (int256) {
if (_priceDecimals < _decimals) {
return _price * int256(10 ** uint256(_decimals - _priceDecimals));
} else if (_priceDecimals > _decimals) {
return _price / int256(10 ** uint256(_priceDecimals - _decimals));
}
return _price;
uint256 exchangeRate = uint256(bytes32(paymasterAndData[79:111]));
bytes memory signature = bytes(paymasterAndData[111:]);
return PaymasterData(token, mode, validUntil, fee, exchangeRate, signature);
}

/**
Expand All @@ -162,15 +114,14 @@ contract CandidePaymaster is BasePaymaster {
PaymasterData memory paymasterData = parsePaymasterAndData(userOp.paymasterAndData);
require(paymasterData.signature.length == 64 || paymasterData.signature.length == 65, "CP01: invalid signature length in paymasterAndData");

bytes32 _hash = getHash(userOp, paymasterData);
if (verifyingSigner != _hash.recover(paymasterData.signature)) {
bytes32 _hash = getHash(userOp, paymasterData).toEthSignedMessageHash();
if (owner() != _hash.recover(paymasterData.signature)) {
return ("", _packValidationData(true, paymasterData.validUntil, 0));
}

address account = userOp.getSender();
uint256 maxTokenCost = getDerivedValue(paymasterData.token, maxCost);
uint256 gasPriceUserOp = userOp.gasPrice();
bytes memory _context = abi.encode(account, paymasterData.token, paymasterData.mode, paymasterData.fee, gasPriceUserOp, maxTokenCost, maxCost);
bytes memory _context = abi.encode(account, paymasterData.token, paymasterData.mode, paymasterData.fee, paymasterData.exchangeRate, gasPriceUserOp);

return (_context, _packValidationData(false, paymasterData.validUntil, 0));
}
Expand All @@ -179,17 +130,19 @@ contract CandidePaymaster is BasePaymaster {
* Perform the post-operation to charge the sender for the gas.
*/
function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal override {
(mode);

(address account, IERC20Metadata token, SponsoringMode sponsoringMode, uint256 fee, uint256 gasPricePostOp, uint160 maxTokenCost, uint256 maxCost)
= abi.decode(context, (address, IERC20Metadata, SponsoringMode, uint256, uint256, uint160, uint256));
(address account, IERC20Metadata token, SponsoringMode sponsoringMode, uint256 fee, uint256 exchangeRate, uint256 gasPricePostOp)
= abi.decode(context, (address, IERC20Metadata, SponsoringMode, uint256, uint256, uint256));
if (sponsoringMode == SponsoringMode.FREE) return;
//
uint256 actualTokenCost = (actualGasCost + COST_OF_POST * gasPricePostOp) * maxTokenCost / maxCost;
uint256 actualTokenCost = ((actualGasCost + (COST_OF_POST * gasPricePostOp)) * exchangeRate) / 1e18;
if (sponsoringMode == SponsoringMode.FULL){
actualTokenCost = actualTokenCost + fee;
}
token.safeTransferFrom(account, address(this), actualTokenCost);
balances[token] += actualTokenCost;
if (mode != PostOpMode.postOpReverted) {
token.safeTransferFrom(account, address(this), actualTokenCost);
balances[token] += actualTokenCost;
emit UserOperationSponsored(account, address(token), actualTokenCost);
}
}
}
16 changes: 16 additions & 0 deletions contracts/test/TokenPriceOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

/// @author CandideWallet Team

import "@account-abstraction/contracts/samples/IOracle.sol";

contract TokenPriceOracle is IOracle{

/**
* return amount of tokens that are required to receive that much eth.
*/
function getTokenValueOfEth(uint256 ethOutput) external view returns (uint256 tokenInput){
return 1;
}
}
14 changes: 2 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@
BLSOpen,
TestBLS,
BLSAccountMultisig,
MockTokenPriceOracle,
)
from brownie_tokens import ERC20
import json
import random
from py_ecc.optimized_bn128.optimized_curve import curve_order
from testBLSUtils import get_public_key, xyz_to_affine_G2

entryPoint_addr = "0x0576a174D229E3cFA37253523E645A78A0C91B57" # Goerli
ethUsdOracle = "0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e" # Goerli
entryPoint_addr = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" # Goerli
# entryPoint_addr = 0x79b0F2a81D2b5d507E56d42D452239e94b18Ddc8 #optimism Goerli
# should be the same as the bundler's RPC
bundler_pk = "e0cb334cac07d3555270bff73b3d7656a1256c2cebe856b85104ec84725c98c4" # noqa: E501
Expand Down Expand Up @@ -170,18 +168,10 @@ def candidePaymaster(CandidePaymaster, entryPoint, bundler):
Deploy CandidePaymaster contract
"""
return CandidePaymaster.deploy(
entryPoint.address, bundler, ethUsdOracle, {"from": bundler}
entryPoint.address, bundler, {"from": bundler}
)


@pytest.fixture(scope="module")
def mockOracle(accounts):
"""
Deploy MockTokenPriceOracle contract
"""
return MockTokenPriceOracle.deploy({"from": accounts[0]})


@pytest.fixture(scope="module")
def depositPaymaster(
DepositPaymaster, entryPoint, TokenPriceOracle, tokenErc20, owner
Expand Down
14 changes: 8 additions & 6 deletions tests/test_candideWallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from brownie import reverts, chain
from web3.auto import w3
from eth_abi.packed import encode_packed
from eth_account.messages import defunct_hash_message
from testUtils import (
ExecuteExecTransaction,
ExecuteEntryPointHandleOps,
Expand Down Expand Up @@ -154,7 +155,7 @@ def test_transaction_through_entrypoint(

op = [
candideWalletProxy.address,
candideWalletProxy.nonce(),
candideWalletProxy.getNonce(),
bytes(0),
callData,
215000,
Expand Down Expand Up @@ -298,7 +299,7 @@ def test_transfer_from_entrypoint_with_deposit_paymaster(

op = [
candideWalletProxy.address,
candideWalletProxy.nonce(),
candideWalletProxy.getNonce(),
bytes(0),
callData,
215000,
Expand All @@ -320,7 +321,6 @@ def test_transfer_from_entrypoint_with_candidePaymaster(
bundler,
entryPoint,
candidePaymaster,
mockOracle,
receiver,
accounts,
):
Expand All @@ -333,7 +333,6 @@ def test_transfer_from_entrypoint_with_candidePaymaster(
accounts[0].transfer(bundler, "3 ether")
candidePaymaster.addStake(100, {"from": bundler, "value": "1 ether"})
candidePaymaster.deposit({"from": bundler, "value": "1 ether"})
candidePaymaster.addToken(tokenErc20.address, mockOracle.address)

tokenErc20.transfer(
candideWalletProxy.address, "5 ether", {"from": bundler}
Expand All @@ -344,6 +343,7 @@ def test_transfer_from_entrypoint_with_candidePaymaster(
1, # SponsoringMode
chain.time() + 450, # validUntil
0, # Fee (in case mode == 0)
1000000000000000000, # Exchange Rate
b'',
]

Expand All @@ -361,7 +361,7 @@ def test_transfer_from_entrypoint_with_candidePaymaster(

op = [
candideWalletProxy.address,
candideWalletProxy.nonce(),
candideWalletProxy.getNonce(),
bytes(0),
callData,
215000,
Expand All @@ -376,14 +376,16 @@ def test_transfer_from_entrypoint_with_candidePaymaster(
datahash = candidePaymaster.getHash(
op, paymasterData
)
message_hash = defunct_hash_message(datahash)
bundlerSigner = w3.eth.account.from_key(bundler.private_key)
sig = bundlerSigner.signHash(datahash)
sig = bundlerSigner.signHash(message_hash)
paymasterAndData = (
str(candidePaymaster.address[2:])
+ str(paymasterData[0][2:])
+ str("{0:0{1}x}".format(paymasterData[1], 2))
+ str("{0:0{1}x}".format(paymasterData[2], 12))
+ str("{0:0{1}x}".format(paymasterData[3], 64))
+ str("{0:0{1}x}".format(paymasterData[4], 64))
+ sig.signature.hex()[2:]
)
op[9] = paymasterAndData
Expand Down

0 comments on commit f7131da

Please sign in to comment.