-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathArbitraryCallsProposal.sol
257 lines (243 loc) · 10.3 KB
/
ArbitraryCallsProposal.sol
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;
import "../tokens/IERC721.sol";
import "../tokens/IERC721Receiver.sol";
import "../tokens/ERC1155Receiver.sol";
import "../utils/LibSafeERC721.sol";
import "../utils/LibAddress.sol";
import "../vendor/markets/IReserveAuctionCoreEth.sol";
import "./vendor/IOpenseaExchange.sol";
import "./LibProposal.sol";
import "./IProposalExecutionEngine.sol";
// Implements arbitrary call proposals. Inherited by the `ProposalExecutionEngine`.
// This contract will be delegatecall'ed into by `Party` proxy instances.
contract ArbitraryCallsProposal {
using LibSafeERC721 for IERC721;
using LibAddress for address payable;
struct ArbitraryCall {
// The call target.
address payable target;
// Amount of ETH to attach to the call.
uint256 value;
// Calldata.
bytes data;
// Hash of the successful return data of the call.
// If 0x0, no return data checking will occur for this call.
bytes32 expectedResultHash;
}
error PreciousLostError(IERC721 token, uint256 tokenId);
error CallProhibitedError(address target, bytes data);
error ArbitraryCallFailedError(bytes revertData);
error UnexpectedCallResultHashError(
uint256 idx,
bytes32 resultHash,
bytes32 expectedResultHash
);
error NotEnoughEthAttachedError(uint256 callValue, uint256 ethAvailable);
error InvalidApprovalCallLength(uint256 callDataLength);
event ArbitraryCallExecuted(uint256 proposalId, uint256 idx, uint256 count);
IReserveAuctionCoreEth private immutable _ZORA;
constructor(IReserveAuctionCoreEth zora) {
_ZORA = zora;
}
function _executeArbitraryCalls(
IProposalExecutionEngine.ExecuteProposalParams memory params,
bool allowArbCallsToSpendPartyEth
) internal returns (bytes memory nextProgressData) {
// Get the calls to execute.
ArbitraryCall[] memory calls = abi.decode(params.proposalData, (ArbitraryCall[]));
// Check whether the proposal was unanimously passed.
bool isUnanimous = params.flags & LibProposal.PROPOSAL_FLAG_UNANIMOUS ==
LibProposal.PROPOSAL_FLAG_UNANIMOUS;
// If not unanimous, keep track of which preciouses we had before the calls
// so we can check that we still have them later.
bool[] memory hadPreciouses = new bool[](params.preciousTokenIds.length);
if (!isUnanimous) {
for (uint256 i; i < hadPreciouses.length; ++i) {
hadPreciouses[i] = _getHasPrecious(
params.preciousTokens[i],
params.preciousTokenIds[i]
);
}
}
// If we're not allowing arbitrary calls to spend the Party's ETH, only
// allow forwarded ETH attached to the call to be spent.
uint256 ethAvailable = allowArbCallsToSpendPartyEth ? address(this).balance : msg.value;
for (uint256 i; i < calls.length; ++i) {
// Execute an arbitrary call.
_executeSingleArbitraryCall(
i,
calls,
params.preciousTokens,
params.preciousTokenIds,
isUnanimous,
ethAvailable
);
// Update the amount of ETH available for the subsequent calls.
ethAvailable = allowArbCallsToSpendPartyEth
? address(this).balance
: ethAvailable - calls[i].value;
emit ArbitraryCallExecuted(params.proposalId, i, calls.length);
}
// If not a unanimous vote and we had a precious beforehand,
// ensure that we still have it now.
if (!isUnanimous) {
for (uint256 i; i < hadPreciouses.length; ++i) {
if (hadPreciouses[i]) {
if (!_getHasPrecious(params.preciousTokens[i], params.preciousTokenIds[i])) {
revert PreciousLostError(
params.preciousTokens[i],
params.preciousTokenIds[i]
);
}
}
}
}
// Refund leftover ETH attached to the call if none was spent from the
// Party's balance.
if (!allowArbCallsToSpendPartyEth && ethAvailable > 0) {
payable(msg.sender).transferEth(ethAvailable);
}
// No next step, so no progressData.
return "";
}
function _executeSingleArbitraryCall(
uint256 idx,
ArbitraryCall[] memory calls,
IERC721[] memory preciousTokens,
uint256[] memory preciousTokenIds,
bool isUnanimous,
uint256 ethAvailable
) private {
ArbitraryCall memory call = calls[idx];
// Check that the call is not prohibited.
if (
!_isCallAllowed(call, isUnanimous, idx, calls.length, preciousTokens, preciousTokenIds)
) {
revert CallProhibitedError(call.target, call.data);
}
// Check that we have enough ETH to execute the call.
if (ethAvailable < call.value) {
revert NotEnoughEthAttachedError(call.value, ethAvailable);
}
// Execute the call.
(bool s, bytes memory r) = call.target.call{ value: call.value }(call.data);
if (!s) {
// Call failed. If not optional, revert.
revert ArbitraryCallFailedError(r);
} else {
// Call succeeded.
// If we have a nonzero expectedResultHash, check that the result data
// from the call has a matching hash.
if (call.expectedResultHash != bytes32(0)) {
bytes32 resultHash = keccak256(r);
if (resultHash != call.expectedResultHash) {
revert UnexpectedCallResultHashError(idx, resultHash, call.expectedResultHash);
}
}
}
}
// Do we possess the precious?
function _getHasPrecious(
IERC721 preciousToken,
uint256 preciousTokenId
) private view returns (bool hasPrecious) {
hasPrecious = preciousToken.safeOwnerOf(preciousTokenId) == address(this);
}
function _isCallAllowed(
ArbitraryCall memory call,
bool isUnanimous,
uint256 callIndex,
uint256 callsCount,
IERC721[] memory preciousTokens,
uint256[] memory preciousTokenIds
) private view returns (bool isAllowed) {
// Cannot call ourselves.
if (call.target == address(this)) {
return false;
}
if (call.data.length >= 4) {
// Get the function selector of the call (first 4 bytes of calldata).
bytes4 selector;
{
bytes memory callData = call.data;
assembly {
selector := and(
mload(add(callData, 32)),
0xffffffff00000000000000000000000000000000000000000000000000000000
)
}
}
// Non-unanimous proposals restrict what ways some functions can be
// called on a precious token.
if (!isUnanimous) {
// Cannot call `approve()` or `setApprovalForAll()` on the precious
// unless it's to revoke approvals.
if (selector == IERC721.approve.selector) {
// Can only call `approve()` on the precious if the operator is null.
(address op, uint256 tokenId) = _decodeApproveCallDataArgs(call.data);
if (op != address(0)) {
return
!LibProposal.isTokenIdPrecious(
IERC721(call.target),
tokenId,
preciousTokens,
preciousTokenIds
);
}
// Can only call `setApprovalForAll()` on the precious if
// toggling off.
} else if (selector == IERC721.setApprovalForAll.selector) {
(, bool isApproved) = _decodeSetApprovalForAllCallDataArgs(call.data);
if (isApproved) {
return !LibProposal.isTokenPrecious(IERC721(call.target), preciousTokens);
}
// Can only call cancelAuction on the zora AH if it's the last call
// in the sequence.
} else if (selector == IReserveAuctionCoreEth.cancelAuction.selector) {
if (call.target == address(_ZORA)) {
return callIndex + 1 == callsCount;
}
}
}
// Can never call receive hooks on any target.
if (
selector == IERC721Receiver.onERC721Received.selector ||
selector == ERC1155TokenReceiverBase.onERC1155Received.selector ||
selector == ERC1155TokenReceiverBase.onERC1155BatchReceived.selector
) {
return false;
}
// Disallow calling `validate()` on Seaport if there are preciouses.
if (selector == IOpenseaExchange.validate.selector && preciousTokens.length != 0) {
return false;
}
}
// All other calls are allowed.
return true;
}
// Get the `operator` and `tokenId` from the `approve()` call data.
function _decodeApproveCallDataArgs(
bytes memory callData
) private pure returns (address operator, uint256 tokenId) {
if (callData.length < 68) {
revert InvalidApprovalCallLength(callData.length);
}
assembly {
operator := and(mload(add(callData, 36)), 0xffffffffffffffffffffffffffffffffffffffff)
tokenId := mload(add(callData, 68))
}
}
// Get the `operator` and `tokenId` from the `setApprovalForAll()` call data.
function _decodeSetApprovalForAllCallDataArgs(
bytes memory callData
) private pure returns (address operator, bool isApproved) {
if (callData.length < 68) {
revert InvalidApprovalCallLength(callData.length);
}
assembly {
operator := and(mload(add(callData, 36)), 0xffffffffffffffffffffffffffffffffffffffff)
isApproved := xor(iszero(mload(add(callData, 68))), 1)
}
}
}