forked from OpenZeppelin/openzeppelin-contracts
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathdraft-AccountERC7579Hooked.sol
More file actions
171 lines (147 loc) · 7.19 KB
/
draft-AccountERC7579Hooked.sol
File metadata and controls
171 lines (147 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.6.0) (account/extensions/draft-AccountERC7579Hooked.sol)
pragma solidity ^0.8.26;
import {IERC7579Hook, MODULE_TYPE_HOOK} from "../../interfaces/draft-IERC7579.sol";
import {ERC7579Utils, Mode} from "../../account/utils/draft-ERC7579Utils.sol";
import {AccountERC7579} from "./draft-AccountERC7579.sol";
import {Bytes} from "../../utils/Bytes.sol";
import {LowLevelCall} from "../../utils/LowLevelCall.sol";
/**
* @dev Extension of {AccountERC7579} with support for a single hook module (type 4).
*
* If installed, this extension will call the hook module's {IERC7579Hook-preCheck} before executing any operation
* with {_execute} (including {execute} and {executeFromExecutor} by default) and {IERC7579Hook-postCheck} thereafter.
*
* NOTE: Hook modules break the check-effect-interaction pattern. In particular, the {IERC7579Hook-preCheck} hook can
* lead to potentially dangerous reentrancy. Using the `withHook()` modifier is safe if no effect is performed
* before the preHook or after the postHook. That is the case on all functions here, but it may not be the case if
* functions that have this modifier are overridden. Developers should be extremely careful when implementing hook
* modules or further overriding functions that involve hooks.
*/
abstract contract AccountERC7579Hooked is AccountERC7579 {
address private _hook;
/// @dev A hook module is already present. This contract only supports one hook module.
error ERC7579HookModuleAlreadyPresent(address hook);
/**
* @dev Calls {IERC7579Hook-preCheck} before executing the modified function and {IERC7579Hook-postCheck}
* thereafter.
*/
modifier withHook() {
address hook_ = hook();
bytes memory hookData;
// slither-disable-next-line reentrancy-no-eth
if (hook_ != address(0)) hookData = IERC7579Hook(hook_).preCheck(msg.sender, msg.value, msg.data);
_;
if (hook_ != address(0)) IERC7579Hook(hook_).postCheck(hookData);
}
/// @inheritdoc AccountERC7579
function accountId() public view virtual override returns (string memory) {
// vendorname.accountname.semver
return "@openzeppelin/contracts.AccountERC7579Hooked.v1.0.0";
}
/// @dev Returns the hook module address if installed, or `address(0)` otherwise.
function hook() public view virtual returns (address) {
return _hook;
}
/// @dev Supports hook modules. See {AccountERC7579-supportsModule}
function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) {
return moduleTypeId == MODULE_TYPE_HOOK || super.supportsModule(moduleTypeId);
}
/// @inheritdoc AccountERC7579
function isModuleInstalled(
uint256 moduleTypeId,
address module,
bytes calldata data
) public view virtual override returns (bool) {
return
(moduleTypeId == MODULE_TYPE_HOOK && module == hook()) ||
super.isModuleInstalled(moduleTypeId, module, data);
}
/// @dev Installs a module with support for hook modules. See {AccountERC7579-_installModule}
function _installModule(
uint256 moduleTypeId,
address module,
bytes memory initData
) internal virtual override withHook {
if (moduleTypeId == MODULE_TYPE_HOOK) {
require(_hook == address(0), ERC7579HookModuleAlreadyPresent(_hook));
_hook = module;
}
super._installModule(moduleTypeId, module, initData);
}
/// @dev Uninstalls a module with support for hook modules. See {AccountERC7579-_uninstallModule}
function _uninstallModule(uint256 moduleTypeId, address module, bytes memory deInitData) internal virtual override {
// Inline a variant of the `withHook` modifier that doesn't revert if the hook reverts and the moduleTypeId is `MODULE_TYPE_HOOK`.
// === Beginning of the precheck ===
address hook_ = hook();
bytes memory hookData;
bool preCheckSuccess;
// slither-disable-next-line reentrancy-no-eth
if (hook_ != address(0)) {
preCheckSuccess = LowLevelCall.callNoReturn(
hook_,
abi.encodeCall(IERC7579Hook.preCheck, (msg.sender, msg.value, msg.data))
);
if (preCheckSuccess) {
// Note: abi.decode could revert, and we wouldn't be able to catch it.
// If could be leveraged by a malicious hook to force a revert.
// So we have to do the decode manually.
(preCheckSuccess, hookData) = _tryInPlaceAbiDecodeBytes(LowLevelCall.returnData());
} else if (moduleTypeId != MODULE_TYPE_HOOK) {
LowLevelCall.bubbleRevert();
}
}
// === End of the precheck -- Beginning of the body (`_` part of the modifier) ===
if (moduleTypeId == MODULE_TYPE_HOOK) {
require(_hook == module, ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
_hook = address(0);
}
super._uninstallModule(moduleTypeId, module, deInitData);
// === End of the body (`_` part of the modifier) -- Beginning of the postcheck ===
if (hook_ != address(0) && preCheckSuccess) {
bool postCheckSuccess = LowLevelCall.callNoReturn(
hook_,
abi.encodeCall(IERC7579Hook.postCheck, (hookData))
);
if (!postCheckSuccess && moduleTypeId != MODULE_TYPE_HOOK) {
LowLevelCall.bubbleRevert();
}
}
// === End of the postcheck ===
}
/// @dev Hooked version of {AccountERC7579-_execute}.
function _execute(
Mode mode,
bytes calldata executionCalldata
) internal virtual override withHook returns (bytes[] memory) {
return super._execute(mode, executionCalldata);
}
/// @dev Hooked version of {AccountERC7579-_fallback}.
function _fallback() internal virtual override withHook returns (bytes memory) {
return super._fallback();
}
/**
* @dev Try to abi.decode a bytes array. If successful, the decoding is done in place, overriding the original
* data. If decoding fails, the original data is left untouched.
*/
function _tryInPlaceAbiDecodeBytes(
bytes memory data
) private pure returns (bool success, bytes memory passthrough) {
unchecked {
if (data.length < 0x20) return (false, data);
uint256 offset = uint256(_unsafeReadBytesOffset(data, 0));
if (data.length - 0x20 < offset) return (false, data);
uint256 length = uint256(_unsafeReadBytesOffset(data, offset));
if (data.length - 0x20 - offset < length) return (false, data);
Bytes.splice(data, 0x20 + offset, 0x20 + offset + length);
return (true, data);
}
}
/// @dev Copied from Bytes.sol
function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) {
// This is not memory safe in the general case, but all calls to this private function are within bounds.
assembly ("memory-safe") {
value := mload(add(add(buffer, 0x20), offset))
}
}
}