diff --git a/.env.example b/.env.example index 3304e04..961ea5e 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,10 @@ -OWNER="0x490b97230d82c22b563c3f322470f643b305884e" -VERSION_SALT="0x004" +OWNER="" +VERSION_SALT="0x0004" PRIVATE_KEY="" ENTRYPOINT="0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" -RPC_80001="https://rpc.ankr.com/polygon_mumbai" -ETHERSCAN_URL_80001="https://api-testnet.polygonscan.com/api" -ETHERSCAN_KEY_80001="" +RPC_1442="https://rpc.public.zkevm-test.net" RPC_84531="https://goerli.base.org" -ETHERSCAN_URL_84531="https://api-goerli.basescan.org/api" -ETHERSCAN_KEY_84531="" RPC_420="https://goerli.optimism.io" -ETHERSCAN_URL_420="" -ETHERSCAN_KEY_420="" -RPC_421613="https://arbitrum-goerli.public.blastapi.io" -ETHERSCAN_URL_421613="" -ETHERSCAN_KEY_421613="" -RPC_5="https://rpc.ankr.com/eth_goerli" -ETHERSCAN_URL_5="" -ETHERSCAN_KEY_5="" -RPC_59140="https://rpc.goerli.linea.build" -ETHERSCAN_URL_59140="" -ETHERSCAN_KEY_59140="" +RPC_421613="https://rpc.goerli.arbitrum.gateway.fm" +RPC_5="https://eth-goerli.public.blastapi.io" FEE_MANAGER="0x687f79f1bbc3591c541b25d3ce3a642e3c822909" \ No newline at end of file diff --git a/readme.md b/readme.md index 593c5c9..7c2c9e8 100644 --- a/readme.md +++ b/readme.md @@ -4,11 +4,11 @@ ### PermissiveFactory -`0x825762ccb52fefc37f6d702d83d42f0c571a3836` +`0xcd8d5f03e1f4c2d519a09bf171400b6125c66b7f` ### PermissiveAccount -`0xf0afdb27fc4579d1de23170c265a579c38f1c7c4` +`0x5552cb883b234173670130ff72985290be64afd4` ### FeeManager diff --git a/scripts/Deploy.s.sol b/scripts/Deploy.s.sol index 37de471..b2cc7c5 100644 --- a/scripts/Deploy.s.sol +++ b/scripts/Deploy.s.sol @@ -13,14 +13,16 @@ contract DeployScript is Script { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address entrypoint = vm.envAddress("ENTRYPOINT"); bytes32 versionSalt = vm.envBytes32("VERSION_SALT"); + address feeManager = vm.envAddress("FEE_MANAGER"); vm.startBroadcast(deployerPrivateKey); - FeeManager feeManager = new FeeManager{salt: versionSalt}(); - feeManager.initialize(vm.envAddress("OWNER")); PermissiveAccount impl = new PermissiveAccount{salt: versionSalt}( entrypoint, payable(address(feeManager)) ); - new PermissiveFactory{salt: versionSalt}(address(impl)); + PermissiveFactory factory = new PermissiveFactory{salt: versionSalt}( + address(impl) + ); + factory.initialize(vm.addr(deployerPrivateKey)); vm.stopBroadcast(); } } diff --git a/scripts/Upgrade.s.sol b/scripts/Upgrade.s.sol new file mode 100644 index 0000000..ebb98f6 --- /dev/null +++ b/scripts/Upgrade.s.sol @@ -0,0 +1,26 @@ +// SPDX-License-Indetifier: SEE LICENSE IN LICENSE + +pragma solidity ^0.8.18; + +import "forge-std/Script.sol"; +import "../src/core/PermissiveFactory.sol"; +import "../src/core/PermissiveAccount.sol"; + +contract UpgradeScript is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address entrypoint = vm.envAddress("ENTRYPOINT"); + bytes32 versionSalt = vm.envBytes32("VERSION_SALT"); + address feeManager = vm.envAddress("FEE_MANAGER"); + vm.startBroadcast(deployerPrivateKey); + PermissiveAccount impl = new PermissiveAccount{salt: versionSalt}( + entrypoint, + payable(address(feeManager)) + ); + PermissiveFactory(0x03Fc43A9124813720A14889bE92eE44A73c7b3ec).upgradeTo( + address(impl) + ); + vm.addr(deployerPrivateKey); + vm.stopBroadcast(); + } +} diff --git a/scripts/deploy_all.sh b/scripts/deploy_all.sh index 60a0d84..6b80d62 100755 --- a/scripts/deploy_all.sh +++ b/scripts/deploy_all.sh @@ -1,10 +1,10 @@ -# polygon zkEVM -forge script --rpc-url $RPC_1442 --broadcast --sender $OWNER ./scripts/Deploy.s.sol -# base goerli -forge script --rpc-url $RPC_84531 --broadcast --sender $OWNER ./scripts/Deploy.s.sol -# optimism goerli -forge script --rpc-url $RPC_420 --broadcast --sender $OWNER ./scripts/Deploy.s.sol +# # polygon zkEVM +# forge script --rpc-url $RPC_1442 --broadcast --sender $OWNER ./scripts/Deploy.s.sol +# # base goerli +# forge script --rpc-url $RPC_84531 --broadcast --sender $OWNER ./scripts/Deploy.s.sol +# # optimism goerli +# forge script --rpc-url $RPC_420 --broadcast --sender $OWNER ./scripts/Deploy.s.sol # arbitrum goerli forge script --rpc-url $RPC_421613 --broadcast --sender $OWNER ./scripts/Deploy.s.sol -# goerli -forge script --rpc-url $RPC_5 --broadcast --sender $OWNER ./scripts/Deploy.s.sol \ No newline at end of file +# # goerli +# forge script --rpc-url $RPC_5 --broadcast --sender $OWNER ./scripts/Deploy.s.sol \ No newline at end of file diff --git a/src/core/AllowanceCalldata.sol b/src/core/AllowanceCalldata.sol index d2ac20d..c704585 100644 --- a/src/core/AllowanceCalldata.sol +++ b/src/core/AllowanceCalldata.sol @@ -13,11 +13,10 @@ uint256 constant AND = 5; uint256 constant OR = 6; library AllowanceCalldata { - function sliceRLPItems(RLPReader.RLPItem[] memory arguments, uint256 start) - public - pure - returns (RLPReader.RLPItem[] memory newArguments) - { + function sliceRLPItems( + RLPReader.RLPItem[] memory arguments, + uint256 start + ) public pure returns (RLPReader.RLPItem[] memory newArguments) { uint256 length = arguments.length - start; assembly { newArguments := mload(0x40) @@ -25,7 +24,11 @@ library AllowanceCalldata { mstore(newArguments, length) let hit := mul(add(length, 1), 0x20) let memStart := add(arguments, mul(start, 0x20)) - for { let i := 0x20 } lt(i, hit) { i := add(i, 0x20) } { + for { + let i := 0x20 + } lt(i, hit) { + i := add(i, 0x20) + } { mstore(add(newArguments, i), mload(add(memStart, i))) } mstore(0x40, add(mload(0x40), mul(length, 0x20))) @@ -39,11 +42,17 @@ library AllowanceCalldata { ) public view returns (bool canPass) { if (allowedArguments.length == 0) return true; for (uint256 i = 0; i < allowedArguments.length; i = unsafe_inc(i)) { - RLPReader.RLPItem[] memory prefixAndArg = RLPReader.toList(allowedArguments[i]); + RLPReader.RLPItem[] memory prefixAndArg = RLPReader.toList( + allowedArguments[i] + ); uint256 prefix = RLPReader.toUint(prefixAndArg[0]); - if (prefix == ANY) {} else if (prefix == EQ) { - bytes memory allowedArgument = RLPReader.toBytes(prefixAndArg[1]); + if (prefix == ANY) { + canPass = true; + } else if (prefix == EQ) { + bytes memory allowedArgument = RLPReader.toBytes( + prefixAndArg[1] + ); bytes memory argument = RLPReader.toBytes(arguments[i]); canPass = keccak256(allowedArgument) == keccak256(argument); } else if (prefix == LT) { @@ -55,16 +64,30 @@ library AllowanceCalldata { uint256 argument = RLPReader.toUint(arguments[i]); canPass = argument > allowedArgument; } else if (prefix == OR) { - RLPReader.RLPItem[] memory subAllowance = RLPReader.toList(prefixAndArg[1]); - canPass = validateArguments(subAllowance, sliceRLPItems(arguments, i), true); + RLPReader.RLPItem[] memory subAllowance = RLPReader.toList( + prefixAndArg[1] + ); + canPass = validateArguments( + subAllowance, + sliceRLPItems(arguments, i), + true + ); i = unsafe_inc(i); } else if (prefix == NE) { - bytes memory allowedArgument = RLPReader.toBytes(prefixAndArg[1]); + bytes memory allowedArgument = RLPReader.toBytes( + prefixAndArg[1] + ); bytes memory argument = RLPReader.toBytes(arguments[i]); canPass = keccak256(allowedArgument) != keccak256(argument); } else if (prefix == AND) { - RLPReader.RLPItem[] memory subAllowance = RLPReader.toList(prefixAndArg[1]); - canPass = validateArguments(subAllowance, sliceRLPItems(arguments, i), false); + RLPReader.RLPItem[] memory subAllowance = RLPReader.toList( + prefixAndArg[1] + ); + canPass = validateArguments( + subAllowance, + sliceRLPItems(arguments, i), + false + ); i = unsafe_inc(i); } else { revert("Invalid calldata prefix"); @@ -76,13 +99,15 @@ library AllowanceCalldata { return canPass; } - function isAllowedCalldata(bytes memory allowed, bytes memory data, uint256 value) - internal - view - returns (bool isOk) - { + function isAllowedCalldata( + bytes memory allowed, + bytes memory data, + uint256 value + ) internal view returns (bool isOk) { RLPReader.RLPItem memory RLPAllowed = RLPReader.toRlpItem(allowed); - RLPReader.RLPItem[] memory allowedArguments = RLPReader.toList(RLPAllowed); + RLPReader.RLPItem[] memory allowedArguments = RLPReader.toList( + RLPAllowed + ); RLPReader.RLPItem memory RLPData = RLPReader.toRlpItem(data); RLPReader.RLPItem[] memory arguments = RLPReader.toList(RLPData); if (allowedArguments.length != arguments.length) { @@ -94,11 +119,16 @@ library AllowanceCalldata { isOk = validateArguments(allowedArguments, arguments, false); } - function RLPtoABI(bytes memory data) internal pure returns (bytes memory abiEncoded) { + function RLPtoABI( + bytes memory data + ) internal pure returns (bytes memory abiEncoded) { RLPReader.RLPItem memory RLPData = RLPReader.toRlpItem(data); RLPReader.RLPItem[] memory arguments = RLPReader.toList(RLPData); for (uint256 i = 1; i < arguments.length; i = unsafe_inc(i)) { - abiEncoded = bytes.concat(abiEncoded, RLPReader.toBytes(arguments[i])); + abiEncoded = bytes.concat( + abiEncoded, + RLPReader.toBytes(arguments[i]) + ); } } diff --git a/src/core/PermissiveAccount.sol b/src/core/PermissiveAccount.sol index 1d8489c..2d42e72 100644 --- a/src/core/PermissiveAccount.sol +++ b/src/core/PermissiveAccount.sol @@ -14,6 +14,7 @@ import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "./AllowanceCalldata.sol"; import "bytes/BytesLib.sol"; import "./FeeManager.sol"; +import "forge-std/console.sol"; import "../interfaces/IDataValidator.sol"; // keccak256("PermissionSet(address operator,bytes32 merkleRootPermissions)") @@ -26,11 +27,15 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { mapping(address => bytes32) public operatorPermissions; mapping(bytes32 => uint256) public remainingPermUsage; + mapping(bytes32 => bool) public signed; IEntryPoint private immutable _entryPoint; FeeManager private immutable feeManager; bool private _initialized; - constructor(address __entryPoint, address payable _feeManager) EIP712("Permissive Account", "v0.0.4") { + constructor( + address __entryPoint, + address payable _feeManager + ) EIP712("Permissive Account", "v0.0.4") { _entryPoint = IEntryPoint(__entryPoint); feeManager = FeeManager(_feeManager); } @@ -49,17 +54,35 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { _transferOwnership(owner); } - function setOperatorPermissions(PermissionSet calldata permSet, bytes calldata signature) external { - bytes32 digest = - _hashTypedDataV4(keccak256(abi.encode(typedStruct, permSet.operator, permSet.merkleRootPermissions))); + function setOperatorPermissions( + PermissionSet calldata permSet, + bytes calldata signature + ) external { + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + typedStruct, + permSet.operator, + permSet.merkleRootPermissions + ) + ) + ); address signer = ECDSA.recover(digest, signature); if (signer != owner()) revert("Not Allowed"); bytes32 oldValue = operatorPermissions[permSet.operator]; operatorPermissions[permSet.operator] = permSet.merkleRootPermissions; - emit OperatorMutated(permSet.operator, oldValue, permSet.merkleRootPermissions); + emit OperatorMutated( + permSet.operator, + oldValue, + permSet.merkleRootPermissions + ); } - function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) + function validateUserOp( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external override(BaseAccount, IAccount) returns (uint256 validationData) @@ -67,31 +90,64 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { _requireFromEntryPoint(); bytes32 hash = userOpHash.toEthSignedMessageHash(); if (owner() != hash.recover(userOp.signature)) { - (,,, PermissionLib.Permission memory permission, bytes32[] memory proof, uint256 providedFee) = - abi.decode(userOp.callData[4:], (address, uint256, bytes, PermissionLib.Permission, bytes32[], uint256)); + ( + , + , + , + PermissionLib.Permission memory permission, + bytes32[] memory proof, + uint256 providedFee + ) = abi.decode( + userOp.callData[4:], + ( + address, + uint256, + bytes, + PermissionLib.Permission, + bytes32[], + uint256 + ) + ); if (permission.operator.code.length > 0) { - try IERC1271(permission.operator).isValidSignature(hash, userOp.signature) returns (bytes4 magicValue) { + try + IERC1271(permission.operator).isValidSignature( + hash, + userOp.signature + ) + returns (bytes4 magicValue) { validationData = _packValidationData( ValidationData( - magicValue == IERC1271.isValidSignature.selector ? address(0) : address(1), + magicValue == IERC1271.isValidSignature.selector + ? address(0) + : address(1), permission.validAfter, permission.validUntil ) ); } catch { - validationData = - _packValidationData(ValidationData(address(1), permission.validAfter, permission.validUntil)); + validationData = _packValidationData( + ValidationData( + address(1), + permission.validAfter, + permission.validUntil + ) + ); } } else if (permission.operator != hash.recover(userOp.signature)) { return SIG_VALIDATION_FAILED; } else { - validationData = - _packValidationData(ValidationData(address(0), permission.validAfter, permission.validUntil)); + validationData = _packValidationData( + ValidationData( + address(0), + permission.validAfter, + permission.validUntil + ) + ); } bytes32 permHash = permission.hash(); _validateMerklePermission(permission, proof, permHash); _validatePermission(userOp, permission, permHash); - _validateData(permission); + _validateData(userOp, userOpHash, missingAccountFunds, permission); uint256 gasFee = computeGasFee(userOp); if (providedFee != gasFee) revert("Invalid provided fee"); } @@ -109,11 +165,23 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { uint256 gasFee ) external { _requireFromEntryPointOrOwner(); - payable(address(feeManager)).transfer((gasFee * feeManager.fee()) / 10000); + payable(address(feeManager)).transfer( + (gasFee * feeManager.fee()) / 10000 + ); (bool success, bytes memory result) = dest.call{value: value}( - bytes.concat(func.slice(0, 4), AllowanceCalldata.RLPtoABI(func.slice(4, func.length - 4))) + bytes.concat( + func.slice(0, 4), + AllowanceCalldata.RLPtoABI(func.slice(4, func.length - 4)) + ) + ); + emit PermissionUsed( + permission.hash(), + dest, + value, + func, + permission, + gasFee ); - emit PermissionUsed(permission.hash(), dest, value, func, permission, gasFee); if (!success) { assembly { revert(add(result, 32), mload(result)) @@ -121,27 +189,57 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { } } - function computeGasFee(UserOperation memory userOp) public pure returns (uint256 fee) { + function computeGasFee( + UserOperation memory userOp + ) public pure returns (uint256 fee) { unchecked { - uint256 mul = address(bytes20(userOp.paymasterAndData)) != address(0) ? 3 : 1; - uint256 requiredGas = userOp.callGasLimit + userOp.verificationGasLimit * mul + userOp.preVerificationGas; + uint256 mul = address(bytes20(userOp.paymasterAndData)) != + address(0) + ? 3 + : 1; + uint256 requiredGas = userOp.callGasLimit + + userOp.verificationGasLimit * + mul + + userOp.preVerificationGas; fee = requiredGas * userOp.maxFeePerGas; } } + // @dev structData = typeHash || encodedData + function sign712( + bytes32 domainSeparator, + bytes calldata structData + ) external { + require(msg.sender == address(this)); + bytes32 hash = ECDSA.toTypedDataHash( + domainSeparator, + keccak256(structData) + ); + signed[hash] = true; + } + /* INTERNAL */ - function _hashTypedDataV4(bytes32 structHash) internal view override returns (bytes32) { + function _hashTypedDataV4( + bytes32 structHash + ) internal view override returns (bytes32) { return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); } - function _validateData(PermissionLib.Permission memory permission) internal view { + function _validateData( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds, + PermissionLib.Permission memory permission + ) internal { if ( - permission.dataValidation.validator != address(0) - && !IDataValidator(permission.dataValidation.validator).isValidData( - permission.dataValidation.target, permission.dataValidation.data - ) + permission.dataValidator != address(0) && + !IDataValidator(permission.dataValidator).isValidData( + userOp, + userOpHash, + missingAccountFunds + ) ) { revert("Invalid data"); } @@ -152,8 +250,10 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { PermissionLib.Permission memory permission, bytes32 permHash ) internal { - (address to, uint256 value, bytes memory callData,,) = - abi.decode(userOp.callData[4:], (address, uint256, bytes, PermissionLib.Permission, bytes32[])); + (address to, uint256 value, bytes memory callData, , ) = abi.decode( + userOp.callData[4:], + (address, uint256, bytes, PermissionLib.Permission, bytes32[]) + ); if (permission.to != to) revert("InvalidTo"); uint256 rPermU = remainingPermUsage[permHash]; if (permission.maxUsage > 0) { @@ -169,7 +269,9 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { } if ( !AllowanceCalldata.isAllowedCalldata( - permission.allowed_arguments, callData.slice(4, callData.length - 4), value + permission.allowed_arguments, + callData.slice(4, callData.length - 4), + value ) ) revert("Not allowed Calldata"); if (permission.selector != bytes4(callData)) revert("InvalidSelector"); @@ -188,21 +290,25 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { bytes32[] memory proof, bytes32 permHash ) internal view { - bool isValidProof = - MerkleProof.verify(proof, operatorPermissions[permission.operator], keccak256(bytes.concat(permHash))); + bool isValidProof = MerkleProof.verify( + proof, + operatorPermissions[permission.operator], + keccak256(bytes.concat(permHash)) + ); if (!isValidProof) revert("Invalid Proof"); } function _requireFromEntryPointOrOwner() internal view { - require(msg.sender == address(entryPoint()) || msg.sender == owner(), "account: not from EntryPoint or owner"); + require( + msg.sender == address(entryPoint()) || msg.sender == owner(), + "account: not from EntryPoint or owner" + ); } - function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) - internal - view - override - returns (uint256 validationData) - { + function _validateSignature( + UserOperation calldata userOp, + bytes32 userOpHash + ) internal view override returns (uint256 validationData) { bytes32 hash = userOpHash.toEthSignedMessageHash(); if (owner() != hash.recover(userOp.signature)) { return SIG_VALIDATION_FAILED; @@ -212,13 +318,20 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { function _payPrefund(uint256 missingAccountFunds) internal override { if (missingAccountFunds != 0) { - (bool success,) = payable(msg.sender).call{value: missingAccountFunds, gas: type(uint256).max}(""); + (bool success, ) = payable(msg.sender).call{ + value: missingAccountFunds, + gas: type(uint256).max + }(""); (success); } } - function isValidSignature(bytes32 _hash, bytes calldata _signature) external view returns (bytes4) { - if (ECDSA.recover(_hash, _signature) == owner()) { + function isValidSignature( + bytes32 hash, + bytes calldata _signature + ) external view returns (bytes4) { + if (signed[hash]) return 0x1626ba7e; + if (ECDSA.recover(hash, _signature) == owner()) { return 0x1626ba7e; } else { return 0xffffffff; diff --git a/src/core/PermissiveFactory.sol b/src/core/PermissiveFactory.sol index c7422d3..489cf25 100644 --- a/src/core/PermissiveFactory.sol +++ b/src/core/PermissiveFactory.sol @@ -4,16 +4,28 @@ pragma solidity ^0.8.18; import "@openzeppelin/contracts/utils/Create2.sol"; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "./PermissiveAccount.sol"; import "./FeeManager.sol"; -contract PermissiveFactory is UpgradeableBeacon { - event AccountCreated(address indexed owner, uint256 indexed salt, address indexed account); +contract PermissiveFactory is UpgradeableBeacon, Initializable { + event AccountCreated( + address indexed owner, + uint256 indexed salt, + address indexed account + ); constructor(address _impl) UpgradeableBeacon(_impl) {} - function createAccount(address owner, uint256 salt) public returns (PermissiveAccount ret) { + function initialize(address owner) external initializer { + _transferOwnership(owner); + } + + function createAccount( + address owner, + uint256 salt + ) public returns (PermissiveAccount ret) { address addr = getAddress(owner, salt); uint256 codeSize = addr.code.length; if (codeSize > 0) { @@ -30,15 +42,25 @@ contract PermissiveFactory is UpgradeableBeacon { emit AccountCreated(owner, salt, address(ret)); } - function getAddress(address owner, uint256 salt) public view returns (address) { - return Create2.computeAddress( - bytes32(salt), - keccak256( - abi.encodePacked( - type(BeaconProxy).creationCode, - abi.encode(address(this), abi.encodeCall(PermissiveAccount.initialize, (owner))) + function getAddress( + address owner, + uint256 salt + ) public view returns (address) { + return + Create2.computeAddress( + bytes32(salt), + keccak256( + abi.encodePacked( + type(BeaconProxy).creationCode, + abi.encode( + address(this), + abi.encodeCall( + PermissiveAccount.initialize, + (owner) + ) + ) + ) ) - ) - ); + ); } } diff --git a/src/integrations/safe/SafeModule.sol b/src/integrations/safe/SafeModule.sol index 5da5df1..72e10da 100644 --- a/src/integrations/safe/SafeModule.sol +++ b/src/integrations/safe/SafeModule.sol @@ -56,40 +56,78 @@ contract SafeModule is ISafeModule { _onlySafe(); bytes32 oldValue = operatorPermissions[permSet.operator]; operatorPermissions[permSet.operator] = permSet.merkleRootPermissions; - emit OperatorMutated(permSet.operator, oldValue, permSet.merkleRootPermissions); + emit OperatorMutated( + permSet.operator, + oldValue, + permSet.merkleRootPermissions + ); } - function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) - external - returns (uint256 validationData) - { + function validateUserOp( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData) { _requireFromEntryPoint(); bytes32 hash = userOpHash.toEthSignedMessageHash(); - (,,, PermissionLib.Permission memory permission, bytes32[] memory proof, uint256 providedFee) = - abi.decode(userOp.callData[4:], (address, uint256, bytes, PermissionLib.Permission, bytes32[], uint256)); + ( + , + , + , + PermissionLib.Permission memory permission, + bytes32[] memory proof, + uint256 providedFee + ) = abi.decode( + userOp.callData[4:], + ( + address, + uint256, + bytes, + PermissionLib.Permission, + bytes32[], + uint256 + ) + ); if (permission.operator.code.length > 0) { - try IERC1271(permission.operator).isValidSignature(hash, userOp.signature) returns (bytes4 magicValue) { + try + IERC1271(permission.operator).isValidSignature( + hash, + userOp.signature + ) + returns (bytes4 magicValue) { validationData = _packValidationData( ValidationData( - magicValue == IERC1271.isValidSignature.selector ? address(0) : address(1), + magicValue == IERC1271.isValidSignature.selector + ? address(0) + : address(1), permission.validAfter, permission.validUntil ) ); } catch { - validationData = - _packValidationData(ValidationData(address(1), permission.validAfter, permission.validUntil)); + validationData = _packValidationData( + ValidationData( + address(1), + permission.validAfter, + permission.validUntil + ) + ); } } else if (permission.operator != hash.recover(userOp.signature)) { return 1; } else { - validationData = - _packValidationData(ValidationData(address(0), permission.validAfter, permission.validUntil)); + validationData = _packValidationData( + ValidationData( + address(0), + permission.validAfter, + permission.validUntil + ) + ); } bytes32 permHash = permission.hash(); _validateMerklePermission(permission, proof, permHash); _validatePermission(userOp, permission, permHash); - _validateData(permission); + _validateData(userOp, userOpHash, missingAccountFunds, permission); uint256 gasFee = computeGasFee(userOp); if (providedFee != gasFee) revert("Invalid provided fee"); _payPrefund(missingAccountFunds); @@ -106,11 +144,23 @@ contract SafeModule is ISafeModule { uint256 gasFee ) external { _requireFromEntryPointOrOwner(); - payable(address(feeManager)).transfer((gasFee * feeManager.fee()) / 10000); + payable(address(feeManager)).transfer( + (gasFee * feeManager.fee()) / 10000 + ); (bool success, bytes memory result) = dest.call{value: value}( - bytes.concat(func.slice(0, 4), AllowanceCalldata.RLPtoABI(func.slice(4, func.length - 4))) + bytes.concat( + func.slice(0, 4), + AllowanceCalldata.RLPtoABI(func.slice(4, func.length - 4)) + ) + ); + emit PermissionUsed( + permission.hash(), + dest, + value, + func, + permission, + gasFee ); - emit PermissionUsed(permission.hash(), dest, value, func, permission, gasFee); if (!success) { assembly { revert(add(result, 32), mload(result)) @@ -118,10 +168,18 @@ contract SafeModule is ISafeModule { } } - function computeGasFee(UserOperation memory userOp) public pure returns (uint256 fee) { + function computeGasFee( + UserOperation memory userOp + ) public pure returns (uint256 fee) { unchecked { - uint256 mul = address(bytes20(userOp.paymasterAndData)) != address(0) ? 3 : 1; - uint256 requiredGas = userOp.callGasLimit + userOp.verificationGasLimit * mul + userOp.preVerificationGas; + uint256 mul = address(bytes20(userOp.paymasterAndData)) != + address(0) + ? 3 + : 1; + uint256 requiredGas = userOp.callGasLimit + + userOp.verificationGasLimit * + mul + + userOp.preVerificationGas; fee = requiredGas * userOp.maxFeePerGas; } @@ -129,12 +187,19 @@ contract SafeModule is ISafeModule { /* INTERNAL */ - function _validateData(PermissionLib.Permission memory permission) internal view { + function _validateData( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds, + PermissionLib.Permission memory permission + ) internal { if ( - permission.dataValidation.validator != address(0) - && !IDataValidator(permission.dataValidation.validator).isValidData( - permission.dataValidation.target, permission.dataValidation.data - ) + permission.dataValidator != address(0) && + !IDataValidator(permission.dataValidator).isValidData( + userOp, + userOpHash, + missingAccountFunds + ) ) { revert("Invalid data"); } @@ -145,8 +210,10 @@ contract SafeModule is ISafeModule { PermissionLib.Permission memory permission, bytes32 permHash ) internal { - (address to, uint256 value, bytes memory callData,,) = - abi.decode(userOp.callData[4:], (address, uint256, bytes, PermissionLib.Permission, bytes32[])); + (address to, uint256 value, bytes memory callData, , ) = abi.decode( + userOp.callData[4:], + (address, uint256, bytes, PermissionLib.Permission, bytes32[]) + ); if (permission.to != to) revert("InvalidTo"); uint256 rPermU = remainingPermUsage[permHash]; if (permission.maxUsage > 0) { @@ -162,7 +229,9 @@ contract SafeModule is ISafeModule { } if ( !AllowanceCalldata.isAllowedCalldata( - permission.allowed_arguments, callData.slice(4, callData.length - 4), value + permission.allowed_arguments, + callData.slice(4, callData.length - 4), + value ) ) revert("Not allowed Calldata"); if (permission.selector != bytes4(callData)) revert("InvalidSelector"); @@ -181,20 +250,27 @@ contract SafeModule is ISafeModule { bytes32[] memory proof, bytes32 permHash ) internal view { - bool isValidProof = - MerkleProof.verify(proof, operatorPermissions[permission.operator], keccak256(bytes.concat(permHash))); + bool isValidProof = MerkleProof.verify( + proof, + operatorPermissions[permission.operator], + keccak256(bytes.concat(permHash)) + ); if (!isValidProof) revert("Invalid Proof"); } function _requireFromEntryPointOrOwner() internal view { require( - msg.sender == address(entryPoint) || msg.sender == address(safe), "account: not from EntryPoint or owner" + msg.sender == address(entryPoint) || msg.sender == address(safe), + "account: not from EntryPoint or owner" ); } function _payPrefund(uint256 missingAccountFunds) internal { if (missingAccountFunds != 0) { - (bool success,) = payable(msg.sender).call{value: missingAccountFunds, gas: type(uint256).max}(""); + (bool success, ) = payable(msg.sender).call{ + value: missingAccountFunds, + gas: type(uint256).max + }(""); (success); } } @@ -207,6 +283,9 @@ contract SafeModule is ISafeModule { } function _requireFromEntryPoint() internal view virtual { - require(msg.sender == address(entryPoint), "account: not from EntryPoint"); + require( + msg.sender == address(entryPoint), + "account: not from EntryPoint" + ); } } diff --git a/src/integrations/zerodev/PermissiveValidator.sol b/src/integrations/zerodev/PermissiveValidator.sol index 77bed22..8e3ff63 100644 --- a/src/integrations/zerodev/PermissiveValidator.sol +++ b/src/integrations/zerodev/PermissiveValidator.sol @@ -44,14 +44,21 @@ contract PermissiveValidator is IKernelValidator, EIP712 { event OwnerChanged(address indexed oldOwner, address indexed owner); // Permissive - event OperatorMutated(address indexed operator, bytes32 indexed oldPermissions, bytes32 indexed newPermissions); + event OperatorMutated( + address indexed operator, + bytes32 indexed oldPermissions, + bytes32 indexed newPermissions + ); event UserOpValidated(bytes32 indexed userOpHash, UserOperation userOp); /* CONSTRUCTOR */ - constructor(address _entryPoint, address payable _feeManager) EIP712("Permissive x Zerodev", "v0.0.4") { + constructor( + address _entryPoint, + address payable _feeManager + ) EIP712("Permissive x Zerodev", "v0.0.4") { entryPoint = IEntryPoint(_entryPoint); feeManager = FeeManager(_feeManager); } @@ -73,64 +80,123 @@ contract PermissiveValidator is IKernelValidator, EIP712 { revert("Not implemented"); } - function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256) - external - override - returns (uint256 validationData) - { + function validateUserOp( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external override returns (uint256 validationData) { bytes32 hash = userOpHash.toEthSignedMessageHash(); - (,,, PermissionLib.Permission memory permission, bytes32[] memory proof, uint256 providedFee) = - abi.decode(userOp.callData[4:], (address, uint256, bytes, PermissionLib.Permission, bytes32[], uint256)); + ( + , + , + , + PermissionLib.Permission memory permission, + bytes32[] memory proof, + uint256 providedFee + ) = abi.decode( + userOp.callData[4:], + ( + address, + uint256, + bytes, + PermissionLib.Permission, + bytes32[], + uint256 + ) + ); uint256 operatorCodeSize; address op = permission.operator; assembly { operatorCodeSize := extcodesize(op) } if (permission.operator.code.length > 0) { - try IERC1271(permission.operator).isValidSignature(hash, userOp.signature) returns (bytes4 magicValue) { + try + IERC1271(permission.operator).isValidSignature( + hash, + userOp.signature + ) + returns (bytes4 magicValue) { validationData = _packValidationData( ValidationData( - magicValue == IERC1271.isValidSignature.selector ? address(0) : address(1), + magicValue == IERC1271.isValidSignature.selector + ? address(0) + : address(1), permission.validAfter, permission.validUntil ) ); } catch { - validationData = - _packValidationData(ValidationData(address(1), permission.validAfter, permission.validUntil)); + validationData = _packValidationData( + ValidationData( + address(1), + permission.validAfter, + permission.validUntil + ) + ); } } else if (permission.operator != hash.recover(userOp.signature)) { return 1; } else { - validationData = - _packValidationData(ValidationData(address(0), permission.validAfter, permission.validUntil)); + validationData = _packValidationData( + ValidationData( + address(0), + permission.validAfter, + permission.validUntil + ) + ); } bytes32 permHash = permission.hash(); _validateMerklePermission(permission, proof, permHash); _validatePermission(userOp, permission, permHash); - _validateData(permission); + _validateData(userOp, userOpHash, missingAccountFunds, permission); uint256 gasFee = computeGasFee(userOp); if (providedFee != gasFee) revert("Invalid provided fee"); emit UserOpValidated(userOpHash, userOp); } - function validateSignature(bytes32 hash, bytes calldata signature) external view override returns (uint256) {} + function validateSignature( + bytes32 hash, + bytes calldata signature + ) external view override returns (uint256) {} // Permissive - function setOperatorPermissions(PermissionSet calldata permSet, bytes calldata signature) external { - bytes32 digest = - _hashTypedDataV4(keccak256(abi.encode(typedStruct, permSet.operator, permSet.merkleRootPermissions))); + function setOperatorPermissions( + PermissionSet calldata permSet, + bytes calldata signature + ) external { + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + typedStruct, + permSet.operator, + permSet.merkleRootPermissions + ) + ) + ); address signer = ECDSA.recover(digest, signature); bytes32 oldValue = operatorPermissions[signer][permSet.operator]; - operatorPermissions[signer][permSet.operator] = permSet.merkleRootPermissions; - emit OperatorMutated(permSet.operator, oldValue, permSet.merkleRootPermissions); + operatorPermissions[signer][permSet.operator] = permSet + .merkleRootPermissions; + emit OperatorMutated( + permSet.operator, + oldValue, + permSet.merkleRootPermissions + ); } - function computeGasFee(UserOperation memory userOp) public pure returns (uint256 fee) { + function computeGasFee( + UserOperation memory userOp + ) public pure returns (uint256 fee) { unchecked { - uint256 mul = address(bytes20(userOp.paymasterAndData)) != address(0) ? 3 : 1; - uint256 requiredGas = userOp.callGasLimit + userOp.verificationGasLimit * mul + userOp.preVerificationGas; + uint256 mul = address(bytes20(userOp.paymasterAndData)) != + address(0) + ? 3 + : 1; + uint256 requiredGas = userOp.callGasLimit + + userOp.verificationGasLimit * + mul + + userOp.preVerificationGas; fee = requiredGas * userOp.maxFeePerGas; } @@ -140,12 +206,19 @@ contract PermissiveValidator is IKernelValidator, EIP712 { INTERNAL */ - function _validateData(PermissionLib.Permission memory permission) internal view { + function _validateData( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds, + PermissionLib.Permission memory permission + ) internal { if ( - permission.dataValidation.validator != address(0) - && !IDataValidator(permission.dataValidation.validator).isValidData( - permission.dataValidation.target, permission.dataValidation.data - ) + permission.dataValidator != address(0) && + !IDataValidator(permission.dataValidator).isValidData( + userOp, + userOpHash, + missingAccountFunds + ) ) { revert("Invalid data"); } @@ -156,8 +229,10 @@ contract PermissiveValidator is IKernelValidator, EIP712 { PermissionLib.Permission memory permission, bytes32 permHash ) internal { - (address to, uint256 value, bytes memory callData,,) = - abi.decode(userOp.callData[4:], (address, uint256, bytes, PermissionLib.Permission, bytes32[])); + (address to, uint256 value, bytes memory callData, , ) = abi.decode( + userOp.callData[4:], + (address, uint256, bytes, PermissionLib.Permission, bytes32[]) + ); if (permission.to != to) revert("InvalidTo"); if (permission.maxUsage > 0) { if (permission.maxUsage == 1) revert("OutOfPerms"); @@ -171,7 +246,9 @@ contract PermissiveValidator is IKernelValidator, EIP712 { } require( AllowanceCalldata.isAllowedCalldata( - permission.allowed_arguments, callData.slice(4, callData.length - 4), value + permission.allowed_arguments, + callData.slice(4, callData.length - 4), + value ) == true, "Not allowed Calldata" ); @@ -192,12 +269,16 @@ contract PermissiveValidator is IKernelValidator, EIP712 { bytes32 permHash ) internal view { bool isValidProof = MerkleProof.verify( - proof, operatorPermissions[msg.sender][permission.operator], keccak256(bytes.concat(permHash)) + proof, + operatorPermissions[msg.sender][permission.operator], + keccak256(bytes.concat(permHash)) ); if (!isValidProof) revert("Invalid Proof"); } - function _hashTypedDataV4(bytes32 structHash) internal view virtual override returns (bytes32) { + function _hashTypedDataV4( + bytes32 structHash + ) internal view virtual override returns (bytes32) { return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); } } diff --git a/src/interfaces/IDataValidator.sol b/src/interfaces/IDataValidator.sol index ae03c7d..bc35c8f 100644 --- a/src/interfaces/IDataValidator.sol +++ b/src/interfaces/IDataValidator.sol @@ -1,6 +1,13 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity ^0.8.18; +import "account-abstraction/core/BaseAccount.sol"; +import "./Permission.sol"; + interface IDataValidator { - function isValidData(address target, bytes calldata data) external view returns (bool); + function isValidData( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (bool); } diff --git a/src/interfaces/Permission.sol b/src/interfaces/Permission.sol index 6a0c2ba..23099f7 100644 --- a/src/interfaces/Permission.sol +++ b/src/interfaces/Permission.sol @@ -2,15 +2,6 @@ pragma solidity ^0.8.18; -struct DataValidation { - // address of your IValidator - address validator; - // address of the contract to call - address target; - // address of the calldata sent to the contract - bytes data; -} - library PermissionLib { struct Permission { // the operator @@ -30,8 +21,8 @@ library PermissionLib { uint48 validAfter; // the max number of times + 1 this permision can be used, 0 = infinite uint256 maxUsage; - // validate on-chain data - DataValidation dataValidation; + // the address validating on-chain data + address dataValidator; } function hash( @@ -47,9 +38,7 @@ library PermissionLib { permission.validUntil, permission.validAfter, permission.maxUsage, - permission.dataValidation.validator, - permission.dataValidation.target, - permission.dataValidation.data + permission.dataValidator ) ); } diff --git a/src/validators/ERC20Limiter.sol b/src/validators/ERC20Limiter.sol new file mode 100644 index 0000000..2ce7ea9 --- /dev/null +++ b/src/validators/ERC20Limiter.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity ^0.8.18; + +import "../../src/interfaces/IDataValidator.sol"; +import "../../src/core/AllowanceCalldata.sol"; +import "bytes/BytesLib.sol"; +import "forge-std/console.sol"; + +struct Limit { + address token; + uint256 limit; +} + +contract ERC20Limiter is IDataValidator { + using BytesLib for bytes; + + mapping(address owner => mapping(address token => uint256 limit)) + public limit; + + function setLimits(Limit[] calldata limits) external { + for (uint i = 0; i < limits.length; i++) { + limit[msg.sender][limits[i].token] = limits[i].limit; + } + } + + function isValidData( + UserOperation calldata userOp, + bytes32, + uint256 + ) external override returns (bool) { + ( + , + , + bytes memory func, + PermissionLib.Permission memory permission, + , + + ) = abi.decode( + userOp.callData[4:], + ( + address, + uint256, + bytes, + PermissionLib.Permission, + bytes32[], + uint256 + ) + ); + (, uint256 amount) = abi.decode( + AllowanceCalldata.RLPtoABI(func.slice(4, func.length - 4)), + (address, uint256) + ); + if (limit[msg.sender][permission.to] < amount) return false; + limit[msg.sender][permission.to] -= amount; + return true; + } +} diff --git a/test/PermissiveAccount.t.sol b/test/PermissiveAccount.t.sol index 3bdf93f..75852f3 100644 --- a/test/PermissiveAccount.t.sol +++ b/test/PermissiveAccount.t.sol @@ -11,8 +11,8 @@ import "../lib/account-abstraction/contracts/core/EntryPoint.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "./mock/Aggregator.sol"; import "account-abstraction/interfaces/IEntryPoint.sol"; -import "./mock/DataValidator.sol"; import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; +import "../../src/validators/ERC20Limiter.sol"; address constant receiver = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990; @@ -25,15 +25,28 @@ struct UserOpsPerAggregator { } library DomainSeparatorUtils { - function buildDomainSeparator(bytes32 typeHash, bytes32 nameHash, bytes32 versionHash, address target) - public - view - returns (bytes32) - { - return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, target)); + function buildDomainSeparator( + bytes32 typeHash, + bytes32 nameHash, + bytes32 versionHash, + address target + ) public view returns (bytes32) { + return + keccak256( + abi.encode( + typeHash, + nameHash, + versionHash, + block.chainid, + target + ) + ); } - function efficientHash(bytes32 a, bytes32 b) public pure returns (bytes32 value) { + function efficientHash( + bytes32 a, + bytes32 b + ) public pure returns (bytes32 value) { /// @solidity memory-safe-assembly assembly { mstore(0x00, a) @@ -50,14 +63,33 @@ contract SigUtils { DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR; } - bytes32 public constant TYPEHASH = 0xd7e1e23484f808c5620ce8d904e88d7540a3eeb37ac94e636726ed53571e4e3c; + bytes32 public constant TYPEHASH = + 0xd7e1e23484f808c5620ce8d904e88d7540a3eeb37ac94e636726ed53571e4e3c; - function getStructHash(PermissionSet memory _permit) internal pure returns (bytes32) { - return keccak256(abi.encode(TYPEHASH, _permit.operator, _permit.merkleRootPermissions)); + function getStructHash( + PermissionSet memory _permit + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + TYPEHASH, + _permit.operator, + _permit.merkleRootPermissions + ) + ); } - function getTypedDataHash(PermissionSet memory _permit) public view returns (bytes32) { - return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getStructHash(_permit))); + function getTypedDataHash( + PermissionSet memory _permit + ) public view returns (bytes32) { + return + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + getStructHash(_permit) + ) + ); } } @@ -73,13 +105,16 @@ contract PermissiveAccountTest is Test { UserOperation[] internal ops; UserOpsPerAggregator[] internal agops; address internal owner = 0xa8b802B27FB4FAD58Ed28Cb6F4Ae5061bD432e8c; - uint256 internal ownerPrivateKey = 0x18104766cc86e7fb8a7452ac9fb2bccc465a88a9bba2d2d67a5ffd3f459f820f; + uint256 internal ownerPrivateKey = + 0x18104766cc86e7fb8a7452ac9fb2bccc465a88a9bba2d2d67a5ffd3f459f820f; address internal operator = 0xabe1DE8764303a2d4421Ea583ef693CF6cAc109A; - uint256 internal operatorPrivateKey = 0x19ef7c79dbd4115a8df3d576ea6e75362d661def86250fd3ef4557a285359776; + uint256 internal operatorPrivateKey = + 0x19ef7c79dbd4115a8df3d576ea6e75362d661def86250fd3ef4557a285359776; bytes32[] internal proofs; uint256[] internal numbers; FeeManager internal feeManager; SigUtils internal utils; + Limit[] internal limits; function setUp() public { entrypoint = new EntryPoint{salt: bytes32("Permissive-v0.0.4")}(); @@ -91,7 +126,10 @@ contract PermissiveAccountTest is Test { factory = new PermissiveFactory{salt: bytes32("Permissive-v0.0.4")}( address(impl) ); - account = factory.createAccount(owner, 0x000000000000000000000000a8b802b27fb4fad58ed28cb6f4ae5061bd432e8c); + account = factory.createAccount( + owner, + 0x000000000000000000000000a8b802b27fb4fad58ed28cb6f4ae5061bd432e8c + ); token = new Token("USD Coin", "USDC"); token.mint(); token.transfer(address(account), 100 ether); @@ -105,7 +143,7 @@ contract PermissiveAccountTest is Test { 0, 0, 0, - DataValidation(address(0), address(0), hex"") + address(0) ); permissions.push(perm); utils = new SigUtils( @@ -124,14 +162,27 @@ contract PermissiveAccountTest is Test { bytes32 root = keccak256(bytes.concat(permissions[0].hash())); bytes32 digest = utils.getTypedDataHash(PermissionSet(operator, root)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions(PermissionSet(operator, root), abi.encodePacked(r, s, v)); + account.setOperatorPermissions( + PermissionSet(operator, root), + abi.encodePacked(r, s, v) + ); assert(account.operatorPermissions(operator) == root); } function testTransactionPasses() public { testPermissionsGranted(); UserOperation memory op = UserOperation( - address(account), account.getNonce(), hex"", hex"", 10000000, 10000000, 10000, 10000, 10000, hex"", hex"" + address(account), + account.getNonce(), + hex"", + hex"", + 10000000, + 10000000, + 10000, + 10000, + 10000, + hex"", + hex"" ); uint256 computedFee = account.computeGasFee(op); op.callData = abi.encodeWithSelector( @@ -146,16 +197,26 @@ contract PermissiveAccountTest is Test { proofs, computedFee ); - (uint8 v, bytes32 r, bytes32 s) = - vm.sign(operatorPrivateKey, entrypoint.getUserOpHash(op).toEthSignedMessageHash()); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + operatorPrivateKey, + entrypoint.getUserOpHash(op).toEthSignedMessageHash() + ); op.signature = abi.encodePacked(r, s, v); ops.push(op); uint256 oldFeeManagerBalance = address(feeManager).balance; payable(account).transfer((feeManager.fee() * computedFee) / 10000); entrypoint.handleOps(ops, payable(address(this))); - assert((feeManager.fee() * computedFee) / 10000 == address(feeManager).balance - oldFeeManagerBalance); - assert(token.balanceOf(address(account)) == 100 ether - 0x56bc75e2d630fffff); - assert(token.balanceOf(0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990) == 0x56bc75e2d630fffff); + assert( + (feeManager.fee() * computedFee) / 10000 == + address(feeManager).balance - oldFeeManagerBalance + ); + assert( + token.balanceOf(address(account)) == 100 ether - 0x56bc75e2d630fffff + ); + assert( + token.balanceOf(0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990) == + 0x56bc75e2d630fffff + ); } function testNoArgs() external { @@ -170,7 +231,7 @@ contract PermissiveAccountTest is Test { 0, 0, 0, - DataValidation(address(0), address(0), hex"") + address(0) ); ops.pop(); permissions.pop(); @@ -178,7 +239,10 @@ contract PermissiveAccountTest is Test { bytes32 root = keccak256(bytes.concat(perm.hash())); bytes32 digest = utils.getTypedDataHash(PermissionSet(operator, root)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions(PermissionSet(operator, root), abi.encodePacked(r, s, v)); + account.setOperatorPermissions( + PermissionSet(operator, root), + abi.encodePacked(r, s, v) + ); UserOperation memory op = UserOperation( address(account), account.getNonce(), @@ -188,7 +252,8 @@ contract PermissiveAccountTest is Test { address(incr), 0, abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000000" + incr.increment.selector, + hex"e1a00000000000000000000000000000000000000000000000000000000000000000" ), permissions[0], proofs @@ -207,14 +272,17 @@ contract PermissiveAccountTest is Test { address(incr), 0, abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000000" + incr.increment.selector, + hex"e1a00000000000000000000000000000000000000000000000000000000000000000" ), permissions[0], proofs, computedFee ); - (uint8 v2, bytes32 r2, bytes32 s2) = - vm.sign(operatorPrivateKey, entrypoint.getUserOpHash(op).toEthSignedMessageHash()); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign( + operatorPrivateKey, + entrypoint.getUserOpHash(op).toEthSignedMessageHash() + ); op.signature = abi.encodePacked(r2, s2, v2); ops.push(op); payable(account).transfer((feeManager.fee() * computedFee) / 10000); @@ -233,14 +301,17 @@ contract PermissiveAccountTest is Test { 100, // valid until 100 0, 0, - DataValidation(address(0), address(0), hex"") + address(0) ); permissions.pop(); permissions.push(perm); bytes32 root = keccak256(bytes.concat(perm.hash())); bytes32 digest = utils.getTypedDataHash(PermissionSet(operator, root)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions(PermissionSet(operator, root), abi.encodePacked(r, s, v)); + account.setOperatorPermissions( + PermissionSet(operator, root), + abi.encodePacked(r, s, v) + ); UserOperation memory op = UserOperation( address(account), account.getNonce(), @@ -250,7 +321,8 @@ contract PermissiveAccountTest is Test { address(incr), 0, abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000000" + incr.increment.selector, + hex"e1a00000000000000000000000000000000000000000000000000000000000000000" ), permissions[0], proofs @@ -269,19 +341,28 @@ contract PermissiveAccountTest is Test { address(incr), 0, abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000000" + incr.increment.selector, + hex"e1a00000000000000000000000000000000000000000000000000000000000000000" ), permissions[0], proofs, computedFee ); - (uint8 v2, bytes32 r2, bytes32 s2) = - vm.sign(operatorPrivateKey, entrypoint.getUserOpHash(op).toEthSignedMessageHash()); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign( + operatorPrivateKey, + entrypoint.getUserOpHash(op).toEthSignedMessageHash() + ); op.signature = abi.encodePacked(r2, s2, v2); ops.push(op); payable(account).transfer((feeManager.fee() * computedFee) / 10000); vm.warp(101); - vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, "AA22 expired or not due")); + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOp.selector, + 0, + "AA22 expired or not due" + ) + ); entrypoint.handleOps(ops, payable(address(this))); vm.warp(100); entrypoint.handleOps(ops, payable(address(this))); @@ -299,14 +380,17 @@ contract PermissiveAccountTest is Test { 0, 100, // valid after 100 0, - DataValidation(address(0), address(0), hex"") + address(0) ); permissions.pop(); permissions.push(perm); bytes32 root = keccak256(bytes.concat(perm.hash())); bytes32 digest = utils.getTypedDataHash(PermissionSet(operator, root)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions(PermissionSet(operator, root), abi.encodePacked(r, s, v)); + account.setOperatorPermissions( + PermissionSet(operator, root), + abi.encodePacked(r, s, v) + ); UserOperation memory op = UserOperation( address(account), account.getNonce(), @@ -316,7 +400,8 @@ contract PermissiveAccountTest is Test { address(incr), 0, abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000000" + incr.increment.selector, + hex"e1a00000000000000000000000000000000000000000000000000000000000000000" ), permissions[0], proofs @@ -335,19 +420,28 @@ contract PermissiveAccountTest is Test { address(incr), 0, abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000000" + incr.increment.selector, + hex"e1a00000000000000000000000000000000000000000000000000000000000000000" ), permissions[0], proofs, computedFee ); - (uint8 v2, bytes32 r2, bytes32 s2) = - vm.sign(operatorPrivateKey, entrypoint.getUserOpHash(op).toEthSignedMessageHash()); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign( + operatorPrivateKey, + entrypoint.getUserOpHash(op).toEthSignedMessageHash() + ); op.signature = abi.encodePacked(r2, s2, v2); ops.push(op); payable(account).transfer((feeManager.fee() * computedFee) / 10000); vm.warp(99); - vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, "AA22 expired or not due")); + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOp.selector, + 0, + "AA22 expired or not due" + ) + ); entrypoint.handleOps(ops, payable(address(this))); vm.warp(100); entrypoint.handleOps(ops, payable(address(this))); @@ -365,16 +459,29 @@ contract PermissiveAccountTest is Test { 0, 0, 0, - DataValidation(address(0), address(0), hex"") + address(0) ); permissions.pop(); permissions.push(perm); bytes32 root = keccak256(bytes.concat(perm.hash())); bytes32 digest = utils.getTypedDataHash(PermissionSet(operator, root)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions(PermissionSet(operator, root), abi.encodePacked(r, s, v)); + account.setOperatorPermissions( + PermissionSet(operator, root), + abi.encodePacked(r, s, v) + ); UserOperation memory op = UserOperation( - address(account), account.getNonce(), hex"", hex"", 10000000, 10000000, 10000, 10000, 10000, hex"", hex"" + address(account), + account.getNonce(), + hex"", + hex"", + 10000000, + 10000000, + 10000, + 10000, + 10000, + hex"", + hex"" ); uint256 computedFee = account.computeGasFee(op); op.callData = abi.encodeWithSelector( @@ -382,20 +489,25 @@ contract PermissiveAccountTest is Test { address(incr), 0, abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000001" + incr.increment.selector, + hex"e1a00000000000000000000000000000000000000000000000000000000000000001" ), permissions[0], proofs, computedFee ); - (uint8 v2, bytes32 r2, bytes32 s2) = - vm.sign(operatorPrivateKey, entrypoint.getUserOpHash(op).toEthSignedMessageHash()); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign( + operatorPrivateKey, + entrypoint.getUserOpHash(op).toEthSignedMessageHash() + ); op.signature = abi.encodePacked(r2, s2, v2); ops.push(op); payable(account).transfer((feeManager.fee() * computedFee) / 10000); vm.expectRevert( abi.encodeWithSelector( - IEntryPoint.FailedOp.selector, 0, "AA23 reverted: msg.value not corresponding to allowed value" + IEntryPoint.FailedOp.selector, + 0, + "AA23 reverted: msg.value not corresponding to allowed value" ) ); entrypoint.handleOps(ops, payable(address(this))); @@ -405,117 +517,86 @@ contract PermissiveAccountTest is Test { address(incr), 0, abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000000" + incr.increment.selector, + hex"e1a00000000000000000000000000000000000000000000000000000000000000000" ), permissions[0], proofs, computedFee ); - (uint8 v3, bytes32 r3, bytes32 s3) = - vm.sign(operatorPrivateKey, entrypoint.getUserOpHash(op).toEthSignedMessageHash()); + (uint8 v3, bytes32 r3, bytes32 s3) = vm.sign( + operatorPrivateKey, + entrypoint.getUserOpHash(op).toEthSignedMessageHash() + ); op.signature = abi.encodePacked(r3, s3, v3); ops.push(op); entrypoint.handleOps(ops, payable(address(this))); assert(incr.value() == 1); } - function testBlokchainDataValidationFailsBecauseInvalidTarget() external { - ERC721 nft = new ERC721("Bored Ape Yatch Club", "BAYC"); - DataValidator validator = new DataValidator(nft); - Incrementer incr = new Incrementer(); + function testLimiter() external { + token.mint(); + token.transfer(address(account), 100 ether); + ERC20Limiter limiter = new ERC20Limiter(); + limits.push(Limit(address(token), 50 ether)); + vm.prank(address(account)); + limiter.setLimits(limits); PermissionLib.Permission memory perm = PermissionLib.Permission( operator, - address(incr), - incr.increment.selector, - hex"e3e202a00000000000000000000000000000000000000000000000000000000000000000", + address(token), + token.transfer.selector, + hex"e7e202a00000000000000000000000000000000000000000000000000000000000000000c100c100", address(0), 0, 0, 0, - DataValidation(address(validator), address(0), abi.encode(address(account), 0)) + address(limiter) ); permissions.pop(); permissions.push(perm); bytes32 root = keccak256(bytes.concat(perm.hash())); bytes32 digest = utils.getTypedDataHash(PermissionSet(operator, root)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions(PermissionSet(operator, root), abi.encodePacked(r, s, v)); - UserOperation memory op = UserOperation( - address(account), account.getNonce(), hex"", hex"", 10000000, 10000000, 10000, 10000, 10000, hex"", hex"" + account.setOperatorPermissions( + PermissionSet(operator, root), + abi.encodePacked(r, s, v) ); - uint256 computedFee = account.computeGasFee(op); - op.callData = abi.encodeWithSelector( - account.execute.selector, - address(incr), - 0, - abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000000" - ), - permissions[0], - proofs, - computedFee - ); - (uint8 v2, bytes32 r2, bytes32 s2) = - vm.sign(operatorPrivateKey, entrypoint.getUserOpHash(op).toEthSignedMessageHash()); - op.signature = abi.encodePacked(r2, s2, v2); - ops.push(op); - payable(account).transfer((feeManager.fee() * computedFee) / 10000); - vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, "AA23 reverted: Invalid data")); - entrypoint.handleOps(ops, payable(address(this))); - } - - function testBlokchainDataValidationFailsBecauseInvalidState() external { - ERC721PresetMinterPauserAutoId nft = new ERC721PresetMinterPauserAutoId( - "Bored Ape Yatch Club", - "BAYC", - "" - ); - DataValidator validator = new DataValidator(nft); - Incrementer incr = new Incrementer(); - PermissionLib.Permission memory perm = PermissionLib.Permission( - operator, - address(incr), - incr.increment.selector, - hex"e3e202a00000000000000000000000000000000000000000000000000000000000000000", - address(0), - 0, - 0, - 0, - DataValidation(address(validator), address(nft), abi.encode(address(account), 0)) - ); - permissions.pop(); - permissions.push(perm); - bytes32 root = keccak256(bytes.concat(perm.hash())); - bytes32 digest = utils.getTypedDataHash(PermissionSet(operator, root)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions(PermissionSet(operator, root), abi.encodePacked(r, s, v)); UserOperation memory op = UserOperation( - address(account), account.getNonce(), hex"", hex"", 10000000, 10000000, 10000, 10000, 10000, hex"", hex"" + address(account), + account.getNonce(), + hex"", + hex"", + 10000000, + 10000000, + 10000, + 10000, + 10000, + hex"", + hex"" ); uint256 computedFee = account.computeGasFee(op); op.callData = abi.encodeWithSelector( account.execute.selector, - address(incr), + address(token), 0, abi.encodePacked( - incr.increment.selector, hex"e1a00000000000000000000000000000000000000000000000000000000000000000" + token.transfer.selector, + hex"f863a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000002b5e3af16b1880000" ), permissions[0], proofs, computedFee ); - (uint8 v2, bytes32 r2, bytes32 s2) = - vm.sign(operatorPrivateKey, entrypoint.getUserOpHash(op).toEthSignedMessageHash()); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign( + operatorPrivateKey, + entrypoint.getUserOpHash(op).toEthSignedMessageHash() + ); op.signature = abi.encodePacked(r2, s2, v2); ops.push(op); payable(account).transfer((feeManager.fee() * computedFee) / 10000); - vm.expectRevert( - abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, "AA23 reverted: ERC721: invalid token ID") - ); entrypoint.handleOps(ops, payable(address(this))); - nft.mint(address(account)); - entrypoint.handleOps(ops, payable(address(this))); - assert(incr.value() == 1); + ops.pop(); + assert(token.balanceOf(address(1)) == 50 ether); } function testContractSigner() external { @@ -529,14 +610,29 @@ contract PermissiveAccountTest is Test { 0, 0, 0, - DataValidation(address(0), address(0), hex"") + address(0) ); bytes32 root = keccak256(bytes.concat(perm.hash())); - bytes32 digest = utils.getTypedDataHash(PermissionSet(address(contractOperator), root)); + bytes32 digest = utils.getTypedDataHash( + PermissionSet(address(contractOperator), root) + ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions(PermissionSet(address(contractOperator), root), abi.encodePacked(r, s, v)); + account.setOperatorPermissions( + PermissionSet(address(contractOperator), root), + abi.encodePacked(r, s, v) + ); UserOperation memory op = UserOperation( - address(account), account.getNonce(), hex"", hex"", 10000000, 10000000, 10000, 10000, 10000, hex"", hex"" + address(account), + account.getNonce(), + hex"", + hex"", + 10000000, + 10000000, + 10000, + 10000, + 10000, + hex"", + hex"" ); uint256 computedFee = account.computeGasFee(op); op.callData = abi.encodeWithSelector( @@ -551,19 +647,38 @@ contract PermissiveAccountTest is Test { proofs, computedFee ); - (uint8 v1, bytes32 r1, bytes32 s1) = - vm.sign(operatorPrivateKey, entrypoint.getUserOpHash(op).toEthSignedMessageHash()); + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign( + operatorPrivateKey, + entrypoint.getUserOpHash(op).toEthSignedMessageHash() + ); op.signature = abi.encodePacked(r1, s1, v1); ops.push(op); uint256 oldFeeManagerBalance = address(feeManager).balance; payable(account).transfer((feeManager.fee() * computedFee) / 10000); - vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, "AA24 signature error")); + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOp.selector, + 0, + "AA24 signature error" + ) + ); entrypoint.handleOps(ops, payable(address(this))); - contractOperator.sign(entrypoint.getUserOpHash(op).toEthSignedMessageHash(), op.signature); + contractOperator.sign( + entrypoint.getUserOpHash(op).toEthSignedMessageHash(), + op.signature + ); entrypoint.handleOps(ops, payable(address(this))); - assert((feeManager.fee() * computedFee) / 10000 == address(feeManager).balance - oldFeeManagerBalance); - assert(token.balanceOf(address(account)) == 100 ether - 0x56bc75e2d630fffff); - assert(token.balanceOf(0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990) == 0x56bc75e2d630fffff); + assert( + (feeManager.fee() * computedFee) / 10000 == + address(feeManager).balance - oldFeeManagerBalance + ); + assert( + token.balanceOf(address(account)) == 100 ether - 0x56bc75e2d630fffff + ); + assert( + token.balanceOf(0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990) == + 0x56bc75e2d630fffff + ); } receive() external payable {} @@ -574,12 +689,10 @@ contract ContractSigner is IERC1271 { mapping(address => mapping(bytes32 => bytes32)) signatures; - function isValidSignature(bytes32 hash, bytes memory signature) - external - view - override - returns (bytes4 magicValue) - { + function isValidSignature( + bytes32 hash, + bytes memory signature + ) external view override returns (bytes4 magicValue) { address signer = hash.recover(signature); if (signatures[signer][hash] == keccak256(signature)) { return IERC1271.isValidSignature.selector; diff --git a/test/mock/DataValidator.sol b/test/mock/DataValidator.sol deleted file mode 100644 index 3b189aa..0000000 --- a/test/mock/DataValidator.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: SEE LICENSE IN LICENSE -pragma solidity 0.8.18; - -import "../../src/interfaces/IDataValidator.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - -contract DataValidator is IDataValidator { - IERC721 private immutable token; - - constructor(IERC721 _token) { - token = _token; - } - - function isValidData(address target, bytes calldata data) external view override returns (bool result) { - if (target != address(token)) return false; - (address account, uint256 tokenId) = abi.decode(data, (address, uint256)); - if (token.ownerOf(tokenId) == account) return true; - } -}