diff --git a/contracts/VersaAccountFactory.sol b/contracts/VersaAccountFactory.sol index 60eaf2b..1b7f2af 100644 --- a/contracts/VersaAccountFactory.sol +++ b/contracts/VersaAccountFactory.sol @@ -2,19 +2,24 @@ pragma solidity ^0.8.19; import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; import "@safe-contracts/contracts/proxies/SafeProxyFactory.sol"; +import "@aa-template/contracts/interfaces/IEntryPoint.sol"; import "./VersaWallet.sol"; /** * A wrapper factory contract to deploy Versa account proxy. */ -contract VersaAccountFactory is SafeProxyFactory { +contract VersaAccountFactory is SafeProxyFactory, Ownable { address public immutable versaSingleton; address public immutable defaultFallbackHandler; + IEntryPoint public immutable entryPoint; - constructor(address _versaSingleton, address _fallbackHandler) { + constructor(address _versaSingleton, address _fallbackHandler, address _entryPoint, address _owner) { versaSingleton = _versaSingleton; defaultFallbackHandler = _fallbackHandler; + entryPoint = IEntryPoint(_entryPoint); + transferOwnership(_owner); } function createAccount( @@ -110,4 +115,16 @@ contract VersaAccountFactory is SafeProxyFactory { bytes memory deploymentData = abi.encodePacked(proxyCreationCode(), uint256(uint160(versaSingleton))); return Create2.computeAddress(bytes32(salt2), keccak256(deploymentData), address(this)); } + + function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + entryPoint.addStake{ value: msg.value }(unstakeDelaySec); + } + + function unlockStake() external onlyOwner { + entryPoint.unlockStake(); + } + + function withdrawStake(address payable withdrawAddress) external onlyOwner { + entryPoint.withdrawStake(withdrawAddress); + } } diff --git a/contracts/library/SignatureHandler.sol b/contracts/library/SignatureHandler.sol index 49658a8..a92c4bc 100644 --- a/contracts/library/SignatureHandler.sol +++ b/contracts/library/SignatureHandler.sol @@ -2,10 +2,12 @@ pragma solidity ^0.8.19; import "@aa-template/contracts/interfaces/UserOperation.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; library SignatureHandler { - uint8 constant INSTANT_TRANSACTION = 0x00; - uint8 constant SCHEDULE_TRANSACTION = 0x01; + uint8 constant INSTANT_TX_SIG = 0x00; + uint8 constant SCHEDULE_TX_SIG = 0x01; + uint8 constant MULTICHAIN_OP_SIG = 0x02; uint8 constant SIG_TYPE_OFFSET = 20; uint8 constant SIG_TYPE_LENGTH = 1; @@ -21,6 +23,10 @@ library SignatureHandler { uint8 constant INSTANT_SIG_OFFSET = 21; uint8 constant SCHEDULE_SIG_OFFSET = MAX_PRIORITY_FEE_OFFSET + FEE_LENGTH; + uint8 constant MERKLE_ROOT_OFFSET = 33; + uint8 constant MERKLE_PROOF_LEN_OFFSET = 65; + uint8 constant MERKLE_PROOF_OFFSET = 97; + address constant ENTRYPOINT = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789; // Memory struct for decoded userOp signature @@ -36,15 +42,18 @@ library SignatureHandler { /* User operation's signature field(for ECDSA and Multisig validator): - +-----------------------------+-------------------------------------------------------------------------+ - | siganture type | signature layout | - +---------------------------------------------+---------------+-----------------------------------------+ - | instant transaction (0x00) | validatorAddr | signatureType | signatureField | - | | 20 bytes | 1 byte | n bytes | - +-------------------------------------------------------------------------+----------+------------------+ - | scheduled transaction(0x01) | validatorAddr | signatureType | timeRange | feeData | signatureField | - | | 20 bytes | 1 byte | 12 bytes | 64 bytes | n bytes | - +-----------------------------+---------------+---------------+-----------+----------+------------------+ + +---------------------------+------------------------------------------------------------------------------------+ + | siganture type | signature layout | + +-------------------------------------------+---------------+----------------------------------------------------+ + | instant tx sig (0x00) | validatorAddr | signatureType | signatureField | + | | 20 bytes | 1 byte | n bytes | + +-----------------------------------------------------------|-----------+-----------+----------------------------+ + | scheduled tx sig (0x01) | validatorAddr | signatureType | timeRange | feeData | signatureField | + | | 20 bytes | 1 byte | 12 bytes | 64 bytes | n bytes | + +---------------------------+---------------+---------------+-----------+-----------+----------------------------+ + | multi-chain tx sig (0x02) | validatorAddr | signatureType | timeRange |merkle root|proof len | proof |signature| + | | 20 bytes | 1 byte | 12 bytes | 32 bytes | 32 bytes |m bytes| n bytes | + +---------------------------+---------------+---------------+-----------+-----------+----------+-----------------+ timeRange: validUntil(6 bytes) and validAfter(6 bytes) feeData: maxFeePerGas(32 bytes) and maxPriorityFeePerGas(32 bytes) @@ -63,10 +72,10 @@ library SignatureHandler { address validator = address(bytes20(userOp.signature[0:20])); splitedSig.signatureType = uint8(bytes1(userOp.signature[SIG_TYPE_OFFSET:SIG_TYPE_OFFSET + SIG_TYPE_LENGTH])); // For instant transactions, the signature start from the 22th bytes of the userOp.signature. - if (splitedSig.signatureType == INSTANT_TRANSACTION) { + if (splitedSig.signatureType == INSTANT_TX_SIG) { splitedSig.signature = userOp.signature[INSTANT_SIG_OFFSET:]; splitedSig.hash = keccak256(abi.encode(userOpHash, validator)); - } else if (splitedSig.signatureType == SCHEDULE_TRANSACTION) { + } else if (splitedSig.signatureType == SCHEDULE_TX_SIG) { // For scheduled transactions, decode the individual fields from the signature. splitedSig.validUntil = uint48( bytes6(userOp.signature[VALID_UNTIL_OFFSET:VALID_UNTIL_OFFSET + TIME_LENGTH]) @@ -92,6 +101,33 @@ library SignatureHandler { "SignatureHandler: Invalid scheduled transaction gas fee" ); splitedSig.hash = keccak256(abi.encode(getScheduledOpHash(userOp), validator, extraData)); + } else if (splitedSig.signatureType == MULTICHAIN_OP_SIG) { + // Decode validation time range + splitedSig.validUntil = uint48( + bytes6(userOp.signature[VALID_UNTIL_OFFSET:VALID_UNTIL_OFFSET + TIME_LENGTH]) + ); + splitedSig.validAfter = uint48( + bytes6(userOp.signature[VALID_AFTER_OFFSET:VALID_AFTER_OFFSET + TIME_LENGTH]) + ); + // Decode merkle root and proof + bytes32 userOpsRoot = bytes32(userOp.signature[MERKLE_ROOT_OFFSET:MERKLE_ROOT_OFFSET + 32]); + uint256 proofLength = uint256( + bytes32(userOp.signature[MERKLE_PROOF_LEN_OFFSET:MERKLE_PROOF_LEN_OFFSET + 32]) + ); + + bytes32[] memory proof = new bytes32[](proofLength); + for (uint256 i; i < proofLength; ++i) { + proof[i] = bytes32(userOp.signature[MERKLE_PROOF_OFFSET + i * 32:MERKLE_PROOF_OFFSET + (i + 1) * 32]); + } + // Verify userOp hash belongs to the merkle tree + require( + MerkleProof.verify(proof, userOpsRoot, keccak256(bytes.concat(keccak256(abi.encode(userOpHash))))), + "SignatureHandler: UserOp is not in the merkle tree" + ); + uint256 signatureOffset = MERKLE_PROOF_OFFSET + 32 * proofLength; + splitedSig.signature = userOp.signature[signatureOffset:]; + bytes memory extraData = abi.encode(splitedSig.validUntil, splitedSig.validAfter); + splitedSig.hash = keccak256(abi.encode(userOpsRoot, validator, extraData)); } else { revert("SignatureHandler: invalid signature type"); } diff --git a/contracts/paymaster/TokenSwapHandler.sol b/contracts/paymaster/TokenSwapHandler.sol new file mode 100644 index 0000000..0d32773 --- /dev/null +++ b/contracts/paymaster/TokenSwapHandler.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./interfaces/IUniswapRouter.sol"; +import "./interfaces/IWETH.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +abstract contract TokenSwapHandler { + using SafeERC20 for IERC20; + address public v2SwapRouter02; + address public v3SwapRouter02; + + IWETH public immutable WETH; + + constructor(address _v2SwapRouter02, address _v3SwapRouter02, IWETH _weth) { + _setSwapRouter(_v2SwapRouter02, _v3SwapRouter02); + WETH = _weth; + } + + function _setSwapRouter(address _v2SwapRouter02, address _v3SwapRouter02) internal virtual { + v2SwapRouter02 = _v2SwapRouter02; + v3SwapRouter02 = _v3SwapRouter02; + } + + function _approveRouter(IERC20[] calldata tokens, uint256[] calldata amount) internal virtual { + uint256 len = tokens.length; + require(len == amount.length, "TokenSwapHandler: Invalid para length"); + + for (uint256 i; i < len; ++i) { + tokens[i].safeApprove(v2SwapRouter02, amount[i]); + tokens[i].safeApprove(v3SwapRouter02, amount[i]); + } + } + + struct V2SwapParas { + uint256 amountIn; + uint256 amountOutMin; + address[] path; + } + + function _convert(V2SwapParas memory _swapInfo) internal returns (uint256) { + IERC20 tokenIn = IERC20(_swapInfo.path[0]); + if (_swapInfo.amountIn == type(uint256).max) { + _swapInfo.amountIn = tokenIn.balanceOf(address(this)); + } + uint256[] memory amounts = IUniswapV2Router02(v2SwapRouter02).swapExactTokensForETH( + _swapInfo.amountIn, + _swapInfo.amountOutMin, + _swapInfo.path, + address(this), + block.timestamp + ); + uint256 ethOut = amounts[amounts.length - 1]; + return ethOut; + } + + struct V3SwapParas { + bytes path; + uint256 amountIn; + uint256 amountOutMinimum; + } + + function _convert(V3SwapParas calldata _swapInfo) internal returns (uint256) { + address inputToken = address(bytes20(_swapInfo.path[:20])); + address outPutToken = address(bytes20(_swapInfo.path[_swapInfo.path.length - 20:])); + require(outPutToken == address(WETH), "TokenSwapHandler: Only to wnative token allowed"); + + uint256 amountIn = _swapInfo.amountIn == type(uint256).max + ? IERC20(inputToken).balanceOf(address(this)) + : _swapInfo.amountIn; + + uint256 amountOut = IUniswapV3Router02(v3SwapRouter02).exactInput( + IUniswapV3Router02.ExactInputParams(_swapInfo.path, address(this), amountIn, _swapInfo.amountOutMinimum) + ); + return amountOut; + } +} diff --git a/contracts/paymaster/VersaUniversalPaymaster.sol b/contracts/paymaster/VersaUniversalPaymaster.sol new file mode 100644 index 0000000..e8d2a7e --- /dev/null +++ b/contracts/paymaster/VersaUniversalPaymaster.sol @@ -0,0 +1,446 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "@aa-template/contracts/core/BasePaymaster.sol"; +import "@aa-template/contracts/interfaces/IEntryPoint.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "./TokenSwapHandler.sol"; + +/** + * @title VersaUniversalPaymaster + * @dev A universal paymaster for sponsoring. + */ +contract VersaUniversalPaymaster is BasePaymaster, TokenSwapHandler { + using ECDSA for bytes32; + using UserOperationLib for UserOperation; + using SafeERC20 for IERC20; + + enum SponsorMode { + FREE_PRIVILEGE, + PAY_WITH_TOKEN, + MULTI_CHAIN_UNIVERSAL + } + + enum UniversalSponsorStatus { + UNPAID, + PAID_UNSPONSORED, + PAID_SPONSORED + } + + struct FreePrivilegeModeData { + uint48 validUntil; + bytes signature; + } + + struct PayWithTokenModeData { + uint48 validUntil; + IERC20 token; + uint256 exchangeRate; + bytes signature; + } + + struct MultiChainUniversalModePaymentData { + uint48 validUntil; + IERC20 token; + uint256 value; + bytes32 sponsorInfoHash; + bytes signature; + } + + struct MultiChainUniversalModeSponsorStatusData { + UniversalSponsorStatus status; + uint48 updateAt; + } + + struct MultiChainUniversalModeData { + uint48 validUntil; + bytes32 sponsorInfoHash; + bytes signature; + } + + event FreePrivilegeSponsored(address indexed sender, uint256 cost); + event PayWithTokenSponsored(address indexed sender, uint256 cost); + event MultiChainUniversalPrePaid( + address indexed sender, + address indexed token, + uint256 value, + bytes32 sponsorInfoHash + ); + event MultiChainUniversalSponsored(address indexed sender, bytes32 sponsorInfoHash, uint256 cost); + + event OperatorSet(address indexed oldOperator, address indexed newOperator); + + address private _operator; + + uint256 public constant COST_OF_POST = 35000; + mapping(bytes32 => MultiChainUniversalModeSponsorStatusData) public universalSponsorStatusData; + + modifier onlyOperator() { + require(msg.sender == operator(), "VersaUniversaPaymaster: Only operator"); + _; + } + + constructor( + IEntryPoint _entryPoint, + address _owner, + address _newOperator, + address _v2SwapRouter02, + address _v3SwapRouter02, + IWETH _weth + ) BasePaymaster(_entryPoint) TokenSwapHandler(_v2SwapRouter02, _v3SwapRouter02, _weth) { + _transferOwnership(_owner); + _setOperator(_newOperator); + } + + function operator() public view returns (address) { + return _operator; + } + + function _setOperator(address _newOperator) internal { + address oldOperator = operator(); + _operator = _newOperator; + emit OperatorSet(oldOperator, _newOperator); + } + + function packUserOpData(UserOperation calldata _userOp) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + _userOp.sender, + _userOp.nonce, + keccak256(_userOp.initCode), + keccak256(_userOp.callData), + _userOp.callGasLimit, + _userOp.verificationGasLimit, + _userOp.preVerificationGas, + _userOp.maxFeePerGas, + _userOp.maxPriorityFeePerGas + ) + ); + } + + function getFreePrivilegeModeHash( + UserOperation calldata _userOp, + FreePrivilegeModeData memory _freePrivilegeModeData + ) public view returns (bytes32) { + return + keccak256( + abi.encode(packUserOpData(_userOp), block.chainid, address(this), _freePrivilegeModeData.validUntil) + ); + } + + function parseFreePrivilegeModeData( + bytes calldata _paymasterAndData + ) public pure returns (FreePrivilegeModeData memory) { + uint48 validUntil = uint48(bytes6(_paymasterAndData[21:27])); + bytes memory signature = bytes(_paymasterAndData[27:]); + return FreePrivilegeModeData(validUntil, signature); + } + + function getPayWithTokenModeHash( + UserOperation calldata _userOp, + PayWithTokenModeData memory _payWithTokenModeData + ) public view returns (bytes32) { + return + keccak256( + abi.encode( + packUserOpData(_userOp), + block.chainid, + address(this), + _payWithTokenModeData.validUntil, + address(_payWithTokenModeData.token), + _payWithTokenModeData.exchangeRate + ) + ); + } + + function parsePayWithTokenModeData( + bytes calldata _paymasterAndData + ) public pure returns (PayWithTokenModeData memory) { + uint48 validUntil = uint48(bytes6(_paymasterAndData[21:27])); + IERC20 token = IERC20(address(bytes20(_paymasterAndData[27:47]))); + uint256 exchangeRate = uint256(bytes32(_paymasterAndData[47:79])); + bytes memory signature = bytes(_paymasterAndData[79:]); + return PayWithTokenModeData(validUntil, token, exchangeRate, signature); + } + + function getMultiChainUniversalModeSponsorInfoHash( + UserOperation[] calldata _userOps, + uint256[] memory _chainIds + ) public view returns (bytes32) { + require( + _userOps.length > 0 && _chainIds.length > 0 && _userOps.length == _chainIds.length, + "VersaUniversaPaymaster: params length dismatch" + ); + uint256 dataLength = _userOps.length; + bytes32 packDataHash; + for (uint256 i = 0; i < dataLength; ++i) { + packDataHash = keccak256(abi.encode(packDataHash, packUserOpData(_userOps[i]), _chainIds[i])); + } + return keccak256(abi.encode(packDataHash, address(this))); + } + + function getMultiChainUniversalModePaymentHash( + MultiChainUniversalModePaymentData memory _multiChainUniversalModePaymentData + ) public view returns (bytes32) { + return + keccak256( + abi.encode( + block.chainid, + address(this), + _multiChainUniversalModePaymentData.validUntil, + address(_multiChainUniversalModePaymentData.token), + _multiChainUniversalModePaymentData.value, + _multiChainUniversalModePaymentData.sponsorInfoHash + ) + ); + } + + function parseMultiChainUniversalModePaymentData( + bytes calldata _paymentData + ) public pure returns (MultiChainUniversalModePaymentData memory) { + uint48 validUntil = uint48(bytes6(_paymentData[:6])); + IERC20 token = IERC20(address(bytes20(_paymentData[6:26]))); + uint256 value = uint256(bytes32(_paymentData[26:58])); + bytes32 sponsorInfoHash = bytes32(_paymentData[58:90]); + bytes memory signature = bytes(_paymentData[90:]); + return MultiChainUniversalModePaymentData(validUntil, token, value, sponsorInfoHash, signature); + } + + function _validateMultiChainUniversalModePaymentSignature( + MultiChainUniversalModePaymentData memory _multiChainUniversalModePaymentData + ) internal view returns (bool) { + require(_multiChainUniversalModePaymentData.signature.length == 65, "E203"); + bytes32 _hash = getMultiChainUniversalModePaymentHash(_multiChainUniversalModePaymentData) + .toEthSignedMessageHash(); + if (_hash.recover(_multiChainUniversalModePaymentData.signature) != operator()) { + return false; + } + return true; + } + + function prePayForMultiChainUniversalModeSponsor(bytes calldata _paymentData) external payable { + MultiChainUniversalModePaymentData + memory multiChainUniversalModePaymentData = parseMultiChainUniversalModePaymentData(_paymentData); + require( + universalSponsorStatusData[multiChainUniversalModePaymentData.sponsorInfoHash].status == + UniversalSponsorStatus.UNPAID, + "VersaUniversalPaymaster: paid" + ); + require( + multiChainUniversalModePaymentData.validUntil >= block.timestamp, + "VersaUniversalPaymaster: request expired" + ); + require( + _validateMultiChainUniversalModePaymentSignature(multiChainUniversalModePaymentData), + "VersaUniversalPaymaster: validate signature failed" + ); + if (address(multiChainUniversalModePaymentData.token) != address(0)) { + multiChainUniversalModePaymentData.token.safeTransferFrom( + msg.sender, + address(this), + multiChainUniversalModePaymentData.value + ); + } else { + require( + msg.value == multiChainUniversalModePaymentData.value, + "VersaUniversalPaymaster: payment value mismatch" + ); + } + universalSponsorStatusData[ + multiChainUniversalModePaymentData.sponsorInfoHash + ] = MultiChainUniversalModeSponsorStatusData(UniversalSponsorStatus.PAID_UNSPONSORED, uint48(block.timestamp)); + emit MultiChainUniversalPrePaid( + msg.sender, + address(multiChainUniversalModePaymentData.token), + multiChainUniversalModePaymentData.value, + multiChainUniversalModePaymentData.sponsorInfoHash + ); + } + + function getMultiChainUniversalModeHash( + UserOperation calldata _userOp, + MultiChainUniversalModeData memory _multiChainUniversalModeData + ) public view returns (bytes32) { + return + keccak256( + abi.encode( + packUserOpData(_userOp), + block.chainid, + address(this), + _multiChainUniversalModeData.validUntil, + _multiChainUniversalModeData.sponsorInfoHash + ) + ); + } + + function parseMultiChainUniversalModeData( + bytes calldata _paymasterAndData + ) public pure returns (MultiChainUniversalModeData memory) { + uint48 validUntil = uint48(bytes6(_paymasterAndData[21:27])); + bytes32 sponsorInfoHash = bytes32(_paymasterAndData[27:59]); + bytes memory signature = bytes(_paymasterAndData[59:]); + return MultiChainUniversalModeData(validUntil, sponsorInfoHash, signature); + } + + function _validateSignature(UserOperation calldata userOp, SponsorMode sponsorMode) internal view returns (bool) { + bytes memory signature; + bytes32 _hash; + if (sponsorMode == SponsorMode.FREE_PRIVILEGE) { + FreePrivilegeModeData memory freePrivilegeModeData = parseFreePrivilegeModeData(userOp.paymasterAndData); + signature = freePrivilegeModeData.signature; + _hash = getFreePrivilegeModeHash(userOp, freePrivilegeModeData).toEthSignedMessageHash(); + } else if (sponsorMode == SponsorMode.PAY_WITH_TOKEN) { + PayWithTokenModeData memory payWithTokenModeData = parsePayWithTokenModeData(userOp.paymasterAndData); + signature = payWithTokenModeData.signature; + _hash = getPayWithTokenModeHash(userOp, payWithTokenModeData).toEthSignedMessageHash(); + } else if (sponsorMode == SponsorMode.MULTI_CHAIN_UNIVERSAL) { + MultiChainUniversalModeData memory multiChainUniversalModeData = parseMultiChainUniversalModeData( + userOp.paymasterAndData + ); + signature = multiChainUniversalModeData.signature; + _hash = getMultiChainUniversalModeHash(userOp, multiChainUniversalModeData).toEthSignedMessageHash(); + } else { + return false; + } + require(signature.length == 65, "E203"); + if (_hash.recover(signature) != operator()) { + return false; + } + return true; + } + + function _validatePaymasterUserOp( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) internal view override returns (bytes memory context, uint256 validationData) { + (userOpHash, maxCost); + + SponsorMode sponsorMode = SponsorMode(uint8(bytes1(userOp.paymasterAndData[20:21]))); + if (!_validateSignature(userOp, sponsorMode)) { + return ("", _packValidationData(true, 0, 0)); + } + address sender = userOp.getSender(); + + if (sponsorMode == SponsorMode.FREE_PRIVILEGE) { + FreePrivilegeModeData memory freePrivilegeModeData = parseFreePrivilegeModeData(userOp.paymasterAndData); + context = abi.encode(sponsorMode, sender); + validationData = _packValidationData(false, freePrivilegeModeData.validUntil, 0); + } else if (sponsorMode == SponsorMode.PAY_WITH_TOKEN) { + PayWithTokenModeData memory payWithTokenModeData = parsePayWithTokenModeData(userOp.paymasterAndData); + context = abi.encode( + sponsorMode, + sender, + payWithTokenModeData.token, + payWithTokenModeData.exchangeRate, + userOp.maxFeePerGas, + userOp.maxPriorityFeePerGas + ); + validationData = _packValidationData(false, payWithTokenModeData.validUntil, 0); + } else if (sponsorMode == SponsorMode.MULTI_CHAIN_UNIVERSAL) { + MultiChainUniversalModeData memory multiChainUniversalModeData = parseMultiChainUniversalModeData( + userOp.paymasterAndData + ); + context = abi.encode(sponsorMode, sender, multiChainUniversalModeData.sponsorInfoHash); + validationData = _packValidationData(false, multiChainUniversalModeData.validUntil, 0); + } else { + return ("", _packValidationData(true, 0, 0)); + } + } + + function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal override { + (mode); + + SponsorMode sponsorMode = SponsorMode(uint8(bytes1(context[31:32]))); + address sender = address(bytes20(context[44:64])); + if (sponsorMode == SponsorMode.FREE_PRIVILEGE) { + emit FreePrivilegeSponsored(sender, actualGasCost); + } else if (sponsorMode == SponsorMode.PAY_WITH_TOKEN) { + (, , IERC20 token, uint256 exchangeRate, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas) = abi.decode( + context, + (SponsorMode, address, IERC20, uint256, uint256, uint256) + ); + uint256 gasPricePostOp; + if (maxFeePerGas == maxPriorityFeePerGas) { + gasPricePostOp = maxFeePerGas; + } else { + gasPricePostOp = Math.min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); + } + uint256 actualTokenCost = ((actualGasCost + (COST_OF_POST * gasPricePostOp)) * exchangeRate) / 1e18; + token.safeTransferFrom(sender, address(this), actualTokenCost); + emit PayWithTokenSponsored(sender, actualGasCost); + } else if (sponsorMode == SponsorMode.MULTI_CHAIN_UNIVERSAL) { + (, , bytes32 sponsorInfoHash) = abi.decode(context, (SponsorMode, address, bytes32)); + universalSponsorStatusData[sponsorInfoHash] = MultiChainUniversalModeSponsorStatusData( + UniversalSponsorStatus.PAID_SPONSORED, + uint48(block.timestamp) + ); + emit MultiChainUniversalSponsored(sender, sponsorInfoHash, actualGasCost); + } else { + revert(); + } + } + + function setOperator(address newOperator) external onlyOwner { + _setOperator(newOperator); + } + + struct TokenWithdrawInfo { + IERC20 token; + uint256 amount; + } + + function _withdraw(TokenWithdrawInfo calldata _tokenWithdrawInfo, address _target) internal { + if (address(_tokenWithdrawInfo.token) != address(0)) { + _tokenWithdrawInfo.token.safeTransfer(_target, _tokenWithdrawInfo.amount); + } else { + payable(_target).transfer(_tokenWithdrawInfo.amount); + } + } + + function withdrawTokensTo(TokenWithdrawInfo calldata _tokenWithdrawInfo, address _target) external onlyOwner { + _withdraw(_tokenWithdrawInfo, _target); + } + + function batchWithdrawTokensTo( + TokenWithdrawInfo[] calldata _tokenWithdrawInfo, + address _target + ) external onlyOwner { + for (uint256 i = 0; i < _tokenWithdrawInfo.length; ++i) { + _withdraw(_tokenWithdrawInfo[i], _target); + } + } + + function setSwapRouter(address _v2SwapRouter02, address _v3SwapRouter02) external onlyOwner { + _setSwapRouter(_v2SwapRouter02, _v3SwapRouter02); + } + + function approveRouter(IERC20[] calldata tokens, uint256[] calldata amount) external onlyOperator { + _approveRouter(tokens, amount); + } + + function convertTokensAndDeposit( + V2SwapParas[] memory _v2SwapParas, + V3SwapParas[] calldata _v3SwapParas + ) external onlyOperator returns (uint256 deposited) { + uint256 i; + uint256 ethOut; + for (; i < _v2SwapParas.length; ++i) { + ethOut += _convert(_v2SwapParas[i]); + } + + uint256 wethOut; + for (i = 0; i < _v3SwapParas.length; ++i) { + wethOut += _convert(_v3SwapParas[i]); + } + WETH.withdraw(wethOut); + deposited = ethOut + wethOut; + this.deposit{ value: deposited }(); + } + + receive() external payable {} +} diff --git a/contracts/paymaster/VersaVerifyingPaymaster.sol b/contracts/paymaster/VersaVerifyingPaymaster.sol index bfabe84..0177abd 100644 --- a/contracts/paymaster/VersaVerifyingPaymaster.sol +++ b/contracts/paymaster/VersaVerifyingPaymaster.sol @@ -146,6 +146,8 @@ contract VersaVerifyingPaymaster 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, @@ -167,10 +169,8 @@ contract VersaVerifyingPaymaster is BasePaymaster { if (sponsorMode == SponsorMode.GAS_AND_FEE) { actualTokenCost = actualTokenCost + fee; } - if (mode != PostOpMode.postOpReverted) { - token.safeTransferFrom(account, address(this), actualTokenCost); - balances[token] += actualTokenCost; - emit UserOperationSponsored(account, address(token), actualTokenCost); - } + token.safeTransferFrom(account, address(this), actualTokenCost); + balances[token] += actualTokenCost; + emit UserOperationSponsored(account, address(token), actualTokenCost); } } diff --git a/contracts/paymaster/interfaces/IUniswapRouter.sol b/contracts/paymaster/interfaces/IUniswapRouter.sol new file mode 100644 index 0000000..0a58b90 --- /dev/null +++ b/contracts/paymaster/interfaces/IUniswapRouter.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.6.2; + +interface IUniswapV2Router02 { + function swapExactTokensForETH( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); +} + +interface IUniswapV3Router02 { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, + /// and swap the entire amount, enabling contracts to send tokens before calling this function. + /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata + /// @return amountOut The amount of the received token + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + + struct ExactInputParams { + bytes path; + address recipient; + uint256 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, + /// and swap the entire amount, enabling contracts to send tokens before calling this function. + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata + /// @return amountOut The amount of the received token + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); +} diff --git a/contracts/paymaster/interfaces/IWETH.sol b/contracts/paymaster/interfaces/IWETH.sol new file mode 100644 index 0000000..f9d8694 --- /dev/null +++ b/contracts/paymaster/interfaces/IWETH.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.5.0; + +interface IWETH { + function deposit() external payable; + + function transfer(address to, uint256 value) external returns (bool); + + function withdraw(uint256) external; + + function balanceOf(address) external view returns (uint256); +} diff --git a/contracts/plugin/validator/ECDSAValidator.sol b/contracts/plugin/validator/ECDSAValidator.sol index 49d79b8..677a9f5 100644 --- a/contracts/plugin/validator/ECDSAValidator.sol +++ b/contracts/plugin/validator/ECDSAValidator.sol @@ -78,10 +78,13 @@ contract ECDSAValidator is BaseValidator { // Instant transaction signature length: 20 bytes validator address + 1 byte sig type + 65 bytes signature // Scheduled transaction signature length: 20 bytes validator address + 1 byte sig type // + 12 bytes time range data + 64 bytes fee data + 65 bytes signature - // The signature type must be INSTANT_TRANSACTION or SCHEDULE_TRANSACTION here + // The signature type must be INSTANT_TX_SIG or SCHEDULE_TX_SIG here + // Multichain userOp signature: 20 bytes validator address + 1 byte sig type + + // + 12 bytes time range data + 32 bytes merkle root + at least 32 bytes proof data + 65 bytes signature if ( - (splitedSig.signatureType == SignatureHandler.INSTANT_TRANSACTION && sigLength != 86) || - (splitedSig.signatureType == SignatureHandler.SCHEDULE_TRANSACTION && sigLength != 162) + (splitedSig.signatureType == SignatureHandler.INSTANT_TX_SIG && sigLength != 86) || + (splitedSig.signatureType == SignatureHandler.SCHEDULE_TX_SIG && sigLength != 162) || + (splitedSig.signatureType == SignatureHandler.MULTICHAIN_OP_SIG && sigLength < 162) ) { revert("Invalid signature length"); } diff --git a/deploy/deployAll.ts b/deploy/deployAll.ts index b5e302c..f0053fc 100644 --- a/deploy/deployAll.ts +++ b/deploy/deployAll.ts @@ -3,47 +3,124 @@ import * as deployer from "./helper/deployer"; import { VersaAccountFactoryData } from "./helper/deployer"; import mumbaiAddresses from "./addresses/polygonMumbai.json"; import scrollSepoliaAddresses from "./addresses/scrollSepolia.json"; +import arbitrumSepoliaAddresses from "./addresses/arbitrumSepolia.json"; +import arbitrumGoerliAddresses from "./addresses/arbitrumGoerli.json"; +import baseSepoliaAddresses from "./addresses/baseSepolia.json"; +import baseGoerliAddresses from "./addresses/baseGoerli.json"; +import optimisticSepoliaAddresses from "./addresses/optimisticSepolia.json"; +import optimisticGoerliAddresses from "./addresses/optimisticGoerli.json"; +import polygonzkevmTestnetAddresses from "./addresses/polygonzkevmTestnet.json"; + +import scrollAddresses from "./addresses/scroll.json"; +import arbitrumAddresses from "./addresses/arbitrum.json"; +import baseAddresses from "./addresses/base.json"; +import polygonAddresses from "./addresses/polygon.json"; +import optimismAddresses from "./addresses/optimism.json"; +import polygonzkevmAddress from "./addresses/polygonzkevm.json"; + import fs from "fs"; import { deployConfig } from "./helper/config"; import * as readline from "readline-sync"; +import { parseEther } from "ethers/lib/utils"; +import { salt } from "../scripts/utils/config"; async function deployWithAddresses(addresses: any, config: any) { const deployCompatibilityFallbackHandler = readline.keyInYN("Do you need to deploy compatibilityFallbackHandler?"); if (deployCompatibilityFallbackHandler) { const compatibilityFallbackHandler = await deployer.deployCompatibilityFallbackHandler(config.salt); - addresses.compatibilityFallbackHandler = compatibilityFallbackHandler.address; + if (compatibilityFallbackHandler.address != ethers.constants.AddressZero) { + addresses.compatibilityFallbackHandler = compatibilityFallbackHandler.address; + } } - const deployVersaSingleton = readline.keyInYN("Do you need to deploy versa singleton and versa factory?"); + const deployVersaSingleton = readline.keyInYN("Do you need to deploy versa singleton?"); if (deployVersaSingleton) { const versaSingleton = await deployer.deployVersaSingleton(config.entryPointAddress, config.salt); - addresses.versaSingleton = versaSingleton.address; + if (versaSingleton.address != ethers.constants.AddressZero) { + addresses.versaSingleton = versaSingleton.address; + } + } + const deployFactory = readline.keyInYN("Do you need to deploy versa factory?"); + if (deployFactory) { const versaAccountFactoryData: VersaAccountFactoryData = { versaSingleton: addresses.versaSingleton, defaultFallbackHandler: addresses.compatibilityFallbackHandler, + entryPoint: deployConfig.entryPointAddress, + owner: deployConfig.versaFactoryOwner, }; const versaFactory = await deployer.deployVersaAccountFactory(versaAccountFactoryData, config.salt); - addresses.versaAccountFactory = versaFactory.address; + if (versaFactory.address != ethers.constants.AddressZero) { + addresses.versaAccountFactory = versaFactory.address; + } } - const deployPaymaster = readline.keyInYN("Do you need to deploy paymaster?"); - if (deployPaymaster) { + const needStake = readline.keyInYN("Do you need to stake for factory?"); + if (needStake) { + const stakeAmount = readline.question("Please enter stake amount(in 1e18): "); + const unstakeDelaySec = readline.question("Please enter the unstake delay(in seconds): "); + if (parseEther(stakeAmount.toString()).gt(0)) { + const versaFactory = await ethers.getContractAt("VersaAccountFactory", addresses.versaAccountFactory); + let tx = await versaFactory.addStake(unstakeDelaySec, { value: parseEther(stakeAmount.toString()) }); + await tx.wait(); + console.log("Staked success!"); + } + } + + const deployVerifyingPaymaster = readline.keyInYN("Do you need to deploy verifying paymaster?"); + if (deployVerifyingPaymaster) { + const versaVerifyingPaymaster = await deployer.deployVersaVerifyingPaymaster( + config.entryPointAddress, + config.verifyingPaymasterOwner, + config.salt + ); + if (versaVerifyingPaymaster.address != ethers.constants.AddressZero) { + addresses.versaVerifyingPaymaster = versaVerifyingPaymaster.address; + } } - const deloyPlugins = readline.keyInYN("Do you need to deploy plugins?"); - if (deloyPlugins) { + const deployUniversalPaymaster = readline.keyInYN("Do you need to deploy universal paymaster?"); + if (deployUniversalPaymaster) { + const universalPaymaster = await deployer.deployVersaUniversalPaymaster( + config.entryPointAddress, + config.universalPaymasterOwner, + config.salt + ); + if (universalPaymaster.address != ethers.constants.AddressZero) { + addresses.versaUniversalPaymaster = universalPaymaster.address; + } + } + + const deployECDSAValidator = readline.keyInYN("Do you need to deploy ecdsa validator?"); + if (deployECDSAValidator) { const ecdsaValidator = await deployer.deployECDSAValidator(config.salt); - addresses.ecdsaValidator = ecdsaValidator.address; + if (ecdsaValidator.address != ethers.constants.AddressZero) { + addresses.ecdsaValidator = ecdsaValidator.address; + } + } + const deployMultisigValidator = readline.keyInYN("Do you need to deploy multi-sig validator?"); + if (deployMultisigValidator) { const multisigValidator = await deployer.deployMultiSigValidator(config.salt); - addresses.multisigValidator = multisigValidator.address; + if (multisigValidator.address != ethers.constants.AddressZero) { + addresses.multisigValidator = multisigValidator.address; + } + } - const sessionKeyValdiator = await deployer.deploySessionKeyValidator(config.salt); - addresses.sessionKeyValdiator = sessionKeyValdiator.address; + const deploySessionKeyValidator = readline.keyInYN("Do you need to deploy sessionkey validator?"); + if (deploySessionKeyValidator) { + const sessionKeyValidator = await deployer.deploySessionKeyValidator(config.salt); + if (sessionKeyValidator.address != ethers.constants.AddressZero) { + addresses.sessionKeyValidator = sessionKeyValidator.address; + } + } + const deploySpendingLimitHooks = readline.keyInYN("Do you need to deploy spendingLimitHooks?"); + if (deploySpendingLimitHooks) { const spendingLimitHooks = await deployer.deploySpendingLimitHooks(config.salt); - addresses.spendingLimitHooks = spendingLimitHooks.address; + if (spendingLimitHooks.address != ethers.constants.AddressZero) { + addresses.spendingLimitHooks = spendingLimitHooks.address; + } } return addresses; } @@ -51,20 +128,99 @@ async function deployWithAddresses(addresses: any, config: any) { async function main() { const [signer] = await ethers.getSigners(); const network = await signer.provider?.getNetwork(); + console.log(network?.chainId); switch (network?.chainId) { + case 137: { + const result = await deployWithAddresses(polygonAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/polygon.json'"); + fs.writeFileSync("deploy/addresses/polygon.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 1101: { + const result = await deployWithAddresses(polygonzkevmAddress, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/polygonzkevm.json'"); + fs.writeFileSync("deploy/addresses/polygonzkevm.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 1442: { + const result = await deployWithAddresses(polygonzkevmTestnetAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/polygonzkevmTestnet.json'"); + fs.writeFileSync("deploy/addresses/polygonzkevmTestnet.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } case 80001: { const result = await deployWithAddresses(mumbaiAddresses, deployConfig); console.log("writing changed address to output file 'deploy/addresses/polygonMumbai.json'"); fs.writeFileSync("deploy/addresses/polygonMumbai.json", JSON.stringify(result, null, "\t"), "utf8"); break; } + case 534352: { + const result = await deployWithAddresses(scrollAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/scroll.json'"); + fs.writeFileSync("deploy/addresses/scroll.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } case 534351: { const result = await deployWithAddresses(scrollSepoliaAddresses, deployConfig); console.log("writing changed address to output file 'deploy/addresses/scrollSepolia.json'"); fs.writeFileSync("deploy/addresses/scrollSepolia.json", JSON.stringify(result, null, "\t"), "utf8"); break; } + case 8453: { + const result = await deployWithAddresses(baseAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/base.json'"); + fs.writeFileSync("deploy/addresses/base.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 84532: { + const result = await deployWithAddresses(baseSepoliaAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/baseSepolia.json'"); + fs.writeFileSync("deploy/addresses/baseSepolia.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 84531: { + const result = await deployWithAddresses(baseGoerliAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/baseGoerli.json'"); + fs.writeFileSync("deploy/addresses/baseGoerli.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 42161: { + const result = await deployWithAddresses(arbitrumAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/arbitrum.json'"); + fs.writeFileSync("deploy/addresses/arbitrum.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 421614: { + const result = await deployWithAddresses(arbitrumSepoliaAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/arbitrumSepolia.json'"); + fs.writeFileSync("deploy/addresses/arbitrumSepolia.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 421613: { + const result = await deployWithAddresses(arbitrumGoerliAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/arbitrumGoerli.json'"); + fs.writeFileSync("deploy/addresses/arbitrumGoerli.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 10: { + const result = await deployWithAddresses(optimismAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/optimism.json'"); + fs.writeFileSync("deploy/addresses/optimism.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 11155420: { + const result = await deployWithAddresses(optimisticSepoliaAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/optimisticSepolia.json'"); + fs.writeFileSync("deploy/addresses/optimisticSepolia.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } + case 420: { + const result = await deployWithAddresses(optimisticGoerliAddresses, deployConfig); + console.log("writing changed address to output file 'deploy/addresses/optimisticGoerli.json'"); + fs.writeFileSync("deploy/addresses/optimisticGoerli.json", JSON.stringify(result, null, "\t"), "utf8"); + break; + } default: { console.log("unsupported network"); } diff --git a/deploy/helper/config.ts b/deploy/helper/config.ts index 04a81fe..694dc15 100644 --- a/deploy/helper/config.ts +++ b/deploy/helper/config.ts @@ -4,6 +4,8 @@ export const universalSingletonFactoryAddress = "0xce0042B868300000d44A59004Da54 export const deployConfig = { entryPointAddress: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", - salt: keccak256("0x" + Buffer.from("versa-wallet-v1.0.0-test").toString("hex")), - verifyingPaymasterOwner: "0x43370254AAAce51006cf368eb7734DB43Ddf9880", + salt: keccak256("0x" + Buffer.from("Versa-Wallet-v1.0.0-Mainnet").toString("hex")), + verifyingPaymasterOwner: "0xba4528386736a13b8aC4E9876AC0e220eEd37deb", + universalPaymasterOwner: "0xba4528386736a13b8aC4E9876AC0e220eEd37deb", + versaFactoryOwner: "0x147c18AC67F509B031B696878bB18Abc9F417eE3", }; diff --git a/deploy/helper/deployer.ts b/deploy/helper/deployer.ts index 7c1539f..3a9463e 100644 --- a/deploy/helper/deployer.ts +++ b/deploy/helper/deployer.ts @@ -4,6 +4,8 @@ import { universalSingletonFactoryAddress as singletonFactoryAddress } from "./c export interface VersaAccountFactoryData { versaSingleton: string; defaultFallbackHandler: string; + entryPoint: string; + owner: string; } const singletonFactoryABI = ["function deploy(bytes _initCode,bytes32 _salt) returns (address createdContract)"]; @@ -12,147 +14,177 @@ async function getSingletonFactory() { return await ethers.getContractAt(singletonFactoryABI, singletonFactoryAddress); } +const gaslimit = 5000000; + export async function deployVersaAccountFactory(data: VersaAccountFactoryData, salt: string) { + const [signer] = await ethers.getSigners(); const singletonFactory = await getSingletonFactory(); const VersaAccountFactory = await ethers.getContractFactory("VersaAccountFactory"); - const initCode = VersaAccountFactory.getDeployTransaction(data.versaSingleton, data.defaultFallbackHandler).data!; + const initCode = VersaAccountFactory.getDeployTransaction( + data.versaSingleton, + data.defaultFallbackHandler, + data.entryPoint, + data.owner + ).data!; const address = await singletonFactory.callStatic.deploy(initCode, salt); - - let tx; - const [signer] = await ethers.getSigners(); - tx = await singletonFactory.deploy(initCode, salt, { gasLimit: 5000000 }); - await tx.wait(); if (address == ethers.constants.AddressZero) { - console.log("Deploy failed, this contract with this salt should have been already deployed"); + console.log("Can't deploy, this contract with this salt should have been already deployed"); } else { + let tx; + tx = await singletonFactory.deploy(initCode, salt, { gasLimit: gaslimit }); + // tx = await singletonFactory.deploy(initCode, salt); + await tx.wait(); console.log("VersaAccountFactory deployed to:", address); } return ethers.getContractAt("VersaAccountFactory", address); } export async function deployVersaSingleton(entryPoint: string, salt: string) { + const [signer] = await ethers.getSigners(); const singletonFactory = await getSingletonFactory(); const VersaSingleton = await ethers.getContractFactory("VersaWallet"); const initCode = VersaSingleton.getDeployTransaction(entryPoint).data!; const address = await singletonFactory.callStatic.deploy(initCode, salt); - let tx; - const [signer] = await ethers.getSigners(); - tx = await singletonFactory.deploy(initCode, salt, { gasLimit: 5000000 }); - await tx.wait(); if (address == ethers.constants.AddressZero) { - console.log("Deploy failed, this contract with this salt should have been already deployed"); + console.log("Can't deploy, this contract with this salt should have been already deployed"); } else { - console.log("VersaSingleton deployed to:", address); + let tx; + + tx = await singletonFactory.deploy(initCode, salt, { gasLimit: gaslimit }); + // tx = await singletonFactory.deploy(initCode, salt); + await tx.wait(); + console.log("VersaWallet deployed to:", address); } return await ethers.getContractAt("VersaWallet", address); } export async function deployVersaVerifyingPaymaster(entryPoint: string, verifyingPaymasterOwner: string, salt: string) { + const [signer] = await ethers.getSigners(); const singletonFactory = await getSingletonFactory(); const VersaVerifyingPaymaster = await ethers.getContractFactory("VersaVerifyingPaymaster"); const initCode = VersaVerifyingPaymaster.getDeployTransaction(entryPoint, verifyingPaymasterOwner).data!; const address = await singletonFactory.callStatic.deploy(initCode, salt); - - let tx; - tx = await singletonFactory.deploy(initCode, salt, { gasLimit: 5000000 }); - await tx.wait(); if (address == ethers.constants.AddressZero) { - console.log("Deploy failed, this contract with this salt should have been already deployed"); + console.log("Can't deploy, this contract with this salt should have been already deployed"); } else { - console.log("VersaSingleton deployed to:", address); + let tx; + tx = await singletonFactory.deploy(initCode, salt, { gasLimit: gaslimit }); + // tx = await singletonFactory.deploy(initCode, salt); + await tx.wait(); + console.log("VersaVerifyingPaymaster deployed to:", address); } return await ethers.getContractAt("VersaVerifyingPaymaster", address); } +export async function deployVersaUniversalPaymaster(entryPoint: string, universalPaymasterOwner: string, salt: string) { + const [signer] = await ethers.getSigners(); + const singletonFactory = await getSingletonFactory(); + const VersaVerifyingPaymaster = await ethers.getContractFactory("VersaUniversalPaymaster"); + const initCode = VersaVerifyingPaymaster.getDeployTransaction(entryPoint, universalPaymasterOwner).data!; + const address = await singletonFactory.callStatic.deploy(initCode, salt); + + if (address == ethers.constants.AddressZero) { + console.log("Can't deploy, this contract with this salt should have been already deployed"); + } else { + let tx; + tx = await singletonFactory.deploy(initCode, salt, { gasLimit: gaslimit }); + // tx = await singletonFactory.deploy(initCode, salt); + await tx.wait(); + console.log("VersaUniversalPaymaster deployed to:", address); + } + return await ethers.getContractAt("VersaUniversalPaymaster", address); +} export async function deployCompatibilityFallbackHandler(salt: string) { + const [signer] = await ethers.getSigners(); const singletonFactory = await getSingletonFactory(); const CompatibilityFallbackHandler = await ethers.getContractFactory("CompatibilityFallbackHandler"); const initCode = CompatibilityFallbackHandler.getDeployTransaction().data!; const address = await singletonFactory.callStatic.deploy(initCode, salt); - let tx; - const [signer] = await ethers.getSigners(); - tx = await singletonFactory.deploy(initCode, salt, { gasLimit: 5000000 }); - await tx.wait(); if (address == ethers.constants.AddressZero) { - console.log("Deploy failed, this contract with this salt should have been already deployed"); + console.log("Can't deploy, this contract with this salt should have been already deployed"); } else { + let tx; + tx = await singletonFactory.deploy(initCode, salt, { gasLimit: gaslimit }); + // tx = await singletonFactory.deploy(initCode, salt); + await tx.wait(); console.log("CompatibilityFallbackHandler deployed to:", address); } return await ethers.getContractAt("CompatibilityFallbackHandler", address); } export async function deploySpendingLimitHooks(salt: string) { + const [signer] = await ethers.getSigners(); const singletonFactory = await getSingletonFactory(); const SpendingLimitHooks = await ethers.getContractFactory("SpendingLimitHooks"); const initCode = SpendingLimitHooks.getDeployTransaction().data!; const address = await singletonFactory.callStatic.deploy(initCode, salt); - let tx; - const [signer] = await ethers.getSigners(); - tx = await singletonFactory.deploy(initCode, salt, { gasLimit: 5000000 }); - await tx.wait(); if (address == ethers.constants.AddressZero) { - console.log("Deploy failed, this contract with this salt should have been already deployed"); + console.log("Can't deploy, this contract with this salt should have been already deployed"); } else { + let tx; + tx = await singletonFactory.deploy(initCode, salt, { gasLimit: gaslimit }); + // tx = await singletonFactory.deploy(initCode, salt); + await tx.wait(); console.log("SpendingLimitHooks deployed to:", address); } return await ethers.getContractAt("SpendingLimitHooks", address); } export async function deployECDSAValidator(salt: string) { + const [signer] = await ethers.getSigners(); const singletonFactory = await getSingletonFactory(); const ECDSAValidator = await ethers.getContractFactory("ECDSAValidator"); const initCode = ECDSAValidator.getDeployTransaction().data!; const address = await singletonFactory.callStatic.deploy(initCode, salt); - let tx; - const [signer] = await ethers.getSigners(); - tx = await singletonFactory.deploy(initCode, salt, { gasLimit: 5000000 }); - await tx.wait(); - if (address == ethers.constants.AddressZero) { - console.log("Deploy failed, this contract with this salt should have been already deployed"); + console.log("Can't deploy, this contract with this salt should have been already deployed"); } else { + let tx; + tx = await singletonFactory.deploy(initCode, salt, { gasLimit: gaslimit }); + // tx = await singletonFactory.deploy(initCode, salt); + await tx.wait(); console.log("ECDSAValidator deployed to:", address); } return await ethers.getContractAt("ECDSAValidator", address); } export async function deployMultiSigValidator(salt: string) { + const [signer] = await ethers.getSigners(); const singletonFactory = await getSingletonFactory(); const MultisigValidator = await ethers.getContractFactory("MultiSigValidator"); const initCode = MultisigValidator.getDeployTransaction().data!; const address = await singletonFactory.callStatic.deploy(initCode, salt); - let tx; - const [signer] = await ethers.getSigners(); - tx = await singletonFactory.deploy(initCode, salt, { gasLimit: 5000000 }); - await tx.wait(); - if (address == ethers.constants.AddressZero) { - console.log("Deploy failed, this contract with this salt should have been already deployed"); + console.log("Can't deploy, this contract with this salt should have been already deployed"); } else { + let tx; + tx = await singletonFactory.deploy(initCode, salt, { gasLimit: gaslimit }); + // tx = await singletonFactory.deploy(initCode, salt); + await tx.wait(); console.log("MultiSigValidator deployed to:", address); } return await ethers.getContractAt("MultiSigValidator", address); } export async function deploySessionKeyValidator(salt: string) { + const [signer] = await ethers.getSigners(); const singletonFactory = await getSingletonFactory(); const sessionKeyValdiator = await ethers.getContractFactory("SessionKeyValidator"); const initCode = sessionKeyValdiator.getDeployTransaction().data!; const address = await singletonFactory.callStatic.deploy(initCode, salt); - let tx; - const [signer] = await ethers.getSigners(); - tx = await singletonFactory.deploy(initCode, salt, { gasLimit: 5000000 }); - await tx.wait(); - if (address == ethers.constants.AddressZero) { - console.log("Deploy failed, this contract with this salt should have been already deployed"); + console.log("Can't deploy, this contract with this salt should have been already deployed"); } else { + let tx; + tx = await singletonFactory.deploy(initCode, salt, { gasLimit: gaslimit }); + // tx = await singletonFactory.deploy(initCode, salt); + await tx.wait(); console.log("SessionKeyValidator deployed to:", address); } return await ethers.getContractAt("SessionKeyValidator", address); diff --git a/deploy/helper/verifier.ts b/deploy/helper/verifier.ts index 19dce74..fa8bd1b 100644 --- a/deploy/helper/verifier.ts +++ b/deploy/helper/verifier.ts @@ -1,6 +1,21 @@ -import hre from "hardhat"; +import hre, { ethers } from "hardhat"; import mumbaiAddresses from "../addresses/polygonMumbai.json"; import scrollSepoliaAddresses from "../addresses/scrollSepolia.json"; +import arbitrumGoerliAddresses from "../addresses/arbitrumGoerli.json"; +import arbitrumSepoliaAddresses from "../addresses/arbitrumSepolia.json"; +import baseGoerliAddresses from "../addresses/baseGoerli.json"; +import baseSepoliaAddresses from "../addresses/baseSepolia.json"; +import optimisticGoerliAddresses from "../addresses/optimisticGoerli.json"; +import optimisticSepoliaAddresses from "../addresses/optimisticSepolia.json"; + +import scrollAddresses from "../addresses/scroll.json"; +import arbitrumAddresses from "../addresses/arbitrum.json"; +import baseAddresses from "../addresses/base.json"; +import polygonAddresses from "../addresses/polygon.json"; +import optimismAddresses from "../addresses/optimism.json"; +import polygonzkevmAddresses from "../addresses/polygonzkevm.json"; + +import { deployConfig } from "./config"; async function verify(address: string, constructorArguments?: any) { await hre.run("verify:verify", { @@ -12,46 +27,93 @@ async function verify(address: string, constructorArguments?: any) { async function main() { const [signer] = await ethers.getSigners(); const network = await signer.provider?.getNetwork(); + console.log("chainid", network?.chainId); + let addresses; switch (network?.chainId) { + case 137: { + addresses = polygonAddresses; + break; + } + case 1101: { + addresses = polygonzkevmAddresses; + break; + } case 80001: { - await verify(mumbaiAddresses.versaSingleton, [mumbaiAddresses.entryPoint]); - await verify(mumbaiAddresses.versaAccountFactory, [ - mumbaiAddresses.versaSingleton, - mumbaiAddresses.compatibilityFallbackHandler, - ]); - await verify(mumbaiAddresses.versaVerifyingPaymaster, [ - mumbaiAddresses.entryPoint, - mumbaiAddresses.verifyingPaymasterOwner, - ]); - await verify(mumbaiAddresses.compatibilityFallbackHandler); - await verify(mumbaiAddresses.ecdsaValidator); - await verify(mumbaiAddresses.multisigValidator); - await verify(mumbaiAddresses.sessionKeyValidator); - await verify(mumbaiAddresses.spendingLimitHooks); + addresses = mumbaiAddresses; + break; + } + case 534352: { + addresses = scrollAddresses; break; } case 534351: { - await verify(scrollSepoliaAddresses.versaSingleton, [scrollSepoliaAddresses.entryPoint]); - await verify(scrollSepoliaAddresses.versaAccountFactory, [ - scrollSepoliaAddresses.versaSingleton, - scrollSepoliaAddresses.compatibilityFallbackHandler, - ]); - await verify(scrollSepoliaAddresses.versaVerifyingPaymaster, [ - scrollSepoliaAddresses.entryPoint, - scrollSepoliaAddresses.verifyingPaymasterOwner, - ]); - await verify(scrollSepoliaAddresses.compatibilityFallbackHandler); - await verify(scrollSepoliaAddresses.ecdsaValidator); - await verify(scrollSepoliaAddresses.multisigValidator); - await verify(scrollSepoliaAddresses.sessionKeyValidator); - await verify(scrollSepoliaAddresses.spendingLimitHooks); + addresses = scrollSepoliaAddresses; + break; + } + case 42161: { + addresses = arbitrumAddresses; + break; + } + case 421613: { + addresses = arbitrumGoerliAddresses; + break; + } + case 421614: { + addresses = arbitrumSepoliaAddresses; + break; + } + case 10: { + addresses = optimismAddresses; + break; + } + case 420: { + addresses = optimisticGoerliAddresses; + break; + } + case 11155420: { + addresses = optimisticSepoliaAddresses; + break; + } + case 8453: { + addresses = baseAddresses; + break; + } + case 84531: { + addresses = baseGoerliAddresses; + break; + } + case 84532: { + addresses = baseSepoliaAddresses; break; } default: { console.log("unsupported network"); } } + if (addresses) { + console.log("addresses", addresses); + await verify(addresses.versaSingleton, [deployConfig.entryPointAddress]); + await verify(addresses.versaAccountFactory, [ + addresses.versaSingleton, + addresses.compatibilityFallbackHandler, + deployConfig.entryPointAddress, + deployConfig.versaFactoryOwner, + ]); + // await verify(addresses.versaVerifyingPaymaster, [ + // deployConfig.entryPointAddress, + // deployConfig.verifyingPaymasterOwner, + // ]); + await verify(addresses.versaUniversalPaymaster, [ + deployConfig.entryPointAddress, + deployConfig.verifyingPaymasterOwner, + ]); + await verify(addresses.compatibilityFallbackHandler); + await verify(addresses.ecdsaValidator); + await verify(addresses.multisigValidator); + await verify(addresses.sessionKeyValidator); + await verify(addresses.spendingLimitHooks); + } } main() diff --git a/deploy/versaAccountFactory.ts b/deploy/versaAccountFactory.ts index 0050152..5902575 100644 --- a/deploy/versaAccountFactory.ts +++ b/deploy/versaAccountFactory.ts @@ -10,6 +10,8 @@ async function deployWithAddresses(addresses: any, config: any) { const versaAccountFactoryData: VersaAccountFactoryData = { versaSingleton: addresses.versaSingleton, defaultFallbackHandler: addresses.compatibilityFallbackHandler, + entryPoint: deployConfig.entryPointAddress, + owner: deployConfig.versaFactoryOwner, }; const versaAccountFactory = await deployer.deployVersaAccountFactory(versaAccountFactoryData, config.salt); addresses.versaAccountFactory = versaAccountFactory.address; diff --git a/deploy/versaSingleton.ts b/deploy/versaSingleton.ts index 12dafa5..45fedb7 100644 --- a/deploy/versaSingleton.ts +++ b/deploy/versaSingleton.ts @@ -6,7 +6,7 @@ import fs from "fs"; import { deployConfig } from "./helper/config"; async function deployWithAddresses(addresses: any, config: any) { - const versaSingleton = await deployer.deployVersaSingleton(config.entryPoint, config.salt); + const versaSingleton = await deployer.deployVersaSingleton(config.entryPointAddress, config.salt); addresses.versaSingleton = versaSingleton.address; return addresses; } diff --git a/hardhat.config.ts b/hardhat.config.ts index 13d1c8d..869ecf2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -4,10 +4,39 @@ import "solidity-coverage"; import "dotenv/config"; import "hardhat-contract-sizer"; +const ETH_RPC = process.env.ETH_RPC; + const POLYGON_MUMBAI_RPC = process.env.POLYGON_MUMBAI_RPC || "https://polygon-testnet.public.blastapi.io"; +const POLYGON_ZKEVM_TEST_RPC = + process.env.POLYGON_ZKEVM_TEST_RPC || "https://polygon-zkevm-testnet.blockpi.network/v1/rpc/public"; + const SCROLL_SEPOLIA_RPC = process.env.SCROLL_SEPOLIA_RPC || "https://scroll-sepolia.public.blastapi.io"; +const ARBITRUM_SEPOLIA_RPC = + process.env.ARBITRUM_SEPOLIA_RPC || "https://arbitrum-sepolia.blockpi.network/v1/rpc/public"; + +const OPTIMISM_SEPOLIA_RPC = + process.env.OPTIMISM_SEPOLIA_RPC || + "https://optimism-sepolia.blockpi.network/v1/rpc/76f6a1c5f8f95487af484be096ef3572cd7e14c7"; + +const BASE_SEPOLIA_RPC = + process.env.BASE_SEPOLIA_RPC || + "https://base-sepolia.blockpi.network/v1/rpc/377093fd4b4105db6441f07cf0f746991d5aeda3"; + +const ARBITRUM_GOERLI_RPC = process.env.ARBITRUM_GOERLI_RPC; + +const BASE_GOERLI_RPC = process.env.BASE_GOERLI_RPC; + +const OPTIMISM_GOERLI_RPC = process.env.OPTIMISM_GOERLI_RPC; + +const SCROLL_RPC = process.env.SCROLL_RPC; +const POLYGON_RPC = process.env.POLYGON_RPC; +const ARBITRUM_RPC = process.env.ARBITRUM_RPC; +const OPTIMISM_RPC = process.env.OPTIMISM_RPC; +const BASE_RPC = process.env.BASE_RPC; +const POLYGON_ZKEVM_RPC = process.env.POLYGON_ZKEVM_RPC; + const DEPLOYER_PRIVATE_KEY_1 = process.env.DEPLOYER_PRIVATE_KEY_1 || "0000000000000000000000000000000000000000000000000000000000000001"; @@ -20,10 +49,18 @@ const DEPLOYER_PRIVATE_KEY_3 = const DEPLOYER_PRIVATE_KEY_4 = process.env.DEPLOYER_PRIVATE_KEY_4 || "0000000000000000000000000000000000000000000000000000000000000004"; -const POLYGON_MUMBAI_SCAN_KEY = process.env.POLYGON_MUMBAI_SCAN_KEY; +const POLYGON_SCAN_KEY = process.env.POLYGON_SCAN_KEY; const SCROLL_SEPOLIA_SCAN_KEY = process.env.SCROLL_SEPOLIA_SCAN_KEY; +const ARBITRUM_SCAN_KEY = process.env.ARBITRUM_SCAN_API_KEY; + +const OPTIMISM_SCAN_KEY = process.env.OPTIMISM_SCAN_API_KEY; + +const BASR_SCAN_KEY = process.env.BASE_SCAN_API_KEY; + +const POLYGON_ZKEVM_SCAN_KEY = process.env.POLYGON_ZKEVM_SCAN_KEY; + const config: HardhatUserConfig = { solidity: { compilers: [ @@ -34,11 +71,81 @@ const config: HardhatUserConfig = { enabled: true, runs: 20000, }, + evmVersion: "london", }, }, ], }, networks: { + hardhat: { + forking: { + url: `${ETH_RPC}`, + blockNumber: 19083035, + }, + }, + polygon: { + url: `${POLYGON_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + polygonzkevm: { + url: `${POLYGON_ZKEVM_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + scroll: { + url: `${SCROLL_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + optimism: { + url: `${OPTIMISM_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + base: { + url: `${BASE_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + arbitrum: { + url: `${ARBITRUM_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + polygonzkevmTest: { + url: `${POLYGON_ZKEVM_TEST_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, polygonMumbai: { url: `${POLYGON_MUMBAI_RPC}`, accounts: [ @@ -57,13 +164,100 @@ const config: HardhatUserConfig = { `${DEPLOYER_PRIVATE_KEY_4}`, ], }, + optimismSepolia: { + url: `${OPTIMISM_SEPOLIA_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + baseSepolia: { + url: `${BASE_SEPOLIA_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + arbitrumSepolia: { + url: `${ARBITRUM_SEPOLIA_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + arbitrumGoerli: { + url: `${ARBITRUM_GOERLI_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + }, + baseGoerli: { + url: `${BASE_GOERLI_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + gasPrice: 1500000000, + }, + optimismGoerli: { + url: `${OPTIMISM_GOERLI_RPC}`, + accounts: [ + `${DEPLOYER_PRIVATE_KEY_1}`, + `${DEPLOYER_PRIVATE_KEY_2}`, + `${DEPLOYER_PRIVATE_KEY_3}`, + `${DEPLOYER_PRIVATE_KEY_4}`, + ], + gasPrice: 1500000000, + }, + }, + gasReporter: { + enabled: false, }, etherscan: { apiKey: { - polygonMumbai: `${POLYGON_MUMBAI_SCAN_KEY}`, + polygonMumbai: `${POLYGON_SCAN_KEY}`, scrollSepolia: `${SCROLL_SEPOLIA_SCAN_KEY}`, + scroll: `${SCROLL_SEPOLIA_SCAN_KEY}`, + arbitrumSepolia: `${ARBITRUM_SCAN_KEY}`, + optimismSepolia: `${OPTIMISM_SCAN_KEY}`, + optimisticEthereum: `${OPTIMISM_SCAN_KEY}`, + baseSepolia: `${BASR_SCAN_KEY}`, + base: `${BASR_SCAN_KEY}`, + arbitrumGoerli: `${ARBITRUM_SCAN_KEY}`, + optimisticGoerli: `${OPTIMISM_SCAN_KEY}`, + baseGoerli: `${BASR_SCAN_KEY}`, + arbitrumOne: `${ARBITRUM_SCAN_KEY}`, + polygon: `${POLYGON_SCAN_KEY}`, + polygonzkevm: `${POLYGON_ZKEVM_SCAN_KEY}`, }, customChains: [ + { + network: "polygonzkevm", + chainId: 1101, + urls: { + apiURL: "https://api-zkevm.polygonscan.com/api", + browserURL: "https://zkevm.polygonscan.com", + }, + }, + { + network: "scroll", + chainId: 534352, + urls: { + apiURL: "https://api.scrollscan.com/api", + browserURL: "https://scrollscan.com", + }, + }, { network: "scrollSepolia", chainId: 534351, @@ -72,6 +266,46 @@ const config: HardhatUserConfig = { browserURL: "https://sepolia.scrollscan.dev", }, }, + { + network: "arbitrumSepolia", + chainId: 421614, + urls: { + apiURL: "https://api-sepolia.arbiscan.io/api", + browserURL: "https://sepolia.arbiscan.io", + }, + }, + { + network: "optimismSepolia", + chainId: 11155420, + urls: { + apiURL: "https://api-sepolia.optimistic.etherscan.io", + browserURL: "https://sepolia-optimism.etherscan.io/", + }, + }, + { + network: "base", + chainId: 8453, + urls: { + apiURL: "https://api.basescan.org/api", + browserURL: "https://basescan.org/", + }, + }, + { + network: "baseSepolia", + chainId: 84532, + urls: { + apiURL: "https://base-sepolia.blockscout.com/api", + browserURL: "https://base-sepolia.blockscout.com/", + }, + }, + { + network: "baseGoerli", + chainId: 84531, + urls: { + apiURL: "https://api-goerli.basescan.org/api", + browserURL: "https://goerli.basescan.org/", + }, + }, ], }, }; diff --git a/test/VersaFactory.test.ts b/test/VersaFactory.test.ts index 94624d9..e8134e4 100644 --- a/test/VersaFactory.test.ts +++ b/test/VersaFactory.test.ts @@ -36,7 +36,9 @@ describe("VersaFactory", () => { // Deploy VersaAccountFactory versaFactory = await new VersaAccountFactory__factory(owner).deploy( versaWalletSingleton.address, - fallbackHandler + fallbackHandler, + entryPoint, + owner.address ); validator = await new MockValidator__factory(owner).deploy(); diff --git a/test/VersaWallet.test.ts b/test/VersaWallet.test.ts index d3c4062..58af773 100644 --- a/test/VersaWallet.test.ts +++ b/test/VersaWallet.test.ts @@ -45,7 +45,9 @@ describe("VersaWallet", () => { // Deploy VersaAccountFactory versaFactory = await new VersaAccountFactory__factory(owner).deploy( versaWalletSingleton.address, - fallbackHandler.address + fallbackHandler.address, + entryPoint.address, + owner.address ); sudoValidator = await new MockValidator__factory(owner).deploy(); diff --git a/test/paymaster/VersaUniversalPaymaster.test.ts b/test/paymaster/VersaUniversalPaymaster.test.ts new file mode 100644 index 0000000..67b787b --- /dev/null +++ b/test/paymaster/VersaUniversalPaymaster.test.ts @@ -0,0 +1,246 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { VersaUniversalPaymaster__factory, VersaUniversalPaymaster, IERC20 } from "../../typechain-types"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber } from "ethers"; + +describe("VersaUniversaPaymaster", () => { + let versaUniversaPaymaster: VersaUniversalPaymaster; + let owner: SignerWithAddress; + let operator: SignerWithAddress; + let user: SignerWithAddress; + + let entryPointAddress = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; + + let v2SwapRouter = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; + let v3SwapRouter = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; + let weth = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; + + let usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + let usdtAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; + + let usdcWhaleAddress = "0xF977814e90dA44bFA03b6295A0616a897441aceC"; + let usdtWhaleAddress = "0xF977814e90dA44bFA03b6295A0616a897441aceC"; + + let usdc: IERC20; + let usdt: IERC20; + + let abiCoder = new ethers.utils.AbiCoder(); + + function numberToFixedHex(value: number, length: number): string { + return "0x" + value.toString(16).padStart(length * 2, "0"); + } + + async function deployfixture() { + [owner, operator, user] = await ethers.getSigners(); + versaUniversaPaymaster = await new VersaUniversalPaymaster__factory(owner).deploy( + entryPointAddress, + owner.address, + operator.address, + v2SwapRouter, + v3SwapRouter, + weth + ); + usdc = await ethers.getContractAt("IERC20", usdcAddress); + usdt = await ethers.getContractAt("IERC20", usdtAddress); + + let usdcWhale = await ethers.getImpersonatedSigner(usdcWhaleAddress); + let usdtWhale = await ethers.getImpersonatedSigner(usdtWhaleAddress); + + usdc.connect(usdcWhale).transfer(versaUniversaPaymaster.address, ethers.utils.parseUnits("10000", 6)); + usdt.connect(usdtWhale).transfer(versaUniversaPaymaster.address, ethers.utils.parseUnits("10000", 6)); + + return { owner, operator, user, versaUniversaPaymaster, usdc, usdt }; + } + + describe("roles", () => { + beforeEach(async function () { + let fixture = await helpers.loadFixture(deployfixture); + owner = fixture.owner; + operator = fixture.operator; + user = fixture.user; + versaUniversaPaymaster = fixture.versaUniversaPaymaster; + usdc = fixture.usdc; + usdt = fixture.usdt; + }); + + it("only owner", async () => { + await expect(versaUniversaPaymaster.connect(user).setOperator(user.address)).to.be.revertedWith( + "Ownable: caller is not the owner" + ); + + await expect( + versaUniversaPaymaster + .connect(user) + .setSwapRouter(ethers.constants.AddressZero, ethers.constants.AddressZero) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("only operator", async () => { + await expect( + versaUniversaPaymaster + .connect(user) + .approveRouter( + [usdcAddress, usdtAddress], + [ethers.constants.MaxUint256, ethers.constants.MaxUint256] + ) + ).to.be.revertedWith("VersaUniversaPaymaster: Only operator"); + + const v2SwapParas_1 = { + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMin: 100, + path: [usdcAddress, weth], + }; + + await expect( + versaUniversaPaymaster.connect(user).convertTokensAndDeposit([v2SwapParas_1], []) + ).to.be.revertedWith("VersaUniversaPaymaster: Only operator"); + }); + }); + + describe("swap handler", () => { + beforeEach(async function () { + let fixture = await helpers.loadFixture(deployfixture); + owner = fixture.owner; + operator = fixture.operator; + versaUniversaPaymaster = fixture.versaUniversaPaymaster; + usdc = fixture.usdc; + usdt = fixture.usdt; + + await versaUniversaPaymaster + .connect(operator) + .approveRouter([usdcAddress, usdtAddress], [ethers.constants.MaxUint256, ethers.constants.MaxUint256]); + }); + + it("should approve router", async () => { + expect(await usdc.allowance(versaUniversaPaymaster.address, v2SwapRouter)).to.be.equal( + ethers.constants.MaxUint256 + ); + expect(await usdt.allowance(versaUniversaPaymaster.address, v2SwapRouter)).to.be.equal( + ethers.constants.MaxUint256 + ); + }); + + it("should convert through uniswapV2", async () => { + const v2SwapParas_1 = { + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMin: 100, + path: [usdcAddress, weth], + }; + + const v2SwapParas_2 = { + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMin: 100, + path: [usdtAddress, weth], + }; + const deposited = await versaUniversaPaymaster + .connect(operator) + .callStatic.convertTokensAndDeposit([v2SwapParas_1, v2SwapParas_2], []); + + expect(deposited).to.greaterThan(BigNumber.from(0)); + }); + + it("should convert through uniswapV3", async () => { + const v3SwapParas_1 = { + path: ethers.utils.hexConcat([usdcAddress, numberToFixedHex(3000, 3), weth]), + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMinimum: 100, + }; + + const v3SwapParas_2 = { + path: ethers.utils.hexConcat([usdtAddress, numberToFixedHex(3000, 3), weth]), + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMinimum: 100, + }; + + const deposited = await versaUniversaPaymaster + .connect(operator) + .callStatic.convertTokensAndDeposit([], [v3SwapParas_1, v3SwapParas_2]); + + expect(deposited).to.greaterThan(BigNumber.from(0)); + }); + + it("should convert through v2 and v3 and deposited to entrypoint", async () => { + const v2SwapParas_1 = { + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMin: 100, + path: [usdcAddress, weth], + }; + + const v2SwapParas_2 = { + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMin: 100, + path: [usdtAddress, weth], + }; + + const v3SwapParas_1 = { + path: ethers.utils.hexConcat([usdcAddress, numberToFixedHex(3000, 3), weth]), + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMinimum: 100, + }; + + const v3SwapParas_2 = { + path: ethers.utils.hexConcat([usdtAddress, numberToFixedHex(3000, 3), weth]), + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMinimum: 100, + }; + + const deposited = await versaUniversaPaymaster + .connect(operator) + .callStatic.convertTokensAndDeposit([v2SwapParas_1, v2SwapParas_2], [v3SwapParas_1, v3SwapParas_2]); + + await versaUniversaPaymaster + .connect(operator) + .convertTokensAndDeposit([v2SwapParas_1, v2SwapParas_2], [v3SwapParas_1, v3SwapParas_2]); + + expect(deposited).to.greaterThan(BigNumber.from(0)); + expect(deposited).to.be.equal(await versaUniversaPaymaster.getDeposit()); + }); + + it("amountIn can be type(uint256).max", async () => { + const v2SwapParas_1 = { + amountIn: ethers.constants.MaxUint256, + amountOutMin: 100, + path: [usdcAddress, weth], + }; + + const v3SwapParas_1 = { + path: ethers.utils.hexConcat([usdtAddress, numberToFixedHex(3000, 3), weth]), + amountIn: ethers.constants.MaxUint256, + amountOutMinimum: 100, + }; + + const deposited = await versaUniversaPaymaster + .connect(operator) + .callStatic.convertTokensAndDeposit([v2SwapParas_1], [v3SwapParas_1]); + + await versaUniversaPaymaster.connect(operator).convertTokensAndDeposit([v2SwapParas_1], [v3SwapParas_1]); + + expect(deposited).to.greaterThan(BigNumber.from(0)); + expect(deposited).to.be.equal(await versaUniversaPaymaster.getDeposit()); + }); + + it("should only convert to weth", async () => { + const v2SwapParas_1 = { + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMin: 100, + path: [usdtAddress, usdcAddress], + }; + + await expect( + versaUniversaPaymaster.connect(operator).convertTokensAndDeposit([v2SwapParas_1], []) + ).to.be.revertedWith("UniswapV2Router: INVALID_PATH"); + + const v3SwapParas_1 = { + path: ethers.utils.hexConcat([usdcAddress, numberToFixedHex(3000, 3), usdtAddress]), + amountIn: ethers.utils.parseUnits("1000", 6), + amountOutMinimum: 100, + }; + + await expect( + versaUniversaPaymaster.connect(operator).convertTokensAndDeposit([], [v3SwapParas_1]) + ).to.be.revertedWith("TokenSwapHandler: Only to wnative token allowed"); + }); + }); +}); diff --git a/test/utils.ts b/test/utils.ts index 5c2574e..b6754f6 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -33,7 +33,9 @@ export async function deployVersaWallet(options: { // Deploy VersaAccountFactory let versaFactory = await new VersaAccountFactory__factory(signer).deploy( versaWalletSingleton.address, - fallbackHandler.address + fallbackHandler.address, + entryPoint, + signer.address ); sudoValidatorAddr = diff --git a/test/validator/MultisigValidator.test.ts b/test/validator/MultisigValidator.test.ts index ea67d30..52ddb24 100644 --- a/test/validator/MultisigValidator.test.ts +++ b/test/validator/MultisigValidator.test.ts @@ -12,8 +12,8 @@ import { import { deployVersaWallet, getScheduledUserOpHash, getUserOpHash, entryPointAddress } from "../utils"; import { enablePlugin, execute } from "../base/utils"; import { arrayify, hexConcat, hexlify, keccak256, toUtf8Bytes } from "ethers/lib/utils"; -import * as helper from "@nomicfoundation/hardhat-network-helpers"; import { numberToFixedHex } from "../base/utils"; +import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; describe("MultiSigValidator", () => { let multisigValidator: MultiSigValidator; @@ -554,6 +554,143 @@ describe("MultiSigValidator", () => { expect(validationData).to.equal(expectedValidationData); }); + it("should validate multi-chain userOp signature correctly", async () => { + let sudoValidator = multisigValidator; + let threshold = 2; + let initData = abiCoder.encode(["address[]", "uint256"], [[signer1.address, signer2.address], threshold]); + await enablePlugin({ + executor: wallet, + plugin: sudoValidator.address, + initData, + selector: "enableValidator", + }); + + let op = { + sender: wallet.address, + nonce: 2, + initCode: "0x", + callData: "0x", + callGasLimit: 2150000, + verificationGasLimit: 2150000, + preVerificationGas: 2150000, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + paymasterAndData: "0x", + signature: "0x", + }; + + function getMultiChainUserOpLeaves(n: number) { + let leaves = []; + for (let i = 0; i < n; i++) { + let data = getUserOpHash(op, entryPointAddress, i); + leaves.push([data]); + } + return leaves; + } + + let userOpHash1 = getUserOpHash(op, entryPointAddress, 0); + let userOpHash2 = getUserOpHash(op, entryPointAddress, 999); + + let wrongUserOp = getUserOpHash(op, entryPointAddress, 1000); + + let opsTree = StandardMerkleTree.of(getMultiChainUserOpLeaves(1000), ["bytes32"]); + let root = opsTree.root; + let proof = opsTree.getProof([userOpHash1]); + + let validAfter = Math.floor(Date.now() / 1000); + let validUntil = validAfter + 3600; + + let extraData = abiCoder.encode(["uint256", "uint256"], [validUntil, validAfter]); + + // This remains unchanged for multichain userOps + let finalHash = keccak256( + abiCoder.encode(["bytes32", "address", "bytes"], [root, multisigValidator.address, extraData]) + ); + + let userOpSigs = "0x"; + + let signers = [signer1, signer2]; + + signers.sort((a, b) => { + let addressA = a.address.toLocaleLowerCase(); + let addressB = b.address.toLocaleLowerCase(); + if (addressA < addressB) { + return -1; + } else if (addressA == addressB) { + return 0; + } else { + return 1; + } + }); + + const promises = signers.map(async (signer) => { + const signature = await signer.signMessage(arrayify(finalHash)); + userOpSigs = hexConcat([userOpSigs, signature]); + }); + await Promise.all(promises); + + let combinedProof = "0x"; + for (let i = 0; i < proof.length; i++) { + combinedProof = hexConcat([combinedProof, proof[i]]); + } + + // +---------------------------+---------------+---------------+-----------+-----------+----------------------------+ + // | multi-chain tx sig (0x02) | validatorAddr | signatureType | timeRange |merkle root|proof len | proof |signature| + // | | 20 bytes | 1 byte | 12 bytes | 32 bytes | 32 bytes |m bytes| n bytes | + // +---------------------------+---------------+---------------+-----------+-----------+----------+-----------------+ + + let sign = hexConcat([ + multisigValidator.address, + "0x02", + numberToFixedHex(validUntil, 6), + numberToFixedHex(validAfter, 6), + root, + numberToFixedHex(proof.length, 32), + combinedProof, + userOpSigs, + ]); + + op.signature = sign; + + const validationData = await multisigValidator.validateSignature(op, userOpHash1); + const expectedValidationData = hexConcat([ + numberToFixedHex(validAfter, 6), + numberToFixedHex(validUntil, 6), + numberToFixedHex(0, 20), + ]); + expect(validationData).to.equal(expectedValidationData); + + let proof2 = opsTree.getProof([userOpHash2]); + let combinedProof2 = "0x"; + for (let i = 0; i < proof2.length; i++) { + combinedProof2 = hexConcat([combinedProof2, proof2[i]]); + } + let sign2 = hexConcat([ + multisigValidator.address, + "0x02", + numberToFixedHex(validUntil, 6), + numberToFixedHex(validAfter, 6), + root, + numberToFixedHex(proof2.length, 32), + combinedProof2, + userOpSigs, + ]); + op.signature = sign2; + + const validationData2 = await multisigValidator.validateSignature(op, userOpHash2); + const expectedValidationData2 = hexConcat([ + numberToFixedHex(validAfter, 6), + numberToFixedHex(validUntil, 6), + numberToFixedHex(0, 20), + ]); + expect(validationData2).to.equal(expectedValidationData2); + + // should reject wrong userOp + await expect(multisigValidator.validateSignature(op, wrongUserOp)).to.be.revertedWith( + "SignatureHandler: UserOp is not in the merkle tree" + ); + }); + it("should validate userOp signature correctly", async () => { let sudoValidator = multisigValidator; let threshold = 2;