Skip to content

Commit

Permalink
tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kumaryash90 committed Sep 12, 2024
1 parent 60c8b9b commit 934707d
Show file tree
Hide file tree
Showing 4 changed files with 355 additions and 0 deletions.
30 changes: 30 additions & 0 deletions src/test/mocks/MockERC20CustomDecimals.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";

contract MockERC20CustomDecimals is ERC20PresetMinterPauser, ERC20Permit {
uint8 private immutable _decimals;

constructor(uint8 decimals_) ERC20PresetMinterPauser("Mock Coin", "MOCK") ERC20Permit("Mock Coin") {
_decimals = decimals_;
}

function mint(address to, uint256 amount) public override(ERC20PresetMinterPauser) {
_mint(to, amount);
}

function decimals() public view override returns (uint8) {
return _decimals;
}

function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20PresetMinterPauser, ERC20) {
super._beforeTokenTransfer(from, to, amount);
}
}
44 changes: 44 additions & 0 deletions src/test/mocks/TestOracle2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

// source: https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/test/TestOracle2.sol

interface IOracle {
function decimals() external view returns (uint8);
function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}

contract TestOracle2 is IOracle {
int256 public price;
uint8 private _decimals_;

constructor(int256 _price, uint8 _decimals) {
price = _price;
_decimals_ = _decimals;
}

function setPrice(int256 _price) external {
price = _price;
}

function setDecimals(uint8 _decimals) external {
_decimals_ = _decimals;
}

function decimals() external view override returns (uint8) {
return _decimals_;
}

function latestRoundData()
external
view
override
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
// solhint-disable-next-line not-rely-on-time
return (73786976294838215802, price, 1680509051, block.timestamp, 73786976294838215802);
}
}
52 changes: 52 additions & 0 deletions src/test/mocks/TestUniswap.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

// source: https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/test/TestUniswap.sol

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";

import "./WETH9.sol";

/// @notice Very basic simulation of what Uniswap does with the swaps for the unit tests on the TokenPaymaster
/// @dev Do not use to test any actual Uniswap interaction logic as this is way too simplistic
contract TestUniswap {
WETH9 public weth;

constructor(WETH9 _weth) {
weth = _weth;
}

event StubUniswapExchangeEvent(uint256 amountIn, uint256 amountOut, address tokenIn, address tokenOut);

function exactOutputSingle(ISwapRouter.ExactOutputSingleParams calldata params) external returns (uint256) {
uint256 amountIn = params.amountInMaximum - 5;
emit StubUniswapExchangeEvent(amountIn, params.amountOut, params.tokenIn, params.tokenOut);
IERC20(params.tokenIn).transferFrom(msg.sender, address(this), amountIn);
IERC20(params.tokenOut).transfer(params.recipient, params.amountOut);
return amountIn;
}

function exactInputSingle(ISwapRouter.ExactInputSingleParams calldata params) external returns (uint256) {
uint256 amountOut = params.amountOutMinimum + 5;
emit StubUniswapExchangeEvent(params.amountIn, amountOut, params.tokenIn, params.tokenOut);
IERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn);
IERC20(params.tokenOut).transfer(params.recipient, amountOut);
return amountOut;
}

/// @notice Simplified code copied from here:
/// https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/PeripheryPayments.sol#L19
function unwrapWETH9(uint256 amountMinimum, address recipient) public payable {
uint256 balanceWETH9 = weth.balanceOf(address(this));
require(balanceWETH9 >= amountMinimum, "Insufficient WETH9");

if (balanceWETH9 > 0) {
weth.withdraw(balanceWETH9);
payable(recipient).transfer(balanceWETH9);
}
}

// solhint-disable-next-line no-empty-blocks
receive() external payable {}
}
229 changes: 229 additions & 0 deletions src/test/smart-wallet/token-paymaster/TokenPaymaster.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

// Test utils
import "../../utils/BaseTest.sol";
import { MockERC20CustomDecimals } from "../../mocks/MockERC20CustomDecimals.sol";
import { TestUniswap } from "../../mocks/TestUniswap.sol";
import { TestOracle2 } from "../../mocks/TestOracle2.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

// Account Abstraction setup for smart wallets.
import { EntryPoint, IEntryPoint } from "contracts/prebuilts/account/utils/EntryPoint.sol";
import { PackedUserOperation } from "contracts/prebuilts/account/interfaces/PackedUserOperation.sol";

// Target
import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol";
import { AccountFactory } from "contracts/prebuilts/account/non-upgradeable/AccountFactory.sol";
import { Account as SimpleAccount } from "contracts/prebuilts/account/non-upgradeable/Account.sol";
import { TokenPaymaster, IERC20Metadata } from "contracts/prebuilts/account/token-paymaster/TokenPaymaster.sol";
import { OracleHelper, IOracle } from "contracts/prebuilts/account/utils/OracleHelper.sol";
import { UniswapHelper, ISwapRouter } from "contracts/prebuilts/account/utils/UniswapHelper.sol";

/// @dev This is a dummy contract to test contract interactions with Account.
contract Number {
uint256 public num;

function setNum(uint256 _num) public {
num = _num;
}

function doubleNum() public {
num *= 2;
}

function incrementNum() public {
num += 1;
}
}

contract TokenPaymasterTest is BaseTest {
EntryPoint private entrypoint;
AccountFactory private accountFactory;
SimpleAccount private account;
MockERC20CustomDecimals private token;
TestUniswap private testUniswap;
TestOracle2 private nativeAssetOracle;
TestOracle2 private tokenOracle;
TokenPaymaster private paymaster;

Number private numberContract;

int256 initialPriceToken = 100000000; // USD per TOK
int256 initialPriceEther = 500000000; // USD per ETH

uint256 priceDenominator = 10 ** 26;
uint128 minEntryPointBalance = 1e17;

address payable private beneficiary = payable(address(0x45654));

uint256 private accountAdminPKey = 100;
address private accountAdmin;

uint256 private accountSignerPKey = 200;
address private accountSigner;

uint256 private nonSignerPKey = 300;
address private nonSigner;

uint256 private paymasterOwnerPKey = 400;
address private paymasterOwner;
address private paymasterAddress;

function setUp() public override {
super.setUp();

// Setup signers.
accountAdmin = vm.addr(accountAdminPKey);
vm.deal(accountAdmin, 100 ether);

accountSigner = vm.addr(accountSignerPKey);
nonSigner = vm.addr(nonSignerPKey);
paymasterOwner = vm.addr(paymasterOwnerPKey);

// Setup contracts
entrypoint = new EntryPoint();
testUniswap = new TestUniswap(weth);
accountFactory = new AccountFactory(deployer, IEntryPoint(payable(address(entrypoint))));
account = SimpleAccount(payable(accountFactory.createAccount(accountAdmin, bytes(""))));
token = new MockERC20CustomDecimals(6);
nativeAssetOracle = new TestOracle2(initialPriceEther, 8);
tokenOracle = new TestOracle2(initialPriceToken, 8);
numberContract = new Number();

weth.deposit{ value: 1 ether }();
weth.transfer(address(testUniswap), 1 ether);

TokenPaymaster.TokenPaymasterConfig memory tokenPaymasterConfig = TokenPaymaster.TokenPaymasterConfig({
priceMarkup: (priceDenominator * 15) / 10, // +50%
minEntryPointBalance: minEntryPointBalance,
refundPostopCost: 40000,
priceMaxAge: 86400
});

OracleHelper.OracleHelperConfig memory oracleHelperConfig = OracleHelper.OracleHelperConfig({
cacheTimeToLive: 0,
maxOracleRoundAge: 0,
nativeOracle: IOracle(address(nativeAssetOracle)),
nativeOracleReverse: false,
priceUpdateThreshold: (priceDenominator * 12) / 100, // 20%
tokenOracle: IOracle(address(tokenOracle)),
tokenOracleReverse: false,
tokenToNativeOracle: false
});

UniswapHelper.UniswapHelperConfig memory uniswapHelperConfig = UniswapHelper.UniswapHelperConfig({
minSwapAmount: 1,
slippage: 5,
uniswapPoolFee: 3
});

paymaster = new TokenPaymaster(
IERC20Metadata(address(token)),
entrypoint,
weth,
ISwapRouter(address(testUniswap)),
tokenPaymasterConfig,
oracleHelperConfig,
uniswapHelperConfig,
paymasterOwner
);
paymasterAddress = address(paymaster);

token.mint(paymasterOwner, 10_000 ether);
vm.deal(paymasterOwner, 10_000 ether);

vm.startPrank(paymasterOwner);
token.transfer(address(paymaster), 100);
paymaster.updateCachedPrice(true);
entrypoint.depositTo{ value: 1000 ether }(address(paymaster));
paymaster.addStake{ value: 2 ether }(1);
vm.stopPrank();
}

// test utils
function _packPaymasterStaticFields(
address paymaster,
uint128 validationGasLimit,
uint128 postOpGasLimit
) internal pure returns (bytes memory) {
return abi.encodePacked(bytes20(paymaster), bytes16(validationGasLimit), bytes16(postOpGasLimit));
}

function _setupUserOpWithSenderAndPaymaster(
bytes memory _initCode,
bytes memory _callDataForEntrypoint,
address _sender,
address _paymaster,
uint128 _paymasterVerificationGasLimit,
uint128 _paymasterPostOpGasLimit
) internal returns (PackedUserOperation[] memory ops) {
uint256 nonce = entrypoint.getNonce(_sender, 0);
PackedUserOperation memory op;

{
uint128 verificationGasLimit = 500_000;
uint128 callGasLimit = 500_000;
bytes32 packedAccountGasLimits = (bytes32(uint256(verificationGasLimit)) << 128) |
bytes32(uint256(callGasLimit));
bytes32 packedGasLimits = (bytes32(uint256(1e9)) << 128) | bytes32(uint256(1e9));

// Get user op fields
op = PackedUserOperation({
sender: _sender,
nonce: nonce,
initCode: _initCode,
callData: _callDataForEntrypoint,
accountGasLimits: packedAccountGasLimits,
preVerificationGas: 500_000,
gasFees: packedGasLimits,
paymasterAndData: _packPaymasterStaticFields(
_paymaster,
_paymasterVerificationGasLimit,
_paymasterPostOpGasLimit
),
signature: bytes("")
});
}

// Sign UserOp
bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op);
bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash);
bytes memory userOpSignature = abi.encodePacked(r, s, v);

address recoveredSigner = ECDSA.recover(msgHash, v, r, s);
address expectedSigner = vm.addr(accountAdminPKey);
assertEq(recoveredSigner, expectedSigner);

op.signature = userOpSignature;

// Store UserOp
ops = new PackedUserOperation[](1);
ops[0] = op;
}

// Should be able to sponsor the UserOp while charging correct amount of ERC-20 tokens
function test_validatePaymasterUserOp_correctERC20() public {
token.mint(address(account), 1 ether);
vm.prank(address(account));
token.approve(address(paymaster), type(uint256).max);

PackedUserOperation[] memory ops = _setupUserOpWithSenderAndPaymaster(
bytes(""),
abi.encodeWithSignature(
"execute(address,uint256,bytes)",
address(numberContract),
0,
abi.encodeWithSignature("setNum(uint256)", 42)
),
address(account),
address(paymaster),
3e5,
3e5
);

entrypoint.handleOps(ops, beneficiary);
}
}

0 comments on commit 934707d

Please sign in to comment.