Skip to content

Commit d112518

Browse files
committed
implement multitoken lib supporting transfer via permit2 contract
1 parent 5535eb4 commit d112518

5 files changed

Lines changed: 1330 additions & 0 deletions

File tree

src/Permit2MultiToken.sol

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import { IERC20 } from "openzeppelin/interfaces/IERC20.sol";
5+
import { IERC721 } from "openzeppelin/interfaces/IERC721.sol";
6+
import { IERC1155 } from "openzeppelin/interfaces/IERC1155.sol";
7+
import { IERC20Permit } from "openzeppelin/token/ERC20/extensions/IERC20Permit.sol";
8+
import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
9+
import { ERC165Checker } from "openzeppelin/utils/introspection/ERC165Checker.sol";
10+
import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol";
11+
12+
import { ICryptoKitties } from "multitoken/interfaces/ICryptoKitties.sol";
13+
import { IMultiTokenCategoryRegistry } from "multitoken/interfaces/IMultiTokenCategoryRegistry.sol";
14+
import { IPermit2Like } from "multitoken/interfaces/IPermit2Like.sol";
15+
16+
17+
/**
18+
* @title Permit2 MultiToken library
19+
* @dev Library for handling various token standards (ERC20, ERC721, ERC1155, CryptoKitties) in a single contract via Permit2.
20+
*/
21+
library Permit2MultiToken {
22+
using ERC165Checker for address;
23+
using SafeERC20 for IERC20;
24+
using SafeCast for uint256;
25+
26+
bytes4 public constant ERC20_INTERFACE_ID = 0x36372b07;
27+
bytes4 public constant ERC721_INTERFACE_ID = 0x80ac58cd;
28+
bytes4 public constant ERC1155_INTERFACE_ID = 0xd9b67a26;
29+
bytes4 public constant CRYPTO_KITTIES_INTERFACE_ID = 0x9a20483d;
30+
31+
/**
32+
* @notice A reserved value for a category not registered.
33+
*/
34+
uint8 public constant CATEGORY_NOT_REGISTERED = type(uint8).max;
35+
36+
/**
37+
* @title Category
38+
* @dev Enum representation Asset category.
39+
*/
40+
enum Category {
41+
ERC20,
42+
ERC721,
43+
ERC1155,
44+
CryptoKitties
45+
}
46+
47+
/**
48+
* @title Asset
49+
* @param category Corresponding asset category.
50+
* @param assetAddress Address of the token contract defining the asset.
51+
* @param id TokenID of an NFT or 0.
52+
* @param amount Amount of fungible tokens or 0 -> 1.
53+
*/
54+
struct Asset {
55+
Category category;
56+
address assetAddress;
57+
uint256 id;
58+
uint256 amount;
59+
}
60+
61+
/**
62+
* @notice Thrown when unsupported category is used.
63+
* @param categoryValue Value of the unsupported category.
64+
*/
65+
error UnsupportedCategory(uint8 categoryValue);
66+
67+
/*----------------------------------------------------------*|
68+
|* # FACTORY FUNCTIONS *|
69+
|*----------------------------------------------------------*/
70+
71+
/**
72+
* @notice Factory function for creating an ERC20 asset.
73+
* @param assetAddress Address of the token contract defining the asset.
74+
* @param amount Amount of fungible tokens.
75+
* @return Asset struct representing the ERC20 asset.
76+
*/
77+
function ERC20(address assetAddress, uint256 amount) internal pure returns (Asset memory) {
78+
return Asset(Category.ERC20, assetAddress, 0, amount);
79+
}
80+
81+
/**
82+
* @notice Factory function for creating an ERC721 asset.
83+
* @param assetAddress Address of the token contract defining the asset.
84+
* @param id Token id of an NFT.
85+
* @return Asset struct representing the ERC721 asset.
86+
*/
87+
function ERC721(address assetAddress, uint256 id) internal pure returns (Asset memory) {
88+
return Asset(Category.ERC721, assetAddress, id, 0);
89+
}
90+
91+
/**
92+
* @notice Factory function for creating an ERC1155 asset.
93+
* @param assetAddress Address of the token contract defining the asset.
94+
* @param id Token id of an SFT.
95+
* @param amount Amount of semifungible tokens.
96+
* @return Asset struct representing the ERC1155 asset.
97+
*/
98+
function ERC1155(address assetAddress, uint256 id, uint256 amount) internal pure returns (Asset memory) {
99+
return Asset(Category.ERC1155, assetAddress, id, amount);
100+
}
101+
102+
/**
103+
* @notice Factory function for creating an ERC1155 NFT asset.
104+
* @param assetAddress Address of the token contract defining the asset.
105+
* @param id Token id of an NFT.
106+
* @return Asset struct representing the ERC1155 NFT asset.
107+
*/
108+
function ERC1155(address assetAddress, uint256 id) internal pure returns (Asset memory) {
109+
return Asset(Category.ERC1155, assetAddress, id, 0);
110+
}
111+
112+
/**
113+
* @notice Factory function for creating a CryptoKitties asset.
114+
* @param assetAddress Address of the token contract defining the asset.
115+
* @param id Token id of a CryptoKitty.
116+
* @return Asset struct representing the CryptoKitties asset.
117+
*/
118+
function CryptoKitties(address assetAddress, uint256 id) internal pure returns (Asset memory) {
119+
return Asset(Category.CryptoKitties, assetAddress, id, 0);
120+
}
121+
122+
123+
/*----------------------------------------------------------*|
124+
|* # TRANSFER ASSET *|
125+
|*----------------------------------------------------------*/
126+
127+
/**
128+
* @notice Wrapping function for `transferFrom` calls on various token interfaces.
129+
* @dev If `source` is `address(this)`, function `transfer` is called instead of `transferFrom` for ERC20 category.
130+
* @param asset Struct defining all necessary context of a token.
131+
* @param permit2 Address of the Permit2 contract to be used for transferring ERC20 tokens.
132+
* @param source Account/address that provided the allowance.
133+
* @param dest Destination address.
134+
*/
135+
function transferAssetFrom(Asset memory asset, address permit2, address source, address dest) internal {
136+
_transferAssetFrom(asset, permit2, source, dest, false);
137+
}
138+
139+
/**
140+
* @notice Wrapping function for `safeTransferFrom` calls on various token interfaces.
141+
* @dev If `source` is `address(this)`, function `transfer` is called instead of `transferFrom` for ERC20 category.
142+
* @param asset Struct defining all necessary context of a token.
143+
* @param permit2 Address of the Permit2 contract to be used for transferring ERC20 tokens.
144+
* @param source Account/address that provided the allowance.
145+
* @param dest Destination address.
146+
*/
147+
function safeTransferAssetFrom(Asset memory asset, address permit2, address source, address dest) internal {
148+
_transferAssetFrom(asset, permit2, source, dest, true);
149+
}
150+
151+
function _transferAssetFrom(Asset memory asset, address permit2, address source, address dest, bool isSafe) private {
152+
if (asset.category == Category.ERC20) {
153+
if (source == address(this))
154+
IERC20(asset.assetAddress).safeTransfer(dest, asset.amount);
155+
else
156+
IPermit2Like(permit2).transferFrom(source, dest, asset.amount.toUint160(), asset.assetAddress);
157+
158+
} else if (asset.category == Category.ERC721) {
159+
if (!isSafe)
160+
IERC721(asset.assetAddress).transferFrom(source, dest, asset.id);
161+
else
162+
IERC721(asset.assetAddress).safeTransferFrom(source, dest, asset.id, "");
163+
164+
} else if (asset.category == Category.ERC1155) {
165+
IERC1155(asset.assetAddress).safeTransferFrom(source, dest, asset.id, asset.amount == 0 ? 1 : asset.amount, "");
166+
167+
} else if (asset.category == Category.CryptoKitties) {
168+
if (source == address(this))
169+
ICryptoKitties(asset.assetAddress).transfer(dest, asset.id);
170+
else
171+
ICryptoKitties(asset.assetAddress).transferFrom(source, dest, asset.id);
172+
173+
} else {
174+
revert("MultiToken: Unsupported category");
175+
}
176+
}
177+
178+
/**
179+
* @notice Get amount of asset that would be transferred.
180+
* @dev NFTs (ERC721, CryptoKitties & ERC1155 with amount 0) with return 1.
181+
* Fungible tokens will return its amount (ERC20 with 0 amount is valid).
182+
* In combination with `balanceOf` can be used to check successful asset transfer.
183+
* @param asset Struct defining all necessary context of a token.
184+
* @return Number of tokens that would be transferred of the asset.
185+
*/
186+
function getTransferAmount(Asset memory asset) internal pure returns (uint256) {
187+
if (asset.category == Category.ERC20)
188+
return asset.amount;
189+
else if (asset.category == Category.ERC1155 && asset.amount > 0)
190+
return asset.amount;
191+
else // Return 1 for ERC721, CryptoKitties and ERC1155 used as NFTs (amount = 0)
192+
return 1;
193+
}
194+
195+
196+
/*----------------------------------------------------------*|
197+
|* # BALANCE OF *|
198+
|*----------------------------------------------------------*/
199+
200+
/**
201+
* @notice Wrapping function for checking balances on various token interfaces.
202+
* @param asset Struct defining all necessary context of a token.
203+
* @param target Target address to be checked.
204+
*/
205+
function balanceOf(Asset memory asset, address target) internal view returns (uint256) {
206+
if (asset.category == Category.ERC20) {
207+
return IERC20(asset.assetAddress).balanceOf(target);
208+
209+
} else if (asset.category == Category.ERC721) {
210+
return IERC721(asset.assetAddress).ownerOf(asset.id) == target ? 1 : 0;
211+
212+
} else if (asset.category == Category.ERC1155) {
213+
return IERC1155(asset.assetAddress).balanceOf(target, asset.id);
214+
215+
} else if (asset.category == Category.CryptoKitties) {
216+
return ICryptoKitties(asset.assetAddress).ownerOf(asset.id) == target ? 1 : 0;
217+
218+
} else {
219+
revert("MultiToken: Unsupported category");
220+
}
221+
}
222+
223+
224+
/*----------------------------------------------------------*|
225+
|* # ASSET CHECKS *|
226+
|*----------------------------------------------------------*/
227+
228+
/**
229+
* @notice Checks that provided asset is contract, has correct format and stated category via MultiTokenCategoryRegistry and ERC165 checks.
230+
* @dev Fungible tokens (ERC20) have to have id = 0.
231+
* NFT (ERC721, CryptoKitties) tokens have to have amount = 0.
232+
* Correct asset category is determined via ERC165.
233+
* The check assumes, that asset contract implements only one token standard at a time.
234+
* @param registry Category registry contract.
235+
* @param asset Asset that is examined.
236+
* @return True if asset has correct format and category.
237+
*/
238+
function isValid(Asset memory asset, IMultiTokenCategoryRegistry registry) internal view returns (bool) {
239+
return _checkCategory(asset, registry) && _checkFormat(asset);
240+
}
241+
242+
/**
243+
* @notice Checks that provided asset is contract, has correct format and stated category via ERC165 checks.
244+
* @dev Fungible tokens (ERC20) have to have id = 0.
245+
* NFT (ERC721, CryptoKitties) tokens have to have amount = 0.
246+
* Correct asset category is determined via ERC165.
247+
* The check assumes, that asset contract implements only one token standard at a time.
248+
* @param asset Asset that is examined.
249+
* @return True if asset has correct format and category.
250+
*/
251+
function isValid(Asset memory asset) internal view returns (bool) {
252+
return _checkCategoryViaERC165(asset) && _checkFormat(asset);
253+
}
254+
255+
/**
256+
* @notice Checks that provided asset is contract and stated category is correct via MultiTokenCategoryRegistry and ERC165 checks.
257+
* @dev Will fallback to ERC165 checks if asset is not registered in the category registry.
258+
* The check assumes, that asset contract implements only one token standard at a time.
259+
* @param registry Category registry contract.
260+
* @param asset Asset that is examined.
261+
* @return True if assets stated category is correct.
262+
*/
263+
function _checkCategory(Asset memory asset, IMultiTokenCategoryRegistry registry) internal view returns (bool) {
264+
// Check if asset is registered in the category registry
265+
uint8 categoryValue = registry.registeredCategoryValue(asset.assetAddress);
266+
if (categoryValue != CATEGORY_NOT_REGISTERED)
267+
return uint8(asset.category) == categoryValue;
268+
269+
return _checkCategoryViaERC165(asset);
270+
}
271+
272+
/**
273+
* @notice Checks that provided asset is contract and stated category is correct via ERC165 checks.
274+
* @dev The check assumes, that asset contract implements only one token standard at a time.
275+
* @param asset Asset that is examined.
276+
* @return True if assets stated category is correct.
277+
*/
278+
function _checkCategoryViaERC165(Asset memory asset) internal view returns (bool) {
279+
if (asset.category == Category.ERC20) {
280+
// ERC20 has optional ERC165 implementation
281+
if (asset.assetAddress.supportsERC165()) {
282+
// If contract implements ERC165 and returns true for ERC20 intefrace id, consider it a correct category
283+
if (asset.assetAddress.supportsERC165InterfaceUnchecked(ERC20_INTERFACE_ID))
284+
return true;
285+
286+
// If contract implements ERC165, it has to return false for ERC721, ERC1155, and CryptoKitties interface ids
287+
return
288+
!asset.assetAddress.supportsERC165InterfaceUnchecked(ERC721_INTERFACE_ID) &&
289+
!asset.assetAddress.supportsERC165InterfaceUnchecked(ERC1155_INTERFACE_ID) &&
290+
!asset.assetAddress.supportsERC165InterfaceUnchecked(CRYPTO_KITTIES_INTERFACE_ID);
291+
292+
} else {
293+
// In case token doesn't implement ERC165, its safe to assume that provided category is correct,
294+
// because any other category has to implement ERC165.
295+
296+
// Check that asset address is contract
297+
// Note: Asset address will return code length 0, if this code is called from the constructor.
298+
return asset.assetAddress.code.length > 0;
299+
}
300+
301+
} else if (asset.category == Category.ERC721) {
302+
// Check ERC721 via ERC165
303+
return asset.assetAddress.supportsInterface(ERC721_INTERFACE_ID);
304+
305+
} else if (asset.category == Category.ERC1155) {
306+
// Check ERC1155 via ERC165
307+
return asset.assetAddress.supportsInterface(ERC1155_INTERFACE_ID);
308+
309+
} else if (asset.category == Category.CryptoKitties) {
310+
// Check CryptoKitties via ERC165
311+
return asset.assetAddress.supportsInterface(CRYPTO_KITTIES_INTERFACE_ID);
312+
313+
} else {
314+
revert UnsupportedCategory(uint8(asset.category));
315+
}
316+
}
317+
318+
/**
319+
* @notice Checks that provided asset has correct format.
320+
* @dev Fungible tokens (ERC20) have to have id = 0.
321+
* NFT (ERC721, CryptoKitties) tokens have to have amount = 0.
322+
* Correct asset category is determined via ERC165.
323+
* @param asset Asset that is examined.
324+
* @return True asset struct has correct format.
325+
*/
326+
function _checkFormat(Asset memory asset) internal pure returns (bool) {
327+
if (asset.category == Category.ERC20) {
328+
// Id must be 0 for ERC20
329+
if (asset.id != 0) return false;
330+
331+
} else if (asset.category == Category.ERC721) {
332+
// Amount must be 0 for ERC721
333+
if (asset.amount != 0) return false;
334+
335+
} else if (asset.category == Category.ERC1155) {
336+
// No format check for ERC1155
337+
338+
} else if (asset.category == Category.CryptoKitties) {
339+
// Amount must be 0 for CryptoKitties
340+
if (asset.amount != 0) return false;
341+
342+
} else {
343+
revert UnsupportedCategory(uint8(asset.category));
344+
}
345+
346+
return true;
347+
}
348+
349+
/**
350+
* @notice Compare two assets, ignoring their amounts.
351+
* @param asset First asset to examine.
352+
* @param otherAsset Second asset to examine.
353+
* @return True if both structs represents the same asset.
354+
*/
355+
function isSameAs(Asset memory asset, Asset memory otherAsset) internal pure returns (bool) {
356+
return
357+
asset.category == otherAsset.category &&
358+
asset.assetAddress == otherAsset.assetAddress &&
359+
asset.id == otherAsset.id;
360+
}
361+
362+
}

src/interfaces/IPermit2Like.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
5+
interface IPermit2Like {
6+
7+
/**
8+
* @notice Transfer approved tokens from one address to another
9+
* @dev Requires the from address to have approved at least the desired amount of tokens to msg.sender.
10+
* @param from The address to transfer from
11+
* @param to The address of the recipient
12+
* @param amount The amount of the token to transfer
13+
* @param token The token address to transfer
14+
*/
15+
function transferFrom(address from, address to, uint160 amount, address token) external;
16+
17+
/**
18+
* @notice Approves the spender to use up to amount of the specified token up until the expiration
19+
* @param token The token to approve
20+
* @param spender The spender address to approve
21+
* @param amount The approved amount of the token
22+
* @param expiration The timestamp at which the approval is no longer valid
23+
* @dev The packed allowance also holds a nonce, which will stay unchanged in approve
24+
* @dev Setting amount to type(uint160).max sets an unlimited approval
25+
*/
26+
function approve(address token, address spender, uint160 amount, uint48 expiration) external;
27+
28+
}

0 commit comments

Comments
 (0)