-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathMultiTokenPermit.sol
More file actions
292 lines (261 loc) · 11.7 KB
/
MultiTokenPermit.sol
File metadata and controls
292 lines (261 loc) · 11.7 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
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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { IMultiTokenPermit } from "./interfaces/IMultiTokenPermit.sol";
import { PermitBase } from "./PermitBase.sol";
/**
* @title MultiTokenPermit
* @notice Multi-token support (ERC20, ERC721, ERC1155) for the Permit3 system
* @dev Extends PermitBase with NFT and semi-fungible token functionality
*/
abstract contract MultiTokenPermit is PermitBase, IMultiTokenPermit {
/**
* @notice Query multi-token allowance for a specific token ID
* @param owner Token owner
* @param token Token contract address
* @param spender Approved spender
* @param tokenId Token ID (specific ID for NFT/ERC1155)
* @return amount Approved amount (max uint160 for unlimited)
* @return expiration Timestamp when approval expires (0 for no expiration)
* @return timestamp Timestamp when approval was set
*/
function allowance(
address owner,
address token,
address spender,
uint256 tokenId
) external view override returns (uint160 amount, uint48 expiration, uint48 timestamp) {
bytes32 tokenKey = _getTokenKey(token, tokenId);
Allowance memory allowed = allowances[owner][tokenKey][spender];
return (allowed.amount, allowed.expiration, allowed.timestamp);
}
/**
* @notice Approve a spender for a specific token or collection
* @param token Token contract address
* @param spender Address to approve
* @param tokenId Token ID (specific ID for NFT/ERC1155)
* @param amount Amount to approve (ignored for ERC721, used for ERC20/ERC1155)
* @param expiration Timestamp when approval expires (0 for no expiration)
*/
function approve(
address token,
address spender,
uint256 tokenId,
uint160 amount,
uint48 expiration
) external override {
bytes32 tokenKey = _getTokenKey(token, tokenId);
// Use the same validation as PermitBase
_validateApproval(msg.sender, tokenKey, token, spender, expiration);
// Update the allowance
allowances[msg.sender][tokenKey][spender] =
Allowance({ amount: amount, expiration: expiration, timestamp: uint48(block.timestamp) });
// Emit event with tokenId for better transparency
emit ApprovalWithTokenId(msg.sender, token, spender, tokenId, amount, expiration);
}
/**
* @notice Execute approved ERC721 token transfer
* @dev Uses a dual-allowance system: first checks for specific token ID approval,
* then falls back to collection-wide approval.
* This allows users to either approve individual NFTs or entire collections.
* @param from Token owner address
* @param to Transfer recipient address
* @param token ERC721 contract address
* @param tokenId The unique NFT token ID to transfer
*/
function transferFromERC721(
address from,
address to,
address token,
uint256 tokenId
) public override {
// Check and update dual-allowance
_updateDualAllowance(from, token, tokenId, 1);
// Execute the ERC721 transfer
IERC721(token).safeTransferFrom(from, to, tokenId);
}
/**
* @notice Execute approved ERC1155 token transfer
* @dev Uses a dual-allowance system: first checks for specific token ID approval,
* then falls back to collection-wide approval.
* This allows users to either approve individual token types or entire collections.
* @param from Token owner address
* @param to Transfer recipient address
* @param token ERC1155 contract address
* @param tokenId The specific ERC1155 token ID to transfer
* @param amount Number of tokens to transfer
*/
function transferFromERC1155(
address from,
address to,
address token,
uint256 tokenId,
uint160 amount
) public override {
// Check and update dual-allowance
_updateDualAllowance(from, token, tokenId, amount);
// Execute the ERC1155 transfer
IERC1155(token).safeTransferFrom(from, to, tokenId, amount, "");
}
/**
* @notice Execute multiple approved ERC721 transfers in a single transaction
* @dev Each transfer uses the dual-allowance system independently
* @param transfers Array of ERC721 transfer instructions
*/
function batchTransferERC721(
ERC721Transfer[] calldata transfers
) external override {
uint256 transfersLength = transfers.length;
if (transfersLength == 0) {
revert EmptyArray();
}
for (uint256 i = 0; i < transfersLength; i++) {
transferFromERC721(transfers[i].from, transfers[i].to, transfers[i].token, transfers[i].tokenId);
}
}
/**
* @notice Execute multiple approved ERC1155 transfers in a single transaction
* @dev Each transfer uses the dual-allowance system independently
* @param transfers Array of multi-token transfer instructions
*/
function batchTransferERC1155(
TokenTransfer[] calldata transfers
) external override {
uint256 transfersLength = transfers.length;
if (transfersLength == 0) {
revert EmptyArray();
}
for (uint256 i = 0; i < transfersLength; i++) {
transferFromERC1155(
transfers[i].from, transfers[i].to, transfers[i].token, transfers[i].tokenId, transfers[i].amount
);
}
}
/**
* @notice Execute approved ERC1155 batch transfer for multiple token IDs to a single recipient
* @dev Checks allowances for all token IDs first, then executes batch transfer
* @param transfer Batch transfer details containing arrays of token IDs and amounts
*/
function batchTransferERC1155(
ERC1155BatchTransfer calldata transfer
) external override {
uint256 tokenIdsLength = transfer.tokenIds.length;
if (tokenIdsLength == 0) {
revert EmptyArray();
}
if (tokenIdsLength != transfer.amounts.length) {
revert InvalidArrayLength();
}
// Check and update allowances for all token IDs
for (uint256 i = 0; i < tokenIdsLength; i++) {
_updateDualAllowance(transfer.from, transfer.token, transfer.tokenIds[i], uint160(transfer.amounts[i]));
}
// Execute the batch transfer after all allowances are verified
IERC1155(transfer.token)
.safeBatchTransferFrom(transfer.from, transfer.to, transfer.tokenIds, transfer.amounts, "");
}
/**
* @notice Execute multiple token transfers of any type in a single transaction
* @dev Routes each transfer to the appropriate function based on explicit token type.
* Note: This function uses explicit TokenStandard enum to provide unambiguous routing for mixed-type batches.
* @param transfers Array of multi-token transfer instructions with explicit token types
*/
function batchTransferMultiToken(
TokenTypeTransfer[] calldata transfers
) external override {
uint256 transfersLength = transfers.length;
if (transfersLength == 0) {
revert EmptyArray();
}
for (uint256 i = 0; i < transfersLength; i++) {
TokenTypeTransfer calldata typeTransfer = transfers[i];
TokenTransfer calldata transfer = typeTransfer.transfer;
if (typeTransfer.tokenType == TokenStandard.ERC20) {
// ERC20: Use amount field, tokenId is ignored
PermitBase.transferFrom(transfer.from, transfer.to, transfer.amount, transfer.token);
} else if (typeTransfer.tokenType == TokenStandard.ERC721) {
// ERC721: Use tokenId field, amount must be 1
require(transfer.amount == 1, InvalidAmount(transfer.amount));
// Check and update dual-allowance
_updateDualAllowance(transfer.from, transfer.token, transfer.tokenId, 1);
// Execute the ERC721 transfer
IERC721(transfer.token).safeTransferFrom(transfer.from, transfer.to, transfer.tokenId);
} else if (typeTransfer.tokenType == TokenStandard.ERC1155) {
// ERC1155: Use both tokenId and amount
// Check and update dual-allowance
_updateDualAllowance(transfer.from, transfer.token, transfer.tokenId, transfer.amount);
// Execute the ERC1155 transfer
IERC1155(transfer.token)
.safeTransferFrom(transfer.from, transfer.to, transfer.tokenId, transfer.amount, "");
}
}
}
/**
* @dev Internal helper to get the storage key for a token/tokenId pair
* @param token Token contract address
* @param tokenId Token ID (specific ID for NFT/ERC1155)
* @return Storage key for allowance mapping
*/
function _getTokenKey(
address token,
uint256 tokenId
) internal pure returns (bytes32) {
// Hash token and tokenId together to ensure unique keys
return keccak256(abi.encodePacked(token, tokenId));
}
/**
* @dev Internal helper to check and update dual-allowance (per-token and collection-wide)
* @param from The address to transfer from
* @param token The token contract address
* @param tokenId The specific token ID
* @param amount The amount to transfer (1 for ERC721, variable for ERC1155)
*/
function _updateDualAllowance(
address from,
address token,
uint256 tokenId,
uint160 amount
) internal {
bytes32 encodedId = _getTokenKey(token, tokenId);
// First, try to update allowance for the specific token ID
(, bytes memory revertDataPerId) = _updateAllowance(from, encodedId, msg.sender, amount);
if (revertDataPerId.length > 0) {
// Fallback: if no specific token approval exists, check for collection-wide approval
// Collection-wide approval uses the token address as the key
bytes32 collectionKey = bytes32(uint256(uint160(token)));
(, bytes memory revertDataWildcard) = _updateAllowance(from, collectionKey, msg.sender, amount);
_handleAllowanceError(revertDataPerId, revertDataWildcard);
} else {
// Specific tokenId approval succeeded - verify collection isn't locked
// This prevents bypassing the lockdown mechanism with specific token approvals
bytes32 collectionKey = bytes32(uint256(uint160(token)));
Allowance memory collectionAllowance = allowances[from][collectionKey][msg.sender];
if (collectionAllowance.expiration == LOCKED_ALLOWANCE) {
revert CollectionLocked(from, token, msg.sender);
}
}
}
/**
* @dev Internal helper to handle allowance errors with priority logic
* @param revertDataPerId Revert data from specific token ID allowance check
* @param revertDataWildcard Revert data from collection-wide allowance check
*/
function _handleAllowanceError(
bytes memory revertDataPerId,
bytes memory revertDataWildcard
) internal pure {
if (revertDataPerId.length == 0 || revertDataWildcard.length == 0) {
// If any allowance succeeded, no error to handle
return;
}
bytes4 perIdSelector = bytes4(revertDataPerId);
// Priority error handling: show collection-wide error for insufficient allowance,
// otherwise show the more specific per-token error
if (perIdSelector == InsufficientAllowance.selector) {
_revert(revertDataWildcard);
} else {
_revert(revertDataPerId);
}
}
}