diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4988dd5..ff062a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,3 +18,6 @@ jobs: - name: Run tests run: forge test -vvv + + - name: Check formatting + run: forge fmt --check diff --git a/.gitmodules b/.gitmodules index 391b12a..21116cf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,19 +1,23 @@ [submodule "lib/Solidity-RLP"] path = lib/Solidity-RLP url = https://github.com/hamdiallam/Solidity-RLP - branch = v2.0.7 + branch = master [submodule "lib/account-abstraction"] path = lib/account-abstraction url = https://github.com/eth-infinitism/account-abstraction - branch = v0.4.0 + branch = ver0.6.0 [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts - branch = v4.8.2 + branch = master [submodule "lib/solidity-bytes-utils"] path = lib/solidity-bytes-utils url = https://github.com/GNSPS/solidity-bytes-utils - branch = v0.8.0 + branch = master [submodule "lib/safe-contracts"] path = lib/safe-contracts url = https://github.com/safe-global/safe-contracts +[submodule "lib/zerodev-wallet-kernel"] + path = lib/zerodev-wallet-kernel + url = https://github.com/zerodevapp/zerodev-wallet-kernel + branch = kernel_v2 diff --git a/lib/Solidity-RLP b/lib/Solidity-RLP index ca49298..0212f8e 160000 --- a/lib/Solidity-RLP +++ b/lib/Solidity-RLP @@ -1 +1 @@ -Subproject commit ca4929893192fc1568ad693b9c7e43d3b59ef27c +Subproject commit 0212f8e754471da67fc5387df7855f47f944f925 diff --git a/lib/account-abstraction b/lib/account-abstraction index abff2ac..187613b 160000 --- a/lib/account-abstraction +++ b/lib/account-abstraction @@ -1 +1 @@ -Subproject commit abff2aca61a8f0934e533d0d352978055fddbd96 +Subproject commit 187613b0172c3a21cf3496e12cdfa24af04fb510 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index d00acef..1642b66 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit d00acef4059807535af0bd0dd0ddf619747a044b +Subproject commit 1642b6639b93e3b97be163d49827e1f56b81ca11 diff --git a/lib/safe-contracts b/lib/safe-contracts index e870f51..4b9c46f 160000 --- a/lib/safe-contracts +++ b/lib/safe-contracts @@ -1 +1 @@ -Subproject commit e870f514ad34cd9654c72174d6d4a839e3c6639f +Subproject commit 4b9c46fcfe687aea173e6b42aaa970b7ef701cc2 diff --git a/lib/zerodev-wallet-kernel b/lib/zerodev-wallet-kernel new file mode 160000 index 0000000..c5c5e1b --- /dev/null +++ b/lib/zerodev-wallet-kernel @@ -0,0 +1 @@ +Subproject commit c5c5e1b5e93196a6dc2234af825aa49ccb3d84b8 diff --git a/readme.md b/readme.md index 259b270..74ea1b9 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,31 @@ -# Deployments +## Deployments -| factory | account implementation | fee manager | safe factory | -| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | -| 0x499dcb87582897b050fa0579eb0d5fe71ce491b3 | 0x031d104686cde4d7ac2a31831efe36eee888af2c | 0x2ad29cc25d9fb3647b304353a0a609f27afe380b | 0x0c850e3c696871e6b72a410bea7b1c96870e0715 | +## Core + +### PermissiveFactory + +`0x499dcb87582897b050fa0579eb0d5fe71ce491b3` + +### PermissiveAccount + +`0x031d104686cde4d7ac2a31831efe36eee888af2c` + +### FeeManager + +`0x2ad29cc25d9fb3647b304353a0a609f27afe380b` + +## Safe + +### SafeFactory + +`0x0c850e3c696871e6b72a410bea7b1c96870e0715` + +### SafeModule + +`0x536e29988dcdab2d8e2f2d7621eb2eee7393df28` + +## Zerodev + +### PermissiveValidator + +### PermissiveExecutor diff --git a/remappings.txt b/remappings.txt index 6d56e2e..0195b58 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,4 +4,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ @openzeppelin/=lib/openzeppelin-contracts/ bytes/=lib/solidity-bytes-utils/contracts/ -safe/=lib/safe-contracts/contracts/ \ No newline at end of file +safe/=lib/safe-contracts/contracts/ +zerodev/=lib/zerodev-wallet-kernel/src/ \ No newline at end of file diff --git a/src/core/AllowanceCalldata.sol b/src/core/AllowanceCalldata.sol index 3e8af71..32594bd 100644 --- a/src/core/AllowanceCalldata.sol +++ b/src/core/AllowanceCalldata.sol @@ -3,21 +3,22 @@ pragma solidity ^0.8.18; import "Solidity-RLP/RLPReader.sol"; -uint constant ANY = 0; -uint constant NE = 1; -uint constant EQ = 2; -uint constant GT = 3; -uint constant LT = 4; -uint constant AND = 5; -uint constant OR = 6; +uint256 constant ANY = 0; +uint256 constant NE = 1; +uint256 constant EQ = 2; +uint256 constant GT = 3; +uint256 constant LT = 4; +uint256 constant AND = 5; +uint256 constant OR = 6; library AllowanceCalldata { - function sliceRLPItems( - RLPReader.RLPItem[] memory arguments, - uint start - ) internal pure returns (RLPReader.RLPItem[] memory newArguments) { + function sliceRLPItems(RLPReader.RLPItem[] memory arguments, uint256 start) + internal + pure + returns (RLPReader.RLPItem[] memory newArguments) + { newArguments = new RLPReader.RLPItem[](arguments.length - start); - uint initialStart = start; + uint256 initialStart = start; for (; start < arguments.length; start++) { newArguments[start - initialStart] = arguments[start]; } @@ -29,51 +30,33 @@ library AllowanceCalldata { bool isOr ) internal view returns (bool canPass) { if (allowedArguments.length == 0) return true; - for (uint i = 0; i < allowedArguments.length; i++) { - RLPReader.RLPItem[] memory prefixAndArg = RLPReader.toList( - allowedArguments[i] - ); - uint prefix = RLPReader.toUint(prefixAndArg[0]); + for (uint256 i = 0; i < allowedArguments.length; 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] - ); + bytes memory allowedArgument = RLPReader.toBytes(prefixAndArg[1]); bytes memory argument = RLPReader.toBytes(arguments[i]); canPass = keccak256(allowedArgument) == keccak256(argument); } else if (prefix == LT) { - uint allowedArgument = RLPReader.toUint(prefixAndArg[1]); - uint argument = RLPReader.toUint(arguments[i]); + uint256 allowedArgument = RLPReader.toUint(prefixAndArg[1]); + uint256 argument = RLPReader.toUint(arguments[i]); canPass = argument < allowedArgument; } else if (prefix == GT) { - uint allowedArgument = RLPReader.toUint(prefixAndArg[1]); - uint argument = RLPReader.toUint(arguments[i]); + uint256 allowedArgument = RLPReader.toUint(prefixAndArg[1]); + 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++; } 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++; } else { revert("Invalid calldata prefix"); @@ -85,31 +68,22 @@ library AllowanceCalldata { return canPass; } - function isAllowedCalldata( - bytes memory allowed, - bytes memory data - ) internal view returns (bool isOk) { + function isAllowedCalldata(bytes memory allowed, bytes memory data) 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) + if (allowedArguments.length != arguments.length) { revert("Invalid arguments length"); + } 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 = 0; i < arguments.length; i++) { - abiEncoded = bytes.concat( - abiEncoded, - RLPReader.toBytes(arguments[i]) - ); + abiEncoded = bytes.concat(abiEncoded, RLPReader.toBytes(arguments[i])); } } } diff --git a/src/core/FeeManager.sol b/src/core/FeeManager.sol index e203326..da3f269 100644 --- a/src/core/FeeManager.sol +++ b/src/core/FeeManager.sol @@ -7,7 +7,8 @@ import "@openzeppelin/contracts/access/Ownable.sol"; contract FeeManager is Ownable { uint256 public fee = 2000; bool initialized; - event FeePaid(address indexed from, uint amount); + + event FeePaid(address indexed from, uint256 amount); function initialize(address owner) external { require(!initialized); diff --git a/src/core/PermissiveAccount.sol b/src/core/PermissiveAccount.sol index 665eefb..6fb5e9b 100644 --- a/src/core/PermissiveAccount.sol +++ b/src/core/PermissiveAccount.sol @@ -20,6 +20,8 @@ bytes32 constant typedStruct = 0xcd3966ea44fb027b668c722656f7791caa71de9073b3cbb contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { using ECDSA for bytes32; using BytesLib for bytes; + using PermissionLib for PermissionLib.Permission; + mapping(address => uint256) public remainingFeeForOperator; mapping(address => uint256) public remainingValueForOperator; mapping(address => bytes32) public operatorPermissions; @@ -28,10 +30,7 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { FeeManager private immutable feeManager; bool private _initialized; - constructor( - address __entryPoint, - address payable _feeManager - ) EIP712("Permissive Account", "v0.0.3") { + constructor(address __entryPoint, address payable _feeManager) EIP712("Permissive Account", "v0.0.3") { _entryPoint = IEntryPoint(__entryPoint); feeManager = FeeManager(_feeManager); } @@ -57,37 +56,18 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { uint256 maxFee, bytes calldata signature ) external { - bytes32 digest = _hashTypedDataV4( - keccak256( - abi.encode( - typedStruct, - operator, - merkleRootPermissions, - maxValue, - maxFee - ) - ) - ); + bytes32 digest = + _hashTypedDataV4(keccak256(abi.encode(typedStruct, operator, merkleRootPermissions, maxValue, maxFee))); address signer = ECDSA.recover(digest, signature); if (signer != owner()) revert NotAllowed(signer); bytes32 oldValue = operatorPermissions[operator]; operatorPermissions[operator] = merkleRootPermissions; remainingFeeForOperator[operator] = maxFee; remainingValueForOperator[operator] = maxValue; - emit OperatorMutated( - operator, - oldValue, - merkleRootPermissions, - maxValue, - maxFee - ); + emit OperatorMutated(operator, oldValue, merkleRootPermissions, maxValue, maxFee); } - 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) @@ -95,24 +75,15 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { _requireFromEntryPoint(); bytes32 hash = userOpHash.toEthSignedMessageHash(); if (owner() != hash.recover(userOp.signature)) { - ( - , - , - , - Permission memory permission, - bytes32[] memory proof, - uint256 providedFee - ) = abi.decode( - userOp.callData[4:], - (address, uint256, bytes, 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 != hash.recover(userOp.signature)) { validationData = SIG_VALIDATION_FAILED; } - bytes32 permHash = hashPerm(permission); + bytes32 permHash = permission.hash(); _validateMerklePermission(permission, proof, permHash); _validatePermission(userOp, permission, permHash); - uint gasFee = computeGasFee(userOp); + uint256 gasFee = computeGasFee(userOp); if (providedFee != gasFee) revert("Invalid provided fee"); if (gasFee > remainingFeeForOperator[permission.operator]) { revert("Exceeded Fees"); @@ -127,7 +98,7 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { address dest, uint256 value, bytes memory func, - Permission calldata permission, + PermissionLib.Permission calldata permission, // stores the proof, only used in validateUserOp bytes32[] calldata, uint256 gasFee @@ -135,36 +106,20 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { _requireFromEntryPointOrOwner(); if (msg.sender != owner()) { if (permission.expiresAtUnix != 0) { - if (block.timestamp >= permission.expiresAtUnix) - revert ExpiredPermission( - block.timestamp, - permission.expiresAtUnix - ); + if (block.timestamp >= permission.expiresAtUnix) { + revert ExpiredPermission(block.timestamp, permission.expiresAtUnix); + } } else if (permission.expiresAtBlock != 0) { - if (block.number >= permission.expiresAtBlock) - revert ExpiredPermission( - block.number, - permission.expiresAtBlock - ); + if (block.number >= permission.expiresAtBlock) { + revert ExpiredPermission(block.number, permission.expiresAtBlock); + } } } - 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)) - ) - ); - emit PermissionUsed( - hashPerm(permission), - dest, - value, - func, - permission, - gasFee + bytes.concat(func.slice(0, 4), AllowanceCalldata.RLPtoABI(func.slice(4, func.length - 4))) ); + emit PermissionUsed(permission.hash(), dest, value, func, permission, gasFee); if (!success) { assembly { revert(add(result, 32), mload(result)) @@ -172,18 +127,10 @@ 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; } @@ -191,61 +138,41 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { /* 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 hashPerm( - Permission memory permission - ) internal pure returns (bytes32 permHash) { - permHash = keccak256( - abi.encode( - permission.operator, - permission.to, - permission.selector, - permission.allowed_arguments, - permission.paymaster, - permission.expiresAtUnix, - permission.expiresAtBlock, - permission.maxUsage - ) - ); - } - function _validatePermission( UserOperation calldata userOp, - Permission memory permission, + PermissionLib.Permission memory permission, bytes32 permHash ) internal { - (address to, uint256 value, bytes memory callData, , ) = abi.decode( - userOp.callData[4:], - (address, uint256, bytes, 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 (remainingValueForOperator[permission.operator] < value) + if (remainingValueForOperator[permission.operator] < value) { revert("ExceededValue"); + } remainingValueForOperator[permission.operator] -= value; if (permission.maxUsage > 0) { if (permission.maxUsage == 1) revert("OutOfPerms"); - if (remainingPermUsage[hashPerm(permission)] == 1) + if (remainingPermUsage[permission.hash()] == 1) { revert("OutOfPerms2"); + } if (remainingPermUsage[permHash] == 0) { remainingPermUsage[permHash] = permission.maxUsage; } remainingPermUsage[permHash]--; } require( - AllowanceCalldata.isAllowedCalldata( - permission.allowed_arguments, - callData.slice(4, callData.length - 4) - ) == true, + AllowanceCalldata.isAllowedCalldata(permission.allowed_arguments, callData.slice(4, callData.length - 4)) + == true, "Not allowed Calldata" ); if (permission.selector != bytes4(callData)) revert("InvalidSelector"); - if (permission.expiresAtUnix != 0 && permission.expiresAtBlock != 0) + if (permission.expiresAtUnix != 0 && permission.expiresAtBlock != 0) { revert("InvalidPermission"); + } if (permission.paymaster != address(0)) { address paymaster = address(0); assembly { @@ -257,88 +184,41 @@ contract PermissiveAccount is BaseAccount, IPermissiveAccount, Ownable, EIP712 { } function _validateMerklePermission( - Permission memory permission, + PermissionLib.Permission memory permission, bytes32[] memory proof, bytes32 permHash ) public 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)) + if (owner() != hash.recover(userOp.signature)) { return SIG_VALIDATION_FAILED; + } return 0; } 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 recoverSigner( - bytes32 _hash, - bytes memory _signature - ) internal pure returns (address signer) { - require( - _signature.length == 65, - "SignatureValidator#recoverSigner: invalid signature length" - ); - bytes32 r; - bytes32 s; - uint8 v; - assembly { - r := mload(add(_signature, 32)) - s := mload(add(_signature, 64)) - v := and(mload(add(_signature, 65)), 255) - } - if ( - uint256(s) > - 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 - ) { - revert( - "SignatureValidator#recoverSigner: invalid signature 's' value" - ); - } - if (v != 27 && v != 28) { - revert( - "SignatureValidator#recoverSigner: invalid signature 'v' value" - ); - } - signer = ecrecover(_hash, v, r, s); - require( - signer != address(0x0), - "SignatureValidator#recoverSigner: INVALID_SIGNER" - ); - - return signer; - } - - function isValidSignature( - bytes32 _hash, - bytes calldata _signature - ) external view returns (bytes4) { - if (recoverSigner(_hash, _signature) == owner()) { + function isValidSignature(bytes32 _hash, bytes calldata _signature) external view returns (bytes4) { + if (ECDSA.recover(_hash, _signature) == owner()) { return 0x1626ba7e; } else { return 0xffffffff; diff --git a/src/core/PermissiveFactory.sol b/src/core/PermissiveFactory.sol index b807935..0c7d43f 100644 --- a/src/core/PermissiveFactory.sol +++ b/src/core/PermissiveFactory.sol @@ -11,22 +11,15 @@ import "account-abstraction/interfaces/IEntryPoint.sol"; contract PermissiveFactory { PermissiveAccount public immutable accountImplementation; - event AccountCreated( - address indexed owner, - uint256 indexed salt, - address indexed account - ); + event AccountCreated(address indexed owner, uint256 indexed salt, address indexed account); constructor(address entrypoint, address payable feeManager) { accountImplementation = new PermissiveAccount(entrypoint, feeManager); } - function createAccount( - address owner, - uint256 salt - ) public returns (PermissiveAccount ret) { + function createAccount(address owner, uint256 salt) public returns (PermissiveAccount ret) { address addr = getAddress(owner, salt); - uint codeSize = addr.code.length; + uint256 codeSize = addr.code.length; if (codeSize > 0) { return PermissiveAccount(payable(addr)); } @@ -41,25 +34,15 @@ contract PermissiveFactory { 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(ERC1967Proxy).creationCode, - abi.encode( - address(accountImplementation), - abi.encodeCall( - PermissiveAccount.initialize, - (owner) - ) - ) - ) + function getAddress(address owner, uint256 salt) public view returns (address) { + return Create2.computeAddress( + bytes32(salt), + keccak256( + abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode(address(accountImplementation), abi.encodeCall(PermissiveAccount.initialize, (owner))) ) - ); + ) + ); } } diff --git a/src/integrations/safe/ISafe.sol b/src/integrations/safe/ISafe.sol index 254eff0..5c8d8fc 100644 --- a/src/integrations/safe/ISafe.sol +++ b/src/integrations/safe/ISafe.sol @@ -13,10 +13,7 @@ interface ISafe { /// @param value Ether value of module transaction. /// @param data Data payload of module transaction. /// @param operation Operation type of module transaction. - function execTransactionFromModule( - address to, - uint256 value, - bytes calldata data, - Operation operation - ) external returns (bool success); + function execTransactionFromModule(address to, uint256 value, bytes calldata data, Operation operation) + external + returns (bool success); } diff --git a/src/integrations/safe/ISafeModule.sol b/src/integrations/safe/ISafeModule.sol index 09ff74f..1bd8a9d 100644 --- a/src/integrations/safe/ISafeModule.sol +++ b/src/integrations/safe/ISafeModule.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.18; import "./ISafe.sol"; +import "account-abstraction/interfaces/IAccount.sol"; +import "../../interfaces/Permission.sol"; interface ISafeModule { error InvalidSafe(address safe); @@ -15,15 +17,23 @@ interface ISafeModule { error InvalidPermission(); error InvalidPaymaster(address provided, address expected); error InvalidSelector(bytes4 provided, bytes4 expected); - error ExpiredPermission(uint current, uint expiredAt); + error ExpiredPermission(uint256 current, uint256 expiredAt); event OperatorMutated( - address operator, - bytes32 oldPermissions, - bytes32 newPermissions, + address indexed operator, + bytes32 indexed oldPermissions, + bytes32 indexed newPermissions, uint256 maxValue, uint256 maxFee ); - + event UserOpValidated(bytes32 indexed userOpHash, UserOperation userOp); + event PermissionUsed( + bytes32 indexed permHash, + address dest, + uint256 value, + bytes func, + PermissionLib.Permission permission, + uint256 gasFee + ); event NewSafe(address safe); } diff --git a/src/integrations/safe/SafeFactory.sol b/src/integrations/safe/SafeFactory.sol index 9d90cef..f2e0a05 100644 --- a/src/integrations/safe/SafeFactory.sol +++ b/src/integrations/safe/SafeFactory.sol @@ -8,22 +8,16 @@ import "./SafeModule.sol"; contract SafeFactory { SafeModule public immutable moduleImplementation; - event AccountCreated( - address indexed safe, - uint256 indexed salt, - address indexed account - ); + + event AccountCreated(address indexed safe, uint256 indexed salt, address indexed account); constructor(address entrypoint, address payable feeManager) { moduleImplementation = new SafeModule(entrypoint, feeManager); } - function createAccount( - address safe, - uint256 salt - ) public returns (SafeModule ret) { + function createAccount(address safe, uint256 salt) public returns (SafeModule ret) { address addr = getAddress(safe, salt); - uint codeSize = addr.code.length; + uint256 codeSize = addr.code.length; if (codeSize > 0) { return SafeModule(payable(addr)); } @@ -38,22 +32,15 @@ contract SafeFactory { emit AccountCreated(safe, salt, address(ret)); } - function getAddress( - address safe, - uint256 salt - ) public view returns (address) { - return - Create2.computeAddress( - bytes32(salt), - keccak256( - abi.encodePacked( - type(ERC1967Proxy).creationCode, - abi.encode( - address(moduleImplementation), - abi.encodeCall(SafeModule.setSafe, (safe)) - ) - ) + function getAddress(address safe, uint256 salt) public view returns (address) { + return Create2.computeAddress( + bytes32(salt), + keccak256( + abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode(address(moduleImplementation), abi.encodeCall(SafeModule.setSafe, (safe))) ) - ); + ) + ); } } diff --git a/src/integrations/safe/SafeModule.sol b/src/integrations/safe/SafeModule.sol index 848f6e6..ce65fdb 100644 --- a/src/integrations/safe/SafeModule.sol +++ b/src/integrations/safe/SafeModule.sol @@ -19,8 +19,11 @@ import "../../core/FeeManager.sol"; contract SafeModule is ISafeModule { ISafe public safe; + using ECDSA for bytes32; using BytesLib for bytes; + using PermissionLib for PermissionLib.Permission; + mapping(address => uint256) public remainingFeeForOperator; mapping(address => uint256) public remainingValueForOperator; mapping(address => bytes32) public operatorPermissions; @@ -47,64 +50,46 @@ contract SafeModule is ISafeModule { // EXTERNAL FUNCTIONS - function setOperatorPermissions( - address operator, - bytes32 merkleRootPermissions, - uint256 maxValue, - uint256 maxFee - ) external { + function setOperatorPermissions(address operator, bytes32 merkleRootPermissions, uint256 maxValue, uint256 maxFee) + external + { _onlySafe(); bytes32 oldValue = operatorPermissions[operator]; operatorPermissions[operator] = merkleRootPermissions; remainingFeeForOperator[operator] = maxFee; remainingValueForOperator[operator] = maxValue; - emit OperatorMutated( - operator, - oldValue, - merkleRootPermissions, - maxValue, - maxFee - ); + emit OperatorMutated(operator, oldValue, merkleRootPermissions, maxValue, maxFee); } - 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(); - ( - , - , - , - Permission memory permission, - bytes32[] memory proof, - uint256 providedFee - ) = abi.decode( - userOp.callData[4:], - (address, uint256, bytes, 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 != hash.recover(userOp.signature)) { validationData = 1; } - bytes32 permHash = hashPerm(permission); + bytes32 permHash = permission.hash(); _validateMerklePermission(permission, proof, permHash); _validatePermission(userOp, permission, permHash); - uint gasFee = computeGasFee(userOp); + uint256 gasFee = computeGasFee(userOp); if (providedFee != gasFee) revert("Invalid provided fee"); if (gasFee > remainingFeeForOperator[permission.operator]) { revert("Exceeded Fees"); } remainingFeeForOperator[permission.operator] -= gasFee; _payPrefund(missingAccountFunds); + emit UserOpValidated(userOpHash, userOp); } function execute( address dest, uint256 value, bytes memory func, - Permission calldata permission, + PermissionLib.Permission calldata permission, // stores the proof, only used in validateUserOp bytes32[] calldata, uint256 gasFee @@ -112,28 +97,20 @@ contract SafeModule is ISafeModule { _requireFromEntryPointOrOwner(); if (msg.sender != address(safe)) { if (permission.expiresAtUnix != 0) { - if (block.timestamp >= permission.expiresAtUnix) - revert ExpiredPermission( - block.timestamp, - permission.expiresAtUnix - ); + if (block.timestamp >= permission.expiresAtUnix) { + revert ExpiredPermission(block.timestamp, permission.expiresAtUnix); + } } else if (permission.expiresAtBlock != 0) { - if (block.number >= permission.expiresAtBlock) - revert ExpiredPermission( - block.number, - permission.expiresAtBlock - ); + if (block.number >= permission.expiresAtBlock) { + revert ExpiredPermission(block.number, permission.expiresAtBlock); + } } } - 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); if (!success) { assembly { revert(add(result, 32), mload(result)) @@ -141,18 +118,10 @@ 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; } @@ -160,55 +129,37 @@ contract SafeModule is ISafeModule { /* INTERNAL */ - function hashPerm( - Permission memory permission - ) internal pure returns (bytes32 permHash) { - permHash = keccak256( - abi.encode( - permission.operator, - permission.to, - permission.selector, - permission.allowed_arguments, - permission.paymaster, - permission.expiresAtUnix, - permission.expiresAtBlock, - permission.maxUsage - ) - ); - } - function _validatePermission( UserOperation calldata userOp, - Permission memory permission, + PermissionLib.Permission memory permission, bytes32 permHash ) internal { - (address to, uint256 value, bytes memory callData, , ) = abi.decode( - userOp.callData[4:], - (address, uint256, bytes, 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 (remainingValueForOperator[permission.operator] < value) + if (remainingValueForOperator[permission.operator] < value) { revert("ExceededValue"); + } remainingValueForOperator[permission.operator] -= value; if (permission.maxUsage > 0) { if (permission.maxUsage == 1) revert("OutOfPerms"); - if (remainingPermUsage[hashPerm(permission)] == 1) + if (remainingPermUsage[permission.hash()] == 1) { revert("OutOfPerms2"); + } if (remainingPermUsage[permHash] == 0) { remainingPermUsage[permHash] = permission.maxUsage; } remainingPermUsage[permHash]--; } require( - AllowanceCalldata.isAllowedCalldata( - permission.allowed_arguments, - callData.slice(4, callData.length - 4) - ) == true, + AllowanceCalldata.isAllowedCalldata(permission.allowed_arguments, callData.slice(4, callData.length - 4)) + == true, "Not allowed Calldata" ); if (permission.selector != bytes4(callData)) revert("InvalidSelector"); - if (permission.expiresAtUnix != 0 && permission.expiresAtBlock != 0) + if (permission.expiresAtUnix != 0 && permission.expiresAtBlock != 0) { revert("InvalidPermission"); + } if (permission.paymaster != address(0)) { address paymaster = address(0); assembly { @@ -220,80 +171,28 @@ contract SafeModule is ISafeModule { } function _validateMerklePermission( - Permission memory permission, + PermissionLib.Permission memory permission, bytes32[] memory proof, bytes32 permHash ) public 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); } } - function recoverSigner( - bytes32 _hash, - bytes memory _signature - ) internal pure returns (address signer) { - require( - _signature.length == 65, - "SignatureValidator#recoverSigner: invalid signature length" - ); - bytes32 r; - bytes32 s; - uint8 v; - assembly { - r := mload(add(_signature, 32)) - s := mload(add(_signature, 64)) - v := and(mload(add(_signature, 65)), 255) - } - if ( - uint256(s) > - 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 - ) { - revert( - "SignatureValidator#recoverSigner: invalid signature 's' value" - ); - } - if (v != 27 && v != 28) { - revert( - "SignatureValidator#recoverSigner: invalid signature 'v' value" - ); - } - signer = ecrecover(_hash, v, r, s); - require( - signer != address(0x0), - "SignatureValidator#recoverSigner: INVALID_SIGNER" - ); - - return signer; - } - - function isValidSignature( - bytes32, - bytes calldata - ) external pure returns (bytes4) { - return 0xffffffff; - } - receive() external payable {} function _onlySafe() internal view { @@ -302,9 +201,6 @@ 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/PermissiveExecutor.sol b/src/integrations/zerodev/PermissiveExecutor.sol new file mode 100644 index 0000000..564706d --- /dev/null +++ b/src/integrations/zerodev/PermissiveExecutor.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE + +pragma solidity ^0.8.18; + +import "../../interfaces/Permission.sol"; +import "../../core/AllowanceCalldata.sol"; +import "../../core/FeeManager.sol"; +import "bytes/BytesLib.sol"; + +contract PermissiveExecutor { + using BytesLib for bytes; + using PermissionLib for PermissionLib.Permission; + + error ExpiredPermission(uint256 current, uint256 expiredAt); + + event PermissionUsed( + bytes32 indexed permHash, + address dest, + uint256 value, + bytes func, + PermissionLib.Permission permission, + uint256 gasFee + ); + + FeeManager private immutable feeManager; + + constructor(address payable _feeManager) { + feeManager = FeeManager(_feeManager); + } + + function executeWithPermissive( + address dest, + uint256 value, + bytes memory func, + PermissionLib.Permission calldata permission, + // stores the proof, only used in validateUserOp + bytes32[] calldata, + uint256 gasFee + ) external { + if (permission.expiresAtUnix != 0) { + if (block.timestamp >= permission.expiresAtUnix) { + revert ExpiredPermission(block.timestamp, permission.expiresAtUnix); + } + } else if (permission.expiresAtBlock != 0) { + if (block.number >= permission.expiresAtBlock) { + revert ExpiredPermission(block.number, permission.expiresAtBlock); + } + } + 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))) + ); + emit PermissionUsed(permission.hash(), dest, value, func, permission, gasFee); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } +} diff --git a/src/integrations/zerodev/PermissiveValidator.sol b/src/integrations/zerodev/PermissiveValidator.sol new file mode 100644 index 0000000..3829895 --- /dev/null +++ b/src/integrations/zerodev/PermissiveValidator.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE + +pragma solidity ^0.8.18; + +import "zerodev/validator/IValidator.sol"; +import "account-abstraction/interfaces/IEntryPoint.sol"; +import "../../core/FeeManager.sol"; +import "../../core/AllowanceCalldata.sol"; +import "../../interfaces/Permission.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import "bytes/BytesLib.sol"; + +struct PermissiveValidatorStorage { + address owner; + mapping(address => uint256) remainingFeeForOperator; + mapping(address => uint256) remainingValueForOperator; + mapping(address => bytes32) operatorPermissions; + mapping(bytes32 => uint256) remainingPermUsage; +} + +contract PermissiveValidator is IKernelValidator { + using ECDSA for bytes32; + using BytesLib for bytes; + using PermissionLib for PermissionLib.Permission; + + // Permissive storage + IEntryPoint public immutable entryPoint; + FeeManager private immutable feeManager; + // Zerodev Validator compatible storage + mapping(address => PermissiveValidatorStorage) permissiveValidatorStorage; + + /* + EVENTS + */ + + // Zerodev + event OwnerChanged(address indexed oldOwner, address indexed owner); + + // Permissive + event OperatorMutated( + address indexed operator, + bytes32 indexed oldPermissions, + bytes32 indexed newPermissions, + uint256 maxValue, + uint256 maxFee + ); + event UserOpValidated(bytes32 indexed userOpHash, UserOperation userOp); + + /* + CONSTRUCTOR + */ + + constructor(address _entryPoint, address payable _feeManager) { + entryPoint = IEntryPoint(_entryPoint); + feeManager = FeeManager(_feeManager); + } + + /* + EXTERNALS + */ + + // Zerodev Validator + + function enable(bytes calldata _data) external override { + address owner = address(bytes20(_data[0:20])); + address oldOwner = permissiveValidatorStorage[msg.sender].owner; + permissiveValidatorStorage[msg.sender].owner = owner; + emit OwnerChanged(oldOwner, owner); + } + + function disable(bytes calldata) external pure override { + revert("Not implemented"); + } + + function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256) + 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)); + if (permission.operator != hash.recover(userOp.signature)) { + validationData = 1; + } + bytes32 permHash = permission.hash(); + _validateMerklePermission(permission, proof, permHash); + _validatePermission(userOp, permission, permHash); + uint256 gasFee = computeGasFee(userOp); + if (providedFee != gasFee) revert("Invalid provided fee"); + if (gasFee > permissiveValidatorStorage[msg.sender].remainingFeeForOperator[permission.operator]) { + revert("Exceeded Fees"); + } + permissiveValidatorStorage[msg.sender].remainingFeeForOperator[permission.operator] -= gasFee; + emit UserOpValidated(userOpHash, userOp); + } + + function validateSignature(bytes32 hash, bytes calldata signature) external view override returns (uint256) { + address owner = permissiveValidatorStorage[msg.sender].owner; + return owner == ECDSA.recover(hash, signature) ? 0 : 1; + } + + // Permissive + + function setOperatorPermissions(address operator, bytes32 merkleRootPermissions, uint256 maxValue, uint256 maxFee) + external + { + bytes32 oldValue = permissiveValidatorStorage[msg.sender].operatorPermissions[operator]; + permissiveValidatorStorage[msg.sender].operatorPermissions[operator] = merkleRootPermissions; + permissiveValidatorStorage[msg.sender].remainingFeeForOperator[operator] = maxFee; + permissiveValidatorStorage[msg.sender].remainingValueForOperator[operator] = maxValue; + emit OperatorMutated(operator, oldValue, merkleRootPermissions, maxValue, maxFee); + } + + 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; + + fee = requiredGas * userOp.maxFeePerGas; + } + } + + /* + INTERNAL + */ + + function _validatePermission( + UserOperation calldata userOp, + 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[])); + if (permission.to != to) revert("InvalidTo"); + if (permissiveValidatorStorage[msg.sender].remainingValueForOperator[permission.operator] < value) { + revert("ExceededValue"); + } + permissiveValidatorStorage[msg.sender].remainingValueForOperator[permission.operator] -= value; + if (permission.maxUsage > 0) { + if (permission.maxUsage == 1) revert("OutOfPerms"); + if (permissiveValidatorStorage[msg.sender].remainingPermUsage[permission.hash()] == 1) { + revert("OutOfPerms2"); + } + if (permissiveValidatorStorage[msg.sender].remainingPermUsage[permHash] == 0) { + permissiveValidatorStorage[msg.sender].remainingPermUsage[permHash] = permission.maxUsage; + } + permissiveValidatorStorage[msg.sender].remainingPermUsage[permHash]--; + } + require( + AllowanceCalldata.isAllowedCalldata(permission.allowed_arguments, callData.slice(4, callData.length - 4)) + == true, + "Not allowed Calldata" + ); + if (permission.selector != bytes4(callData)) revert("InvalidSelector"); + if (permission.expiresAtUnix != 0 && permission.expiresAtBlock != 0) { + revert("InvalidPermission"); + } + if (permission.paymaster != address(0)) { + address paymaster = address(0); + assembly { + let paymasterOffset := calldataload(add(userOp, 288)) + paymaster := calldataload(add(paymasterOffset, add(userOp, 20))) + } + if (paymaster != permission.paymaster) revert("InvalidPaymaster"); + } + } + + function _validateMerklePermission( + PermissionLib.Permission memory permission, + bytes32[] memory proof, + bytes32 permHash + ) public view { + bool isValidProof = MerkleProof.verify( + proof, + permissiveValidatorStorage[msg.sender].operatorPermissions[permission.operator], + keccak256(bytes.concat(permHash)) + ); + if (!isValidProof) revert("Invalid Proof"); + } +} diff --git a/src/interfaces/IPermissiveAccount.sol b/src/interfaces/IPermissiveAccount.sol index be712d9..f5b371d 100644 --- a/src/interfaces/IPermissiveAccount.sol +++ b/src/interfaces/IPermissiveAccount.sol @@ -15,23 +15,22 @@ interface IPermissiveAccount is IAccount { error InvalidPermission(); error InvalidPaymaster(address provided, address expected); error InvalidSelector(bytes4 provided, bytes4 expected); - error ExpiredPermission(uint current, uint expiredAt); + error ExpiredPermission(uint256 current, uint256 expiredAt); event OperatorMutated( - address operator, - bytes32 oldPermissions, - bytes32 newPermissions, + address indexed operator, + bytes32 indexed oldPermissions, + bytes32 indexed newPermissions, uint256 maxValue, uint256 maxFee ); - event UserOpValidated(bytes32 indexed userOpHash, UserOperation userOp); event PermissionUsed( bytes32 indexed permHash, address dest, uint256 value, bytes func, - Permission permission, + PermissionLib.Permission permission, uint256 gasFee ); diff --git a/src/interfaces/Permission.sol b/src/interfaces/Permission.sol index ec21680..6abe3d0 100644 --- a/src/interfaces/Permission.sol +++ b/src/interfaces/Permission.sol @@ -2,23 +2,40 @@ pragma solidity ^0.8.18; -struct Permission { - // the operator - address operator; - // the address allowed to interact with - address to; - // the function selector - bytes4 selector; - // specific arguments that are allowed for this permisison (see readme) - bytes allowed_arguments; - // the paymaster if set will pay the transactions - address paymaster; - // the timestamp when the permission isn't valid anymore - // @dev can be 0 if expires_at_block != 0 - uint256 expiresAtUnix; - // the block when the permission isn't valid anymore - // @dev can be 0 if expires_at_unix != 0 - uint256 expiresAtBlock; - // the max number of times + 1 this permision can be used, 0 = infinite - uint256 maxUsage; +library PermissionLib { + struct Permission { + // the operator + address operator; + // the address allowed to interact with + address to; + // the function selector + bytes4 selector; + // specific arguments that are allowed for this permisison (see readme) + bytes allowed_arguments; + // the paymaster if set will pay the transactions + address paymaster; + // the timestamp when the permission isn't valid anymore + // @dev can be 0 if expires_at_block != 0 + uint256 expiresAtUnix; + // the block when the permission isn't valid anymore + // @dev can be 0 if expires_at_unix != 0 + uint256 expiresAtBlock; + // the max number of times + 1 this permision can be used, 0 = infinite + uint256 maxUsage; + } + + function hash(Permission memory permission) internal pure returns (bytes32 permHash) { + permHash = keccak256( + abi.encode( + permission.operator, + permission.to, + permission.selector, + permission.allowed_arguments, + permission.paymaster, + permission.expiresAtUnix, + permission.expiresAtBlock, + permission.maxUsage + ) + ); + } } diff --git a/src/tests/Incrementer.sol b/src/tests/Incrementer.sol index 59ad952..53c7ac5 100644 --- a/src/tests/Incrementer.sol +++ b/src/tests/Incrementer.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.18; contract Incrementer { - uint public value = 0; + uint256 public value = 0; function increment() external { value = value + 1; diff --git a/src/tests/PermissivePaymaster.sol b/src/tests/PermissivePaymaster.sol index 93b56b7..3b3a75d 100644 --- a/src/tests/PermissivePaymaster.sol +++ b/src/tests/PermissivePaymaster.sol @@ -5,9 +5,7 @@ pragma solidity ^0.8.18; import "account-abstraction/samples/TokenPaymaster.sol"; contract PermissivePaymaster is TokenPaymaster { - constructor( - address accountFactory, - string memory _symbol, - IEntryPoint _entryPoint - ) TokenPaymaster(accountFactory, _symbol, _entryPoint) {} + constructor(address accountFactory, string memory _symbol, IEntryPoint _entryPoint) + TokenPaymaster(accountFactory, _symbol, _entryPoint) + {} } diff --git a/test/AllowanceCalldata.t.sol b/test/AllowanceCalldata.t.sol index 43c9ed7..0286e8e 100644 --- a/test/AllowanceCalldata.t.sol +++ b/test/AllowanceCalldata.t.sol @@ -13,14 +13,10 @@ contract AllowanceCalldataTest is Test { function hasNoZeroPrefix(bytes calldata data) internal pure returns (bool) { RLPReader.RLPItem memory RLPAllowed = RLPReader.toRlpItem(data); if (!RLPReader.isList(RLPAllowed)) return false; - RLPReader.RLPItem[] memory allowedArguments = RLPReader.toList( - RLPAllowed - ); - for (uint i = 0; i < allowedArguments.length; i++) { - RLPReader.RLPItem[] memory prefixAndArg = RLPReader.toList( - allowedArguments[i] - ); - uint prefix = RLPReader.toUint(prefixAndArg[0]); + RLPReader.RLPItem[] memory allowedArguments = RLPReader.toList(RLPAllowed); + for (uint256 i = 0; i < allowedArguments.length; i++) { + RLPReader.RLPItem[] memory prefixAndArg = RLPReader.toList(allowedArguments[i]); + uint256 prefix = RLPReader.toUint(prefixAndArg[0]); if (prefix == 0) return false; } return true; @@ -32,16 +28,12 @@ contract AllowanceCalldataTest is Test { AllowanceCalldata.isAllowedCalldata(allowanceData, data); } - function testShouldFailBecauseUnauthorizedAllowanceData( - bytes calldata data - ) public { + function testShouldFailBecauseUnauthorizedAllowanceData(bytes calldata data) public { vm.expectRevert(); AllowanceCalldata.isAllowedCalldata(data, callData); } function testValidCall() public view { - assert( - AllowanceCalldata.isAllowedCalldata(allowanceData, callData) == true - ); + assert(AllowanceCalldata.isAllowedCalldata(allowanceData, callData) == true); } } diff --git a/test/PermissiveAccount.t.sol b/test/PermissiveAccount.t.sol index 12f0a55..56d1574 100644 --- a/test/PermissiveAccount.t.sol +++ b/test/PermissiveAccount.t.sol @@ -20,28 +20,15 @@ struct Permit { } 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) @@ -58,54 +45,35 @@ contract SigUtils { DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR; } - bytes32 public constant TYPEHASH = - 0xcd3966ea44fb027b668c722656f7791caa71de9073b3cbb77585cc6fa97ce82e; + bytes32 public constant TYPEHASH = 0xcd3966ea44fb027b668c722656f7791caa71de9073b3cbb77585cc6fa97ce82e; - function getStructHash( - Permit memory _permit - ) internal pure returns (bytes32) { - return - keccak256( - abi.encode( - TYPEHASH, - _permit.operator, - _permit.merkleRootPermissions, - _permit.maxValue, - _permit.maxFee - ) - ); + function getStructHash(Permit memory _permit) internal pure returns (bytes32) { + return keccak256( + abi.encode(TYPEHASH, _permit.operator, _permit.merkleRootPermissions, _permit.maxValue, _permit.maxFee) + ); } - function getTypedDataHash( - Permit memory _permit - ) public view returns (bytes32) { - return - keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR, - getStructHash(_permit) - ) - ); + function getTypedDataHash(Permit memory _permit) public view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getStructHash(_permit))); } } contract PermissiveAccountTest is Test { using ECDSA for bytes32; + using PermissionLib for PermissionLib.Permission; + PermissiveAccount internal account; EntryPoint internal entrypoint; PermissiveFactory internal factory; Token internal token; - Permission[] internal permissions; + PermissionLib.Permission[] internal permissions; UserOperation[] internal ops; address internal owner = 0xa8b802B27FB4FAD58Ed28Cb6F4Ae5061bD432e8c; - uint internal ownerPrivateKey = - 0x18104766cc86e7fb8a7452ac9fb2bccc465a88a9bba2d2d67a5ffd3f459f820f; + uint256 internal ownerPrivateKey = 0x18104766cc86e7fb8a7452ac9fb2bccc465a88a9bba2d2d67a5ffd3f459f820f; address internal operator = 0xabe1DE8764303a2d4421Ea583ef693CF6cAc109A; - uint internal operatorPrivateKey = - 0x19ef7c79dbd4115a8df3d576ea6e75362d661def86250fd3ef4557a285359776; + uint256 internal operatorPrivateKey = 0x19ef7c79dbd4115a8df3d576ea6e75362d661def86250fd3ef4557a285359776; bytes32[] internal proofs; - uint[] internal numbers; + uint256[] internal numbers; FeeManager internal feeManager; SigUtils internal utils; @@ -116,15 +84,12 @@ contract PermissiveAccountTest is Test { address(entrypoint), payable(address(feeManager)) ); - account = factory.createAccount( - owner, - 0x000000000000000000000000a8b802b27fb4fad58ed28cb6f4ae5061bd432e8c - ); + account = factory.createAccount(owner, 0x000000000000000000000000a8b802b27fb4fad58ed28cb6f4ae5061bd432e8c); token = new Token("USD Coin", "USDC"); token.mint(); token.transfer(address(account), 100 ether); entrypoint.depositTo{value: 0.11 ether}(address(account)); - Permission memory perm = Permission( + PermissionLib.Permission memory perm = PermissionLib.Permission( operator, address(token), token.transfer.selector, @@ -147,36 +112,11 @@ contract PermissiveAccountTest is Test { ); } - function hashPermission( - Permission memory permission - ) internal pure returns (bytes32 permHash) { - permHash = keccak256( - abi.encode( - permission.operator, - permission.to, - permission.selector, - permission.allowed_arguments, - permission.paymaster, - permission.expiresAtUnix, - permission.expiresAtBlock, - permission.maxUsage - ) - ); - } - function testPermissionsGranted() public { - bytes32 root = keccak256(bytes.concat(hashPermission(permissions[0]))); - bytes32 digest = utils.getTypedDataHash( - Permit(operator, root, 0, 1 ether) - ); + bytes32 root = keccak256(bytes.concat(permissions[0].hash())); + bytes32 digest = utils.getTypedDataHash(Permit(operator, root, 0, 1 ether)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions( - operator, - root, - 0, - 1 ether, - abi.encodePacked(r, s, v) - ); + account.setOperatorPermissions(operator, root, 0, 1 ether, abi.encodePacked(r, s, v)); assert(account.remainingFeeForOperator(operator) == 1 ether); assert(account.remainingValueForOperator(operator) == 0); assert(account.operatorPermissions(operator) == root); @@ -185,19 +125,9 @@ contract PermissiveAccountTest is Test { 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"" ); - uint computedFee = account.computeGasFee(op); + uint256 computedFee = account.computeGasFee(op); op.callData = abi.encodeWithSelector( account.execute.selector, address(token), @@ -210,56 +140,31 @@ 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); - uint oldFeeManagerBalance = address(feeManager).balance; + 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 { testTransactionPasses(); Incrementer incr = new Incrementer(); - Permission memory perm = Permission( - operator, - address(incr), - incr.increment.selector, - hex"c0", - address(0), - 1713986312, - 0, - 0 + PermissionLib.Permission memory perm = PermissionLib.Permission( + operator, address(incr), incr.increment.selector, hex"c0", address(0), 1713986312, 0, 0 ); ops.pop(); permissions.pop(); permissions.push(perm); - bytes32 root = keccak256(bytes.concat(hashPermission(perm))); - bytes32 digest = utils.getTypedDataHash( - Permit(operator, root, 0, 0.11 ether) - ); + bytes32 root = keccak256(bytes.concat(perm.hash())); + bytes32 digest = utils.getTypedDataHash(Permit(operator, root, 0, 0.11 ether)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); - account.setOperatorPermissions( - operator, - root, - 0, - 0.11 ether, - abi.encodePacked(r, s, v) - ); + account.setOperatorPermissions(operator, root, 0, 0.11 ether, abi.encodePacked(r, s, v)); UserOperation memory op = UserOperation( address(account), account.getNonce(), @@ -280,7 +185,7 @@ contract PermissiveAccountTest is Test { hex"", hex"" ); - uint computedFee = account.computeGasFee(op); + uint256 computedFee = account.computeGasFee(op); op.callData = abi.encodeWithSelector( account.execute.selector, address(incr), @@ -290,10 +195,8 @@ contract PermissiveAccountTest is Test { 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); diff --git a/test/SafeModule.t.sol b/test/SafeModule.t.sol index 3711eb4..159f0b7 100644 --- a/test/SafeModule.t.sol +++ b/test/SafeModule.t.sol @@ -15,20 +15,20 @@ address constant receiver = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990; contract SafeModuleTest is Test { using ECDSA for bytes32; + using PermissionLib for PermissionLib.Permission; + SafeModule internal account; EntryPoint internal entrypoint; SafeFactory internal factory; Token internal token; - Permission[] internal permissions; + PermissionLib.Permission[] internal permissions; UserOperation[] internal ops; Safe internal owner = new Safe(); - uint internal ownerPrivateKey = - 0x18104766cc86e7fb8a7452ac9fb2bccc465a88a9bba2d2d67a5ffd3f459f820f; + uint256 internal ownerPrivateKey = 0x18104766cc86e7fb8a7452ac9fb2bccc465a88a9bba2d2d67a5ffd3f459f820f; address internal operator = 0xabe1DE8764303a2d4421Ea583ef693CF6cAc109A; - uint internal operatorPrivateKey = - 0x19ef7c79dbd4115a8df3d576ea6e75362d661def86250fd3ef4557a285359776; + uint256 internal operatorPrivateKey = 0x19ef7c79dbd4115a8df3d576ea6e75362d661def86250fd3ef4557a285359776; bytes32[] internal proofs; - uint[] internal numbers; + uint256[] internal numbers; FeeManager internal feeManager; function setUp() public { @@ -38,15 +38,13 @@ contract SafeModuleTest is Test { address(entrypoint), payable(address(feeManager)) ); - account = factory.createAccount( - address(owner), - 0x000000000000000000000000a8b802b27fb4fad58ed28cb6f4ae5061bd432e8c - ); + account = + factory.createAccount(address(owner), 0x000000000000000000000000a8b802b27fb4fad58ed28cb6f4ae5061bd432e8c); token = new Token("USD Coin", "USDC"); token.mint(); token.transfer(address(account), 100 ether); entrypoint.depositTo{value: 0.11 ether}(address(account)); - Permission memory perm = Permission( + PermissionLib.Permission memory perm = PermissionLib.Permission( operator, address(token), token.transfer.selector, @@ -59,25 +57,8 @@ contract SafeModuleTest is Test { permissions.push(perm); } - function hashPermission( - Permission memory permission - ) internal pure returns (bytes32 permHash) { - permHash = keccak256( - abi.encode( - permission.operator, - permission.to, - permission.selector, - permission.allowed_arguments, - permission.paymaster, - permission.expiresAtUnix, - permission.expiresAtBlock, - permission.maxUsage - ) - ); - } - function testPermissionsGranted() public { - bytes32 root = keccak256(bytes.concat(hashPermission(permissions[0]))); + bytes32 root = keccak256(bytes.concat(permissions[0].hash())); vm.prank(address(owner)); account.setOperatorPermissions(operator, root, 0, 1 ether); assert(account.remainingFeeForOperator(operator) == 1 ether); @@ -88,19 +69,9 @@ contract SafeModuleTest is Test { 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"" ); - uint computedFee = account.computeGasFee(op); + uint256 computedFee = account.computeGasFee(op); op.callData = abi.encodeWithSelector( account.execute.selector, address(token), @@ -113,45 +84,28 @@ contract SafeModuleTest 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); - uint oldFeeManagerBalance = address(feeManager).balance; + 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 { testTransactionPasses(); Incrementer incr = new Incrementer(); - Permission memory perm = Permission( - operator, - address(incr), - incr.increment.selector, - hex"c0", - address(0), - 1713986312, - 0, - 0 + PermissionLib.Permission memory perm = PermissionLib.Permission( + operator, address(incr), incr.increment.selector, hex"c0", address(0), 1713986312, 0, 0 ); ops.pop(); permissions.pop(); permissions.push(perm); - bytes32 root = keccak256(bytes.concat(hashPermission(perm))); + bytes32 root = keccak256(bytes.concat(perm.hash())); vm.prank(address(owner)); account.setOperatorPermissions(operator, root, 0, 0.11 ether); UserOperation memory op = UserOperation( @@ -174,7 +128,7 @@ contract SafeModuleTest is Test { hex"", hex"" ); - uint computedFee = account.computeGasFee(op); + uint256 computedFee = account.computeGasFee(op); op.callData = abi.encodeWithSelector( account.execute.selector, address(incr), @@ -184,10 +138,8 @@ contract SafeModuleTest is Test { 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);