|
| 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 | +} |
0 commit comments