-
-
Notifications
You must be signed in to change notification settings - Fork 112
Expand file tree
/
Copy pathERC1155TransferEnforcer.sol
More file actions
212 lines (194 loc) · 10 KB
/
Copy pathERC1155TransferEnforcer.sol
File metadata and controls
212 lines (194 loc) · 10 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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import { CaveatEnforcer } from "./CaveatEnforcer.sol";
import { ModeCode } from "../utils/Types.sol";
import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
/**
* @title ERC1155TransferEnforcer
* @notice Enforces transfer restrictions for ERC1155 tokens within delegation contexts
* @dev This enforcer:
* - Operates exclusively in single execution call type with default execution mode
* - Supports both single and batch transfer operations (safeTransferFrom and safeBatchTransferFrom)
* - Automatically selects transfer function based on terms length
* - Maintains per-token ID transfer limits through spent amount tracking
* - Implements cumulative spending limits per delegation hash
* - Validates that only permitted contracts and token IDs can be transferred
*
* Terms Encoding Format:
* - Single transfer: abi.encode(address contract, uint256 tokenId, uint256 maxAmount) [96 bytes]
* - Batch transfer: abi.encode(address contract, uint256[] tokenIds, uint256[] maxAmounts) [≥224 bytes]
* @notice For nonfungible ERC1155 tokens, the transfer amount must be 1.
*/
contract ERC1155TransferEnforcer is CaveatEnforcer {
////////////////////////////// State //////////////////////////////
/// @notice Maps delegation manager address => delegation hash => token ID => spent amount
mapping(address delegationManager => mapping(bytes32 delegationHash => mapping(uint256 tokenId => uint256 amount))) public
spentMap;
////////////////////////////// Events //////////////////////////////
/// @notice Emitted when the spent amount for a token ID is increased
/// @param sender The address of the delegation manager
/// @param delegationHash The hash of the delegation
/// @param tokenId The ID of the token being transferred
/// @param limit The maximum amount allowed for this token ID
/// @param spent The new total amount spent for this token ID
event IncreasedSpentMap(address indexed sender, bytes32 indexed delegationHash, uint256 tokenId, uint256 limit, uint256 spent);
/**
* @notice Enforces that the contract and tokenIds are permitted for transfer
* @dev Validates that the transfer execution matches the permitted terms and updates spent amounts
* @param _terms encoded terms containing transfer type, contract address, token IDs and amounts
* @param _mode The execution mode. (Must be Single callType, Default execType)
* @param _executionCallData the call data of the transferFrom call
* @param _delegationHash the hash of the delegation being operated on
*/
function beforeHook(
bytes calldata _terms,
bytes calldata,
ModeCode _mode,
bytes calldata _executionCallData,
bytes32 _delegationHash,
address,
address
)
public
virtual
override
onlySingleCallTypeMode(_mode)
onlyDefaultExecutionMode(_mode)
{
_validateTransfer(_terms, _delegationHash, _executionCallData);
}
/**
* @notice Decodes the terms to get the transfer type, permitted contract and token IDs
* @dev Validates the terms length and structure
* @param _terms The encoded terms containing transfer type, contract address and token IDs
* @return _isBatch The transfer type flag
* @return _permittedContract The address of the permitted ERC1155 contract
* @return _permittedIds Array of token IDs
* @return _permittedAmounts Array of token amounts
*/
function getTermsInfo(bytes calldata _terms)
public
pure
returns (bool _isBatch, address _permittedContract, uint256[] memory _permittedIds, uint256[] memory _permittedAmounts)
{
if (_terms.length == 96) {
_permittedIds = new uint256[](1);
_permittedAmounts = new uint256[](1);
(_permittedContract, _permittedIds[0], _permittedAmounts[0]) = abi.decode(_terms, (address, uint256, uint256));
} else if (_terms.length >= 224) {
_isBatch = true;
(_permittedContract, _permittedIds, _permittedAmounts) = abi.decode(_terms, (address, uint256[], uint256[]));
} else {
revert("ERC1155TransferEnforcer:invalid-terms-length");
}
if (_permittedContract == address(0)) revert("ERC1155TransferEnforcer:invalid-contract-address");
if (_permittedIds.length != _permittedAmounts.length) revert("ERC1155TransferEnforcer:invalid-ids-values-length");
}
/**
* @notice Validates that the transfer execution matches the permitted terms
* @dev Checks that the contract, token IDs and amounts match what is permitted in the terms
* @param _terms The encoded terms containing transfer type, contract address, token IDs and amounts
* @param _delegationHash The hash of the delegation being operated on
* @param _executionCallData The encoded execution data containing the transfer details
*/
function _validateTransfer(bytes calldata _terms, bytes32 _delegationHash, bytes calldata _executionCallData) internal {
(bool isBatch_, address permittedContract_, uint256[] memory permittedTokenIds_, uint256[] memory permittedAmounts_) =
getTermsInfo(_terms);
(address target_, uint256 value_, bytes calldata callData_) = ExecutionLib.decodeSingle(_executionCallData);
if (value_ != 0) revert("ERC1155TransferEnforcer:invalid-value");
if (callData_.length != 196 && callData_.length < 324) revert("ERC1155TransferEnforcer:invalid-calldata-length");
if (target_ != permittedContract_) revert("ERC1155TransferEnforcer:unauthorized-contract-target");
bytes4 selector_ = bytes4(callData_[0:4]);
if (isBatch_ && selector_ != IERC1155.safeBatchTransferFrom.selector) {
revert("ERC1155TransferEnforcer:unauthorized-selector-batch");
}
if (!isBatch_ && selector_ != IERC1155.safeTransferFrom.selector) {
revert("ERC1155TransferEnforcer:unauthorized-selector-single");
}
if (isBatch_) {
_validateBatchTransfer(_delegationHash, callData_, permittedTokenIds_, permittedAmounts_);
} else {
_validateSingleTransfer(_delegationHash, callData_, permittedTokenIds_[0], permittedAmounts_[0]);
}
}
/**
* @notice Validates a single ERC1155 token transfer against permitted parameters
* @dev Checks that the transfer addresses are valid and matches token ID and amount against permitted values
* @param _delegationHash The hash of the delegation being operated on
* @param _callData The encoded transfer function call data
* @param _permittedTokenId The token ID that is permitted to be transferred
* @param _permittedAmount The amount that is permitted to be transferred
*/
function _validateSingleTransfer(
bytes32 _delegationHash,
bytes calldata _callData,
uint256 _permittedTokenId,
uint256 _permittedAmount
)
internal
{
(address from_, address to_, uint256 id_, uint256 amount_,) =
abi.decode(_callData[4:], (address, address, uint256, uint256, bytes));
if (from_ == address(0) || to_ == address(0)) {
revert("ERC1155TransferEnforcer:invalid-address");
}
if (_permittedTokenId != id_) {
revert("ERC1155TransferEnforcer:unauthorized-token-id");
}
_increaseSpentMap(_delegationHash, id_, amount_, _permittedAmount);
}
/**
* @notice Validates a batch ERC1155 token transfer against permitted parameters
* @dev Checks that all token IDs in the batch are permitted and their amounts don't exceed limits
* @param _delegationHash The hash of the delegation being operated on
* @param _callData The encoded batch transfer function call data
* @param _permittedTokenIds Array of permitted token IDs
* @param _permittedAmounts Array of permitted amounts for each token ID
*/
function _validateBatchTransfer(
bytes32 _delegationHash,
bytes calldata _callData,
uint256[] memory _permittedTokenIds,
uint256[] memory _permittedAmounts
)
internal
{
(address from_, address to_, uint256[] memory ids_, uint256[] memory amounts_,) =
abi.decode(_callData[4:], (address, address, uint256[], uint256[], bytes));
if (from_ == address(0) || to_ == address(0)) {
revert("ERC1155TransferEnforcer:invalid-address");
}
uint256 idsLength_ = ids_.length;
uint256 permittedTokenIdsLength_ = _permittedTokenIds.length;
// Check if all token IDs in the batch are permitted
for (uint256 i = 0; i < idsLength_; i++) {
bool isPermitted_ = false;
for (uint256 j = 0; j < permittedTokenIdsLength_; j++) {
if (ids_[i] == _permittedTokenIds[j]) {
_increaseSpentMap(_delegationHash, ids_[i], amounts_[i], _permittedAmounts[j]);
isPermitted_ = true;
break;
}
}
if (!isPermitted_) {
revert("ERC1155TransferEnforcer:unauthorized-token-id");
}
}
}
/**
* @notice Updates and validates the spent amount for a token ID
* @dev Increments the spent amount and checks against permitted limit
* @param _delegationHash The hash of the delegation being operated on
* @param _id The token ID being tracked
* @param _amount The amount to increase the spent tracker by
* @param _permittedAmount The maximum amount allowed for this token ID
*/
function _increaseSpentMap(bytes32 _delegationHash, uint256 _id, uint256 _amount, uint256 _permittedAmount) private {
uint256 spent_ = spentMap[msg.sender][_delegationHash][_id] += _amount;
if (spent_ > _permittedAmount) {
revert("ERC1155TransferEnforcer:unauthorized-amount");
}
emit IncreasedSpentMap(msg.sender, _delegationHash, _id, _permittedAmount, spent_);
}
}