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

safe-global / safe-modules / 6407391525

04 Oct 2023 02:23PM UTC coverage: 47.236%. First build
6407391525

Pull #78

github

rmeissner
Set correct pathts in CI
Pull Request #78: Closes #77: Initialize 4337 directory

20 of 58 branches covered (0.0%)

Branch coverage included in aggregate %.

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

74 of 141 relevant lines covered (52.48%)

2.0 hits per line

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

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

4
import "@gnosis.pm/safe-contracts/contracts/handler/HandlerContext.sol";
5
import "./vendor/CompatibilityFallbackHandler.sol";
6
import "./UserOperation.sol";
7
import "./interfaces/Safe.sol";
8

9
/// @title EIP4337Module
10
/// TODO should implement default fallback methods
11
abstract contract EIP4337Module is HandlerContext, CompatibilityFallbackHandler {
12
    using UserOperationLib for UserOperation;
13
    bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");
14

15
    bytes32 private constant SAFE_OP_TYPEHASH =
16
        keccak256(
17
            "SafeOp(address safe,bytes callData,uint256 nonce,uint256 verificationGas,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 callGas,address entryPoint)"
18
        );
19

20
    address public immutable supportedEntryPoint;
21
    bytes4 public immutable expectedExecutionFunctionId;
22

23
    constructor(address entryPoint, bytes4 executionFunctionId) {
24
        supportedEntryPoint = entryPoint;
14✔
25
        expectedExecutionFunctionId = executionFunctionId;
14✔
26
    }
27

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

42
        validateReplayProtection(userOp);
2✔
43

44
        require(expectedExecutionFunctionId == bytes4(userOp.callData), "Unsupported execution function id");
2!
45

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

49
        address entryPoint = _msgSender();
2✔
50
        require(entryPoint == supportedEntryPoint, "Unsupported entry point");
2!
51
        _validateSignatures(entryPoint, userOp);
2✔
52

53
        if (requiredPrefund != 0) {
2✔
54
            Safe(safeAddress).execTransactionFromModule(entryPoint, requiredPrefund, "", 0);
1✔
55
        }
56
        return 0;
2✔
57
    }
58

59
    function validateReplayProtection(UserOperation calldata userOp) internal virtual;
60

61
    function domainSeparator() public view returns (bytes32) {
62
        return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, block.chainid, this));
3✔
63
    }
64

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

102
        return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeOperationHash);
3✔
103
    }
104

105
    function getOperationHash(
106
        address safe,
107
        bytes calldata callData,
108
        uint256 nonce,
109
        uint256 verificationGas,
110
        uint256 preVerificationGas,
111
        uint256 maxFeePerGas,
112
        uint256 maxPriorityFeePerGas,
113
        uint256 callGas,
114
        address entryPoint
115
    ) public view returns (bytes32) {
116
        return
3✔
117
            keccak256(
118
                encodeOperationData(
119
                    safe,
120
                    callData,
121
                    nonce,
122
                    verificationGas,
123
                    preVerificationGas,
124
                    maxFeePerGas,
125
                    maxPriorityFeePerGas,
126
                    callGas,
127
                    entryPoint
128
                )
129
            );
130
    }
131

132
    /// @dev Validates that the user operation is correctly signed. Users methods from Gnosis Safe contract, reverts if signatures are invalid
133
    /// @param entryPoint Address of the entry point
134
    /// @param userOp User operation struct
135
    function _validateSignatures(address entryPoint, UserOperation calldata userOp) internal view {
136
        bytes32 operationHash = getOperationHash(
2✔
137
            payable(userOp.sender),
138
            userOp.callData,
139
            userOp.nonce,
140
            userOp.verificationGasLimit,
141
            userOp.preVerificationGas,
142
            userOp.maxFeePerGas,
143
            userOp.maxPriorityFeePerGas,
144
            userOp.callGasLimit,
145
            entryPoint
146
        );
147

148
        Safe(payable(userOp.sender)).checkSignatures(operationHash, "", userOp.signature);
2✔
149
    }
150
}
151

152
contract Simple4337Module is EIP4337Module {
153
    // NOTE There is a change proposed to EIP-4337 to move nonce tracking to the entrypoint
154
    mapping(address => mapping(bytes32 => uint64)) private nonces;
155

156
    constructor(address entryPoint)
157
        EIP4337Module(entryPoint, bytes4(keccak256("execTransactionFromModule(address,uint256,bytes,uint8)")))
158
    {}
159

160
    function validateReplayProtection(UserOperation calldata userOp) internal override {
161
        // We need to increase the nonce to make it impossible to drain the safe by making it send prefunds for the same transaction
162
        // Right shifting fills up with 0s from the left
163
        bytes32 key = bytes32(userOp.nonce >> 64);
2✔
164
        uint64 safeNonce = nonces[userOp.sender][key];
2✔
165
        nonces[userOp.sender][key]++;
2✔
166

167
        // Casting to uint64 to remove the key segment
168
        require(safeNonce == uint64(userOp.nonce), "Invalid Nonce");
2!
169
    }
170
}
171

172
contract DoubleCheck4337Module is EIP4337Module {
173
    bytes32 private constant SAFE_4337_EXECUTION_TYPEHASH =
174
        keccak256("Safe4337Execution(address safe,address target,uint256 value,bytes calldata data,uint8 operation,uint256 nonce)");
175

176
    struct ExecutionStatus {
177
        bool approved;
178
        bool executed;
179
    }
180

181
    mapping(address => mapping(bytes32 => ExecutionStatus)) private hashes;
182

183
    constructor(address entryPoint)
184
        EIP4337Module(entryPoint, bytes4(keccak256("checkAndExecTransaction(address,address,uint256,bytes,uint8,uint256)")))
185
    {}
186

187
    function encodeSafeExecutionData(
188
        address safe,
189
        address target,
190
        uint256 value,
191
        bytes memory data,
192
        uint8 operation,
193
        uint256 nonce
194
    ) public view returns (bytes memory) {
195
        bytes32 safeExecutionTypeData = keccak256(
×
196
            abi.encode(SAFE_4337_EXECUTION_TYPEHASH, safe, target, value, keccak256(data), operation, nonce)
197
        );
198

199
        return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeExecutionTypeData);
×
200
    }
201

202
    function validateReplayProtection(UserOperation calldata userOp) internal override {
203
        (address safe, address target, uint256 value, bytes memory data, uint8 operation, uint256 nonce) = abi.decode(
×
204
            userOp.callData,
205
            (address, address, uint256, bytes, uint8, uint256)
206
        );
207
        bytes32 executionHash = keccak256(encodeSafeExecutionData(safe, target, value, data, operation, nonce));
×
208
        require(userOp.sender == safe, "Unexpected Safe in calldata");
×
209
        require(userOp.nonce == nonce, "Unexpected nonce in calldata");
×
210
        ExecutionStatus memory status = hashes[userOp.sender][executionHash];
×
211
        require(!status.approved && !status.executed, "Unexpected status");
×
212
        hashes[userOp.sender][executionHash].approved = true;
×
213
    }
214

215
    function checkAndExecTransactionFromModule(
216
        address safe,
217
        address target,
218
        uint256 value,
219
        bytes calldata data,
220
        uint8 operation,
221
        uint256 nonce
222
    ) external {
223
        bytes32 executionHash = keccak256(encodeSafeExecutionData(safe, target, value, data, operation, nonce));
×
224
        ExecutionStatus memory status = hashes[safe][executionHash];
×
225
        require(status.approved && !status.executed, "Unexpected status");
×
226
        hashes[safe][executionHash].executed = true;
×
227
        Safe(safe).execTransactionFromModule(target, value, data, operation);
×
228
    }
229
}
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