• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-modules / 6444259509

08 Oct 2023 12:13AM UTC coverage: 71.605% (-0.5%) from 72.152%
6444259509

Pull #90

github

rmeissner
Make compatible with stackup
Pull Request #90: Closes #83: Add integration tests

27 of 50 branches covered (0.0%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 2 files covered. (100.0%)

89 of 112 relevant lines covered (79.46%)

3.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.86
/4337/contracts/EIP4337Module.sol
1
// SPDX-License-Identifier: LGPL-3.0-only
2
pragma solidity >=0.8.0 <0.9.0;
3

4
import {HandlerContext} from "@safe-global/safe-contracts/contracts/handler/HandlerContext.sol";
5
import {CompatibilityFallbackHandler} from "@safe-global/safe-contracts/contracts/handler/CompatibilityFallbackHandler.sol";
6
import {UserOperation, UserOperationLib} from "./UserOperation.sol";
7
import {INonceManager} from "./interfaces/ERC4337.sol";
8
import {ISafe} from "./interfaces/Safe.sol";
9

10
/// @title EIP4337Module
11
abstract contract EIP4337Module is HandlerContext, CompatibilityFallbackHandler {
12
    using UserOperationLib for UserOperation;
13

14
    //return value in case of signature failure, with no time-range.
15
    // equivalent to _packValidationData(true,0,0);
16
    uint256 internal constant SIG_VALIDATION_FAILED = 1;
17

18
    bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");
19

20
    bytes32 private constant SAFE_OP_TYPEHASH =
21
        keccak256(
22
            "SafeOp(address safe,bytes callData,uint256 nonce,uint256 preVerificationGas,uint256 verificationGasLimit,uint256 callGasLimit,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,address entryPoint)"
23
        );
24

25
    address public immutable supportedEntryPoint;
26
    bytes4 public immutable expectedExecutionFunctionId;
27

28
    constructor(address entryPoint, bytes4 executionFunctionId) {
29
        supportedEntryPoint = entryPoint;
14✔
30
        expectedExecutionFunctionId = executionFunctionId;
14✔
31
    }
32

33
    /// @dev Validates user operation provided by the entry point
34
    /// @param userOp User operation struct
35
    /// @param requiredPrefund Required prefund to execute the operation
36
    function validateUserOp(
37
        UserOperation calldata userOp,
38
        bytes32,
39
        uint256 requiredPrefund
40
    ) external returns (uint256 validationResult) {
41
        address payable safeAddress = payable(userOp.sender);
4✔
42
        // The entryPoint address is appended to the calldata in `HandlerContext` contract
43
        // Because of this, the relayer may manipulate the entryPoint address, therefore we have to verify that
44
        // the sender is the Safe specified in the userOperation
45
        require(safeAddress == msg.sender, "Invalid Caller");
4!
46

47
        validateReplayProtection(userOp);
4✔
48

49
        require(expectedExecutionFunctionId == bytes4(userOp.callData), "Unsupported execution function id");
4!
50

51
        // We need to make sure that the entryPoint's requested prefund is in bounds
52
        require(requiredPrefund <= userOp.requiredPreFund(), "Prefund too high");
4!
53

54
        address entryPoint = _msgSender();
4✔
55
        require(entryPoint == supportedEntryPoint, "Unsupported entry point");
4!
56
        validationResult = validateSignatures(entryPoint, userOp);
4✔
57

58
        // We need to perform this even if the signature is not valid, else the simulation function of the Entrypoint will not work
59
        if (requiredPrefund != 0) {
4✔
60
            ISafe(safeAddress).execTransactionFromModule(entryPoint, requiredPrefund, "", 0);
2✔
61
        }
62
    }
63

64
    function validateReplayProtection(UserOperation calldata userOp) internal virtual;
65

66
    function domainSeparator() public view returns (bytes32) {
67
        return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, block.chainid, this));
5✔
68
    }
69

70
    /// @dev Returns the bytes that are hashed to be signed by owners.
71
    /// @param safe Safe address
72
    /// @param callData Call data
73
    /// @param nonce Nonce of the operation
74
    /// @param preVerificationGas Gas required for pre-verification (e.g. for EOA signature verification)
75
    /// @param verificationGasLimit Gas required for verification
76
    /// @param callGasLimit Gas available during the execution of the call
77
    /// @param maxFeePerGas Max fee per gas
78
    /// @param maxPriorityFeePerGas Max priority fee per gas
79
    /// @param entryPoint Address of the entry point
80
    /// @return Operation hash bytes
81
    function encodeOperationData(
82
        address safe,
83
        bytes calldata callData,
84
        uint256 nonce,
85
        uint256 preVerificationGas,
86
        uint256 verificationGasLimit,
87
        uint256 callGasLimit,
88
        uint256 maxFeePerGas,
89
        uint256 maxPriorityFeePerGas,
90
        address entryPoint
91
    ) public view returns (bytes memory) {
92
        bytes32 safeOperationHash = keccak256(
5✔
93
            abi.encode(
94
                SAFE_OP_TYPEHASH,
95
                safe,
96
                keccak256(callData),
97
                nonce,
98
                preVerificationGas,
99
                verificationGasLimit,
100
                callGasLimit,
101
                maxFeePerGas,
102
                maxPriorityFeePerGas,
103
                entryPoint
104
            )
105
        );
106

107
        return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeOperationHash);
5✔
108
    }
109

110
    function getOperationHash(
111
        address safe,
112
        bytes calldata callData,
113
        uint256 nonce,
114
        uint256 preVerificationGas,
115
        uint256 verificationGasLimit,
116
        uint256 callGasLimit,
117
        uint256 maxFeePerGas,
118
        uint256 maxPriorityFeePerGas,
119
        address entryPoint
120
    ) public view returns (bytes32) {
121
        return
5✔
122
            keccak256(
123
                encodeOperationData(
124
                    safe,
125
                    callData,
126
                    nonce,
127
                    preVerificationGas,
128
                    verificationGasLimit,
129
                    callGasLimit,
130
                    maxFeePerGas,
131
                    maxPriorityFeePerGas,
132
                    entryPoint
133
                )
134
            );
135
    }
136

137
    /// @dev Validates that the user operation is correctly signed. Users methods from Safe contract, reverts if signatures are invalid
138
    /// @param entryPoint Address of the entry point
139
    /// @param userOp User operation struct
140
    function validateSignatures(address entryPoint, UserOperation calldata userOp) internal view returns (uint256) {
141
        bytes32 operationHash = getOperationHash(
4✔
142
            payable(userOp.sender),
143
            userOp.callData,
144
            userOp.nonce,
145
            userOp.preVerificationGas,
146
            userOp.verificationGasLimit,
147
            userOp.callGasLimit,
148
            userOp.maxFeePerGas,
149
            userOp.maxPriorityFeePerGas,
150
            entryPoint
151
        );
152

153
        try ISafe(payable(userOp.sender)).checkSignatures(operationHash, "", userOp.signature) {
4✔
154
            return 0;
4✔
155
        } catch {
156
            return SIG_VALIDATION_FAILED;
×
157
        }
158
    }
159
}
160

161
contract Simple4337Module is EIP4337Module {
162
    constructor(address entryPoint)
163
        EIP4337Module(entryPoint, bytes4(keccak256("execTransactionFromModule(address,uint256,bytes,uint8)")))
164
    {}
165

166
    function validateReplayProtection(UserOperation calldata userOp) internal override {
167
        // The entrypoints handles the increase of the nonce
168
        // Right shifting fills up with 0s from the left
169
        uint192 key = uint192(userOp.nonce >> 64);
4✔
170
        uint256 safeNonce = INonceManager(supportedEntryPoint).getNonce(userOp.sender, key);
4✔
171

172
        // Check returned nonce against the user operation nonce
173
        require(safeNonce == userOp.nonce, "Invalid Nonce");
4!
174
    }
175
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc