├── .gitignore ├── FnutRuVWAAAOaVY.jpeg ├── FnuuanrXEAAYQch.jpeg ├── FnuuepzWAAE4Hx4.png ├── FnuunmuXkAAS-bssq.jpeg ├── LICENSE ├── README.md ├── contracts ├── BlurExchange │ ├── BlurExchange.sol │ ├── BlurExchange_new.sol │ ├── ExecutionDelegate.sol │ ├── interfaces │ │ ├── IBlurExchange.sol │ │ ├── IBlurPool.sol │ │ ├── IExecutionDelegate.sol │ │ ├── IMatchingPolicy.sol │ │ └── IPolicyManager.sol │ └── lib │ │ ├── EIP712.sol │ │ ├── MerkleVerifier.sol │ │ ├── OrderStructs.sol │ │ └── ReentrancyGuarded.sol ├── BlurPool │ ├── BlurPool.sol │ ├── WETH9.sol │ └── interfaces │ │ └── IBlurPool.sol ├── BlurSwap │ ├── BlurSwap.sol │ ├── SpecialTransferHelper.sol │ ├── interfaces │ │ ├── ICryptoPunks.sol │ │ ├── IERC1155.sol │ │ ├── IERC20.sol │ │ ├── IERC721.sol │ │ ├── IMoonCatsRescue.sol │ │ └── IWrappedPunk.sol │ ├── markets │ │ └── MarketRegistry.sol │ └── utils │ │ └── ReentrancyGuard.sol └── PolicyManager │ ├── PolicyManager.sol │ ├── interfaces │ ├── IMatchingPolicy.sol │ └── IPolicyManager.sol │ ├── lib │ └── OrderStructs.sol │ └── matchingPolicies │ ├── SafeCollectionBidPolicyERC721.sol │ ├── StandardPolicyERC721.sol │ └── StandardPolicyERC721_1.sol ├── exchange_architecture.png ├── hardhat.config.js ├── mainnet-0x39da41747a83aee658334415666f3ef92dd0d541.svg ├── package.json ├── scripts └── deploy.js ├── test └── Lock.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | 8 | # Hardhat files 9 | cache 10 | artifacts 11 | 12 | -------------------------------------------------------------------------------- /FnutRuVWAAAOaVY.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteWizardJ/blur-analysis/caddf818d627c5b4a001cfd82850feff0bc31d80/FnutRuVWAAAOaVY.jpeg -------------------------------------------------------------------------------- /FnuuanrXEAAYQch.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteWizardJ/blur-analysis/caddf818d627c5b4a001cfd82850feff0bc31d80/FnuuanrXEAAYQch.jpeg -------------------------------------------------------------------------------- /FnuuepzWAAE4Hx4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteWizardJ/blur-analysis/caddf818d627c5b4a001cfd82850feff0bc31d80/FnuuepzWAAE4Hx4.png -------------------------------------------------------------------------------- /FnuunmuXkAAS-bssq.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteWizardJ/blur-analysis/caddf818d627c5b4a001cfd82850feff0bc31d80/FnuunmuXkAAS-bssq.jpeg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jukes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blur-analysis 2 | 3 | 借贷协议 Blend 的解析可以看这里:https://github.com/cryptochou/blend-analysis 4 | 5 | 6 | 7 | 8 | - [1. BlurSwap](#1-blurswap) 9 | - [2. BlurExchange](#2-blurexchange) 10 | - [2.1 代码地址](#21-%E4%BB%A3%E7%A0%81%E5%9C%B0%E5%9D%80) 11 | - [2.2 整体架构](#22-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84) 12 | - [2.3 BlurExchange](#23-blurexchange) 13 | - [3. PolicyManager](#3-policymanager) 14 | - [3.1 MatchingPolicy](#31-matchingpolicy) 15 | - [4. BlurPool](#4-blurpool) 16 | - [5. Blur Bid](#5-blur-bid) 17 | - [5.1 出价](#51-%E5%87%BA%E4%BB%B7) 18 | - [5.2 Opensea Offer](#52-opensea-offer) 19 | - [5.3 Blur Bid](#53-blur-bid) 20 | - [6. Opensea 和 版税](#6-opensea-%E5%92%8C-%E7%89%88%E7%A8%8E) 21 | - [7. 总结](#7-%E6%80%BB%E7%BB%93) 22 | - [8. 参考](#8-%E5%8F%82%E8%80%83) 23 | 24 | 25 | 26 | ## 1. BlurSwap 27 | 28 | 29 | 30 | BlurSwap 合约, fork 自 GemSwap。二者代码一致。 31 | 32 | 主要用于处理聚合交易相关逻辑。 33 | 34 | ## 2. BlurExchange 35 | 36 | 37 | 38 | BlurExchange 合约。Blur 自建的交易市场合约。 39 | 40 | BlurExchange 的交易模型于 Opensea 一样是中央订单簿的交易模型,都是由链下的中心化的订单簿和链上的交易组成。 41 | 42 | 其中链下的订单簿负责存储用户的挂单信息,并对订单进行撮合。最终的成交和转移 NFT 是由 BlurExchange 来负责的。 43 | 44 | ### 2.1 代码地址 45 | 46 | Blur 官方没有给出具体的代码仓库地址。不过我在 GitHub 上找到了下面这个代码仓库,应该是之前提交审计的时候留下来的。 47 | 48 | 49 | 50 | ps: 这个代码库里的代码跟最新的实现合约有了不小的差别,仅做参考。 51 | 52 | ### 2.2 整体架构 53 | 54 | ![](exchange_architecture.png) 55 | 56 | 57 | 58 | ![](mainnet-0x39da41747a83aee658334415666f3ef92dd0d541.svg) 59 | 60 | 按照模块可以分为一下几类: 61 | 62 | 1. BlurExchange:主合约,负责交易的执行。 63 | 2. PolicyManager:订单交易策略管理者。 64 | 3. MatchingPolicy:订单交易策略,负责判断买单、买单是否可以匹配。 65 | 4. ExecutionDelegate:负责具体的转移代币的逻辑。 66 | 67 | ### 2.3 BlurExchange 68 | 69 | 这是一个 upgradeable 合约,因此会有不同版本的实现合约。 70 | 71 | 目前实现合约的地址是 72 | 73 | #### 2.3.0 数据结构 74 | 75 | ```solidity 76 | // 交易方向 77 | enum Side { Buy, Sell } 78 | // 签名类型 79 | enum SignatureVersion { Single, Bulk } 80 | // 资产类型 81 | enum AssetType { ERC721, ERC1155 } 82 | 83 | // 收费详情 84 | struct Fee { 85 | uint16 rate; // 比率 86 | address payable recipient; // 接收者 87 | } 88 | 89 | // 订单数据 90 | struct Order { 91 | address trader; // 订单创建者 92 | Side side; // 交易方向 93 | address matchingPolicy; // 交易策略 94 | address collection; // 合约地址 95 | uint256 tokenId; // tokenId 96 | uint256 amount; // 数量 97 | address paymentToken; // 支付的代币 98 | uint256 price; // 价格 99 | uint256 listingTime; // 挂单时间 100 | /* Order expiration timestamp - 0 for oracle cancellations. */ 101 | uint256 expirationTime; // 过期时间,oracle cancellations 的是 0 102 | Fee[] fees; // 费用 103 | uint256 salt; 104 | bytes extraParams; // 额外数据,如果长度大于 0,且第一个元素是 1 则表示是oracle authorization 105 | } 106 | 107 | // 订单和签名数据 108 | struct Input { 109 | Order order; // 订单数据 110 | uint8 v; 111 | bytes32 r; 112 | bytes32 s; 113 | bytes extraSignature; // 批量订单校验和 Oracle 校验使用的额外数据 114 | SignatureVersion signatureVersion; // 签名类型 115 | uint256 blockNumber; // 挂单时的区块高度 116 | } 117 | 118 | // 交易双方的数据 119 | struct Execution { 120 | Input sell; 121 | Input buy; 122 | } 123 | 124 | ``` 125 | 126 | #### 2.3.1 需要注意的成员变量 127 | 128 | ##### 2.3.1.1 isOpen 129 | 130 | 交易开启和关闭的开关。 131 | 132 | 设置的时候会发出事件。 133 | 134 | ```solidity 135 | uint256 public isOpen; 136 | 137 | modifier whenOpen() { 138 | require(isOpen == 1, "Closed"); 139 | _; 140 | } 141 | 142 | event Opened(); 143 | event Closed(); 144 | 145 | function open() external onlyOwner { 146 | isOpen = 1; 147 | emit Opened(); 148 | } 149 | function close() external onlyOwner { 150 | isOpen = 0; 151 | emit Closed(); 152 | } 153 | ``` 154 | 155 | ##### 2.3.1.2 isInternal 和 remainingETH 156 | 157 | `isInternal` 用来防止重入攻击,并限制 `_execute()` 函数只能通过被 `setupExecution` 修饰的函数调用,目前只有 `execute()` 和 `bulkExecute()` 被 `setupExecution` 修饰。 158 | 159 | `remainingETH` 用来记录 `msg.sender` 的 ETH。交易过程中会根据订单信息来转移指定数量的 ETH,如果最后执行玩交易后还剩余的就通过 `_returnDust()` 转回给 `msg.sender`。 160 | 161 | ```solidity 162 | bool public isInternal = false; 163 | uint256 public remainingETH = 0; 164 | 165 | modifier setupExecution() { 166 | require(!isInternal, "Unsafe call"); // add redundant re-entrancy check for clarity 167 | remainingETH = msg.value; 168 | isInternal = true; 169 | _; 170 | remainingETH = 0; 171 | isInternal = false; 172 | } 173 | 174 | modifier internalCall() { 175 | require(isInternal, "Unsafe call"); 176 | _; 177 | } 178 | ``` 179 | 180 | ```solidity 181 | function _returnDust() private { 182 | uint256 _remainingETH = remainingETH; 183 | assembly { 184 | if gt(_remainingETH, 0) { 185 | let callStatus := call( 186 | gas(), 187 | caller(), 188 | _remainingETH, 189 | 0, 190 | 0, 191 | 0, 192 | 0 193 | ) 194 | if iszero(callStatus) { 195 | revert(0, 0) 196 | } 197 | } 198 | } 199 | } 200 | ``` 201 | 202 | ##### 2.3.1.3 cancelledOrFilled 203 | 204 | 用于记录取消的和已成交订单信息。类型是 mapping,key是订单的 `orderHash`。 205 | 206 | ```solidity 207 | mapping(bytes32 => bool) public cancelledOrFilled; 208 | ``` 209 | 210 | 正如之前说的 BlurExchange 是链下中心化订单簿的交易模型,所有的订单数据都存在链下。因此挂单的时候只需要进行签名,Blur 会将签名信息和订单信息放到自己的中心化服务器上,这个流程是不消耗 gas 的(当然授权 NFT 的操作还是需要消耗 gas)。 211 | 212 | 但是如果用户想要取消挂单,就需要调用 BlurExchange 合约的 `cancelOrder()` 方法将这个订单的 hash 设置到 `cancelledOrFilled` 这一成员变量中,这一个过程涉及到链上数据的修改,因此需要消耗 gas。 213 | 214 | 而且这一步是必须的。如果只在链下订单簿上将订单删除,没有设置 `cancelledOrFilled`。这时候其他人如果能在用户删除订单数据之前拿到这个订单数据和签名信息还是能通过 BlurExchange 合约进行成单的。而大部分交易所(比如 Opensea 和 Blur)的订单数据都是能通过特定 API 来获取的。 215 | 216 | ##### 2.3.1.4 nonces 217 | 218 | 用于记录用户的 nonce 。类型是 mapping,key是用户的 `address`。 219 | 220 | ```solidity 221 | mapping(address => uint256) public nonces; 222 | ``` 223 | 224 | 这一数据主要来管理用户的订单。如果用户想要取消所有的订单,不需要一个个调用 `cancelOrder()` ,只需要调用 `incrementNonce()` 方法将 `nonces` 中存储的 nonce 值加 1 就行。 225 | 226 | ```solidity 227 | /** 228 | * @dev Cancel all current orders for a user, preventing them from being matched. Must be called by the trader of the order 229 | */ 230 | function incrementNonce() external { 231 | nonces[msg.sender] += 1; 232 | emit NonceIncremented(msg.sender, nonces[msg.sender]); 233 | } 234 | ``` 235 | 236 | 这是因为所有订单的 hash 都是通过订单数据和订单 `trader` 的 nonce 来生成的。而如果 nonce 的值改变了之后,订单的 hash 就会发生改变。这时候再去校验用户的签名的时候就会失败。从而使用户所有以之前 nonce 签名的的订单全部失效。具体校验逻辑可以看下面的 Signature Authentication。 237 | 238 | ```solidity 239 | function _hashOrder(Order calldata order, uint256 nonce) 240 | internal 241 | pure 242 | returns (bytes32) 243 | { 244 | return keccak256( 245 | bytes.concat( 246 | abi.encode( 247 | ORDER_TYPEHASH, 248 | order.trader, 249 | order.side, 250 | order.matchingPolicy, 251 | order.collection, 252 | order.tokenId, 253 | order.amount, 254 | order.paymentToken, 255 | order.price, 256 | order.listingTime, 257 | order.expirationTime, 258 | _packFees(order.fees), 259 | order.salt, 260 | keccak256(order.extraParams) 261 | ), 262 | abi.encode(nonce) 263 | ) 264 | ); 265 | } 266 | ``` 267 | 268 | ##### 2.3.1.4 其他一些成员变量 269 | 270 | ```solidity 271 | // ExecutionDelegate 合约的地址,用于执行代币转移 272 | IExecutionDelegate public executionDelegate; 273 | // PolicyManager 合约的地址,用于管理交易策略 274 | IPolicyManager public policyManager; 275 | // oracle 的地址,用于 oracle 签名交易的校验 276 | address public oracle; 277 | // 订单发起到成交时区块高度的最大范围,用于 Oracle Signature 类型的订单。 278 | uint256 public blockRange; 279 | 280 | // 收费比率 281 | uint256 public feeRate; 282 | // 费用接收地址 283 | address public feeRecipient; 284 | // 官方地址,设置费用比率的时候必须是这个官方地址发起的调用 285 | address public governor; 286 | ``` 287 | 288 | #### 2.3.2 Execute 289 | 290 | 订单的匹配通过 `execute()` 和 `bulkExecute()` 进行匹配。他们一个是单个订单匹配,一个是多个订单匹配。最终都会调用 `_execute()` 方法。 291 | 292 | ```solidity 293 | function _execute(Input calldata sell, Input calldata buy) 294 | public 295 | payable 296 | internalCall 297 | reentrancyGuard 298 | { 299 | require(sell.order.side == Side.Sell); 300 | 301 | // 计算订单 hash 302 | bytes32 sellHash = _hashOrder(sell.order, nonces[sell.order.trader]); 303 | bytes32 buyHash = _hashOrder(buy.order, nonces[buy.order.trader]); 304 | 305 | // 校验订单参数 306 | require(_validateOrderParameters(sell.order, sellHash), "Sell has invalid parameters"); 307 | require(_validateOrderParameters(buy.order, buyHash), "Buy has invalid parameters"); 308 | 309 | // 校验签名,order.trader == msg.sender 则不去校验直接返回 true 310 | require(_validateSignatures(sell, sellHash), "Sell failed authorization"); 311 | require(_validateSignatures(buy, buyHash), "Buy failed authorization"); 312 | 313 | // 校验买卖订单是否能匹配 314 | (uint256 price, uint256 tokenId, uint256 amount, AssetType assetType) = _canMatchOrders(sell.order, buy.order); 315 | 316 | /* Mark orders as filled. */ 317 | // 存储订单状态 318 | cancelledOrFilled[sellHash] = true; 319 | cancelledOrFilled[buyHash] = true; 320 | 321 | // 执行资产转移 322 | _executeFundsTransfer( 323 | sell.order.trader, 324 | buy.order.trader, 325 | sell.order.paymentToken, 326 | sell.order.fees, 327 | price 328 | ); 329 | 330 | // 执行 NFT 转移 331 | _executeTokenTransfer( 332 | sell.order.collection, 333 | sell.order.trader, 334 | buy.order.trader, 335 | tokenId, 336 | amount, 337 | assetType 338 | ); 339 | 340 | // 发出事件 341 | emit OrdersMatched( 342 | // 买单时间大,表明此次是由买家触发,事件中的 maker 是买家。相反的 maker 是卖家表明是有卖家触发的订单。 343 | sell.order.listingTime <= buy.order.listingTime ? sell.order.trader : buy.order.trader, 344 | sell.order.listingTime > buy.order.listingTime ? sell.order.trader : buy.order.trader, 345 | sell.order, 346 | sellHash, 347 | buy.order, 348 | buyHash 349 | ); 350 | } 351 | ``` 352 | 353 | 订单完成后发出 `OrdersMatched` 事件。 354 | 355 | ```solidity 356 | event OrdersMatched( 357 | address indexed maker, 358 | address indexed taker, 359 | Order sell, 360 | bytes32 sellHash, 361 | Order buy, 362 | bytes32 buyHash 363 | ); 364 | ``` 365 | 366 | #### 2.3.3 canMatchOrders 367 | 368 | 在上面撮合订单的方法中会进行校验买卖单能否成交。具体的 matchingPolicy 分析见下文。 369 | 370 | ```solidity 371 | function _canMatchOrders(Order calldata sell, Order calldata buy) 372 | internal 373 | view 374 | returns (uint256 price, uint256 tokenId, uint256 amount, AssetType assetType) 375 | { 376 | bool canMatch; 377 | if (sell.listingTime <= buy.listingTime) { 378 | /* Seller is maker. */ 379 | // 校验订单的成交策略是否在白名单中 380 | require(policyManager.isPolicyWhitelisted(sell.matchingPolicy), "Policy is not whitelisted"); 381 | // 调用具体的校验方法进行校验 382 | (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(sell.matchingPolicy).canMatchMakerAsk(sell, buy); 383 | } else { 384 | /* Buyer is maker. */ 385 | require(policyManager.isPolicyWhitelisted(buy.matchingPolicy), "Policy is not whitelisted"); 386 | (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(buy.matchingPolicy).canMatchMakerBid(buy, sell); 387 | } 388 | require(canMatch, "Orders cannot be matched"); 389 | 390 | return (price, tokenId, amount, assetType); 391 | } 392 | ``` 393 | 394 | #### 2.3.4 Signature Authentication 395 | 396 | 由于采用链下中心化订单簿的交易模型,用户首先将 NFT 授权给 Blur,然后 Blur 在撮合成交的时候将 NFT 转移给买家。为了确保交易按照卖家的要求成交,因此需要卖家对订单信息进行签名。然后在成交的时候对签名进行校验,以此来保证交易的安全。 397 | 398 | 在 BlurExchange 中,可以一次签名一个订单,也可以一次签名多个订单。而且除了 User Authorization,还有 Oracle Authorization。下面会详细介绍。 399 | 400 | 所有的签名校验都通过 `_validateSignatures()` 方法进行。 401 | 402 | ```solidity 403 | function _validateSignatures(Input calldata order, bytes32 orderHash) 404 | internal 405 | view 406 | returns (bool) 407 | { 408 | // 如果订单的 extraParams 中有数据,且第一个元素是 1 表示需要进行 Oracle Authorization 409 | if (order.order.extraParams.length > 0 && order.order.extraParams[0] == 0x01) { 410 | /* Check oracle authorization. */ 411 | // 订单的挂单区块高度与当前成单时的区块高度的差值要小于 blockRange 412 | require(block.number - order.blockNumber < blockRange, "Signed block number out of range"); 413 | if ( 414 | !_validateOracleAuthorization( 415 | orderHash, 416 | order.signatureVersion, 417 | order.extraSignature, 418 | order.blockNumber 419 | ) 420 | ) { 421 | return false; 422 | } 423 | } 424 | 425 | // 交易方与调用者相同的时候不用校验,因为这个交易是调用者自己触发的。 426 | if (order.order.trader == msg.sender) { 427 | return true; 428 | } 429 | 430 | /* Check user authorization. */ 431 | if ( 432 | !_validateUserAuthorization( 433 | orderHash, 434 | order.order.trader, 435 | order.v, 436 | order.r, 437 | order.s, 438 | order.signatureVersion, 439 | order.extraSignature 440 | ) 441 | ) { 442 | return false; 443 | } 444 | 445 | return true; 446 | } 447 | ``` 448 | 449 | ##### 2.3.4.1 User Authorization 450 | 451 | 订单中的 SignatureVersion 参数确定了两种类型的签名类型: 单一(Single)和批量(Bulk)。单个校验通过订单哈希的签名信息进行身份验证。批量则更为复杂一些。 452 | 453 | ###### Bulk SignatureVersion 454 | 455 | 批量校验签名用到了大家都很熟悉的 Merkle Tree。 456 | 457 | 要进行批量校验签名的时候,用户需要根据要签署的多个订单信息生成订单 hash。然后利用订单 hash 生成 Merkle Tree,并得到 Merkle Tree Root。最后将订单 hash 各自的 path 打包在订单数据的 extraSignature 中。这样在成交的时候利用订单 hash 和 proof 数据生成Merkle Tree Root,然后再验证签名信息。 458 | 459 | ```solidity 460 | function _validateUserAuthorization( 461 | bytes32 orderHash, 462 | address trader, 463 | uint8 v, 464 | bytes32 r, 465 | bytes32 s, 466 | SignatureVersion signatureVersion, 467 | bytes calldata extraSignature 468 | ) internal view returns (bool) { 469 | bytes32 hashToSign; 470 | if (signatureVersion == SignatureVersion.Single) { // 单个签名 471 | /* Single-listing authentication: Order signed by trader */ 472 | hashToSign = _hashToSign(orderHash); 473 | } else if (signatureVersion == SignatureVersion.Bulk) { // 批量签名 474 | /* Bulk-listing authentication: Merkle root of orders signed by trader */ 475 | // 从 extraSignature 中解出 merkle tree 的路径 476 | (bytes32[] memory merklePath) = abi.decode(extraSignature, (bytes32[])); 477 | // 计算 merkle tree 的 root 节点 478 | bytes32 computedRoot = MerkleVerifier._computeRoot(orderHash, merklePath); 479 | hashToSign = _hashToSignRoot(computedRoot); 480 | } 481 | // 校验签名 482 | return _recover(hashToSign, v, r, s) == trader; 483 | } 484 | ``` 485 | 486 | ##### 2.3.4.2 Oracle Authorization 487 | 488 | 这里的 Oracle 跟 Chainlink 那样的预言机没有什么关系,反而跟 NFT Mint 阶段进行签名校验的逻辑相似。 489 | 490 | 上面我们提到过 BlurExchange 合约中有一个 oracle 的成员变量。他是一个地址类型的变量。 491 | 492 | ```solidity 493 | address public oracle; 494 | ``` 495 | 496 | 如果要使用 Oracle Authorization 这项功能,用户需要选择授权 Oracle 这个地址对订单进行签名。然后将 Oracle 的签名信息放到订单的 extraSignature 这一参数中去。最后订单校验的时候会对这一签名信息进行校验,如果校验通过就可以进行接下来的校验。 497 | 498 | Oracle Authorization 需要注意以下几点: 499 | 500 | ###### 1. Oracle Authorization 是可选的 501 | 502 | Oracle Authorization 是可选的,User Authorization 是每次成单都必须进行的。 503 | 504 | ###### 2. Oracle Authorization 实现了链下取消订单的方法 505 | 506 | 因为 Oracle 这一账户对订单进行签名是在链下 Blur 中心化服务器上进行的。如果用户想要取消某个使用了 Oracle Authorization 方式的订单,只需要告诉 Blur 的服务器不再对其进行生成签名就可以了。 507 | 508 | ###### 3. blockNumber 和 blockRange 509 | 510 | 进行这种形式校验的订单需要提供订单创建时的区块高度信息(blockNumber)。并且在校验的时候,订单创建时候的区块高度与当前成单时候的区块高度的差值必须小于 `blockRange` 这一成员变量。 511 | 512 | ```solidity 513 | /* Check oracle authorization. */ 514 | require(block.number - order.blockNumber < blockRange, "Signed block number out of range"); 515 | ``` 516 | 517 | 这样做的目的应该是是安全上面的考量。减少了签名的有效时间,防止签名的滥用。 518 | 519 | ###### 4. extraSignature 存储了批量订单校验和 Oracle 校验使用的额外数据 520 | 521 | 订单信息中的 `extraSignature` 是一个 `bytes` 类型的参数。 522 | 523 | 如果当前订单是一个单个校验订单,则 `extraSignature` 存储的只有 Oracle 校验使用的额外数据。为空则表示该单个校验订单不支持 Oracle Authorization。 524 | 525 | 如果当前订单是一个批量校验订单,则 `extraSignature` 存储的既有批量订单校验需要用到的Merkle Path 数据,也有 Oracle 校验使用的签名数据。 526 | 527 | 其中前 32 个字节是批量订单校验的 Merkle Path 数据,接着后面每 32 个字节都是一个签名数据。 528 | 529 | ##### _validateOracleAuthorization() 530 | 531 | ```solidity 532 | function _validateOracleAuthorization( 533 | bytes32 orderHash, 534 | SignatureVersion signatureVersion, 535 | bytes calldata extraSignature, 536 | uint256 blockNumber 537 | ) internal view returns (bool) { 538 | bytes32 oracleHash = _hashToSignOracle(orderHash, blockNumber); 539 | 540 | uint8 v; bytes32 r; bytes32 s; 541 | if (signatureVersion == SignatureVersion.Single) { 542 | assembly { 543 | v := calldataload(extraSignature.offset) 544 | r := calldataload(add(extraSignature.offset, 0x20)) 545 | s := calldataload(add(extraSignature.offset, 0x40)) 546 | } 547 | /* 548 | REFERENCE 549 | (v, r, s) = abi.decode(extraSignature, (uint8, bytes32, bytes32)); 550 | */ 551 | } else if (signatureVersion == SignatureVersion.Bulk) { 552 | /* If the signature was a bulk listing the merkle path must be unpacked before the oracle signature. */ 553 | assembly { 554 | v := calldataload(add(extraSignature.offset, 0x20)) 555 | r := calldataload(add(extraSignature.offset, 0x40)) 556 | s := calldataload(add(extraSignature.offset, 0x60)) 557 | } 558 | /* 559 | REFERENCE 560 | uint8 _v, bytes32 _r, bytes32 _s; 561 | (bytes32[] memory merklePath, uint8 _v, bytes32 _r, bytes32 _s) = abi.decode(extraSignature, (bytes32[], uint8, bytes32, bytes32)); 562 | v = _v; r = _r; s = _s; 563 | */ 564 | } 565 | 566 | return _verify(oracle, oracleHash, v, r, s); 567 | } 568 | ``` 569 | 570 | #### 2.3.5 Token Transfer 571 | 572 | 买卖双方在授权代币的时候会向 ExecutionDelegate 授权。然后在订单成交的时候由 ExecutionDelegate 负责具体的转移代币的逻辑。 573 | 574 | ##### 2.3.5.1 货币的转移 575 | 576 | 通过下面的代码可以发现 Blur 只支持 ETH、WETH 和 BlurPool 作为支付货币。 其他的 ERC20 代币还不支持作为支付代币。 577 | 578 | BlurPool 比较特殊,可以简单理解为 WETH。下面会详细介绍。 579 | 580 | ```solidity 581 | // ETH or WETH or BlurPool 582 | function _transferTo( 583 | address paymentToken, 584 | address from, 585 | address to, 586 | uint256 amount 587 | ) internal { 588 | if (amount == 0) { 589 | return; 590 | } 591 | 592 | if (paymentToken == address(0)) { 593 | /* Transfer funds in ETH. */ 594 | require(to != address(0), "Transfer to zero address"); 595 | (bool success,) = payable(to).call{value: amount}(""); 596 | require(success, "ETH transfer failed"); 597 | } else if (paymentToken == POOL) { 598 | /* Transfer Pool funds. */ 599 | bool success = IBlurPool(POOL).transferFrom(from, to, amount); 600 | require(success, "Pool transfer failed"); 601 | } else if (paymentToken == WETH) { 602 | /* Transfer funds in WETH. */ 603 | executionDelegate.transferERC20(WETH, from, to, amount); 604 | } else { 605 | revert("Invalid payment token"); 606 | } 607 | } 608 | ``` 609 | 610 | ##### 2.3.5.2 资产的转移 611 | 612 | ```solidity 613 | // NFT 的转移 614 | function _executeTokenTransfer( 615 | address collection, 616 | address from, 617 | address to, 618 | uint256 tokenId, 619 | uint256 amount, 620 | AssetType assetType 621 | ) internal { 622 | /* Call execution delegate. */ 623 | if (assetType == AssetType.ERC721) { 624 | executionDelegate.transferERC721(collection, from, to, tokenId); 625 | } else if (assetType == AssetType.ERC1155) { 626 | executionDelegate.transferERC1155(collection, from, to, tokenId, amount); 627 | } 628 | } 629 | ``` 630 | 631 | ## 3. PolicyManager 632 | 633 | 634 | 635 | 用于管理所有的交易策略。 636 | 637 | 包括交易策略的添加,移除和查看等功能。 638 | 639 | ### 3.1 MatchingPolicy 640 | 641 | 结合 PolicyManager 的 Event 信息,可以找到目前 PolicyManager 白名单中有三种交易策略: 642 | 643 | 1. StandardPolicyERC721(normal): 644 | 2. StandardPolicyERC721(oracle): 645 | 3. SafeCollectionBidPolicyERC721: 646 | 647 | 注意前两个是不同的合约,具体的策略也有一些区别。 648 | 649 | 其中 StandardPolicyERC721(normal)和 StandardPolicyERC721(oracle)基本逻辑差不多,不同的是 oracle 类型的策略要求必须用于支持 Oracle Authorization 的订单。 650 | 651 | SafeCollectionBidPolicyERC721 策略中不对 token id 进行校验而且调用 canMatchMakerAsk 方法直接 revert。这说明使用这种策略的订单只能进行接受出价(bid),不能直接 listing。这跟 Blur 中的 bid 功能有关。 652 | 653 | ## 4. BlurPool 654 | 655 | BlurPool 可以简单看成 WETH 的一个特殊版本。他们的代码有很多地方都是一样的。 656 | 657 | BlurPool 特殊之处有以下两点。 658 | 659 | 1. BlurPool 的 `transferFrom()` 函数,只能被 BlurExchange 和 BlurSwap 这两个合约外部调用。 660 | 2. BlurPool 中没有 approve 相关的逻辑。 661 | 662 | 这些特性看起来很奇怪,其实这都是为 Blur Bid 功能来服务的。我们接下来详细看看 Bid 这一功能。 663 | 664 | ## 5. Blur Bid 665 | 666 | Blur 的 Bid 功能是一个设计相当巧妙的功能,甚至可以说 Blur 就是靠着这个功能完成了对 Opensea 的超越的。下面我们来看看具体的实现方法。 667 | 668 | ### 5.1 出价 669 | 670 | 首先我们先要明确一下 NFT 交易中存在的两个交易方向。 671 | 672 | 一个是挂单,也就是将自己拥有的 NFT 挂到交易市场。然后等待买家来购买。这种交易方向 Opensea 和 Blur 都称之为 Listing。 673 | 674 | 另一个是出价,也就是自己看上了某个 NFT,然后对这个 NFT 的拥有者发出一个购买请求。如果 NFT 的拥有者觉得价格合适就可以选择接受该出价。这种交易方向在 Opensea 上称之为 Offer,Blur 上称为 Bid。 675 | 676 | ### 5.2 Opensea Offer 677 | 678 | 在 Opensea 上对某个 NFT 进行 Offer 需要以下几个步骤: 679 | 680 | 1. 查询 WETH 余额,如果没有余额需要将 ETH 转换成 WETH。 681 | 2. 授权 WETH (只需要一次)。 682 | 3. 对 Offer 订单进行签名。 683 | 684 | 如果要取消 Offer 则需要调用 Seaport 合约的 `cancle()` 方法,将 Offer 对应的这个订单的取消状态写入到 Seaport 的成员变量 `_orderStatus` 中,来确保这个订单无法成交。这一步是必须的。具体原因可以看看 上面 BlurExchange 中的 `cancelledOrFilled` 成员变量的解释。由于涉及到修改合约中的数据,因此这一步是需要支付 gas 的。 685 | 686 | 这里还要解释一下 Opensea 的 Offer 为什么必须使用 WETH,而不是使用 ETH。 687 | 688 | 这是因为 WETH 原本作用是为了 ETH 包装成 ERC20 代币。然后就可以使用 ERC20 的一些功能。比如授权和转移。 689 | 690 | 如果在 Offer 订单中使用 ETH 的话,由于 ETH 无法进行授权操作,因此需要将 ETH 先转移到 Seaport 合约之中,然后再在成交的时候将 ETH 转移给 NFT 拥有者。如果用户想对多个 NFT 进行 Offer 的话就需要先提供出去足额的 ETH。这种方法显然占用了太多的用户的资金。 691 | 692 | 而使用 WETH 的话,用户只需要将 WETH 授权给 Seaport 合约,而不占用户的资金。用户可以对多个 NFT 进行 Offer。这无疑提高了用户的体验。 693 | 694 | ### 5.3 Blur Bid 695 | 696 | 在 Blur 上对 NFT 进行 Bid 需要以下几个步骤: 697 | 698 | 1. 选择某个 collection(Blur 不能对单个 NFT 进行 Bid,而是要对该 NFT 整个 collection 进行 bid)。 699 | 2. 查询 BlurPool 的余额,如果余额为空需要将 ETH 存入 BlurPool 中。 700 | 3. 对 Bid 订单进行签名。 701 | 702 | 如果想要取消 Bid 订单不需要支付 gas 就能取消。 703 | 704 | 这一点曾经让我很费解,毕竟 Opensea和 Blur 自己的交易合约中取消订单都是必须的(具体原因可以参考 cancelledOrFilled 的介绍)。其实我们只需要将上面内容中的 Oracle Authorization 和 SafeCollectionBidPolicyERC721 串起来就好理解了。根本原因是因为 Oracle Authentication 这一步骤中校验的签名是通过链下 Blur 中心化服务器上生成的。 705 | 706 | 我们通过一个例子来了解具体的实现方法。 707 | 708 | https://etherscan.io/tx/0xdd0058f2bfd06bfe6c265cfa01d8082333966a1c7d6a7bd430cfcf7c1ac9f223 709 | 710 | #### 5.3.1 解析参数 711 | 712 | 通过解析交易的数据我们可以了解到调用了这个交易是地址为0x56a6ff5eca020a8ffc67fe7682887ccae12ac2d3 的 EOA 账号调用 BlurExchange(0x000000000000ad05ccc4f10045630fb830b95127) 的 `execute()` 方法。 713 | 714 | 输入的参数如下 715 | 716 | ```json 717 | { 718 | "sell": { 719 | "order": { 720 | "trader": "0x56a6ff5eca020a8ffc67fe7682887ccae12ac2d3", 721 | "side": 1, 722 | "matchingPolicy": "0x0000000000b92d5d043faf7cecf7e2ee6aaed232", 723 | "collection": "0x39ee2c7b3cb80254225884ca001f57118c8f21b6", 724 | "tokenId": "3960", 725 | "amount": "1", 726 | "paymentToken": "0x0000000000a39bb272e79075ade125fd351887ac", 727 | "price": "2130000000000000000", 728 | "listingTime": "1677644849", 729 | "expirationTime": "1677652366", 730 | "fees": [ 731 | { 732 | "rate": 333, 733 | "recipient": "0xf3b985336fd574a0aa6e02cbe61c609861e923d6" 734 | } 735 | ], 736 | "salt": "114616841359518544842066950455752933050", 737 | "extraParams": "0x01" 738 | }, 739 | "v": 0, 740 | "r": "0x0000000000000000000000000000000000000000000000000000000000000000", 741 | "s": "0x0000000000000000000000000000000000000000000000000000000000000000", 742 | "extraSignature": "0x000000000000000000000000000000000000000000000000000000000000001bc9f1fde1f59a56a5343044a91c595b42f451bf8b13ba1fff391dda90221229f37912e2977ccca951de1612270e3acdb865103f9f44acd807c0f22792a9c07f4d", 743 | "signatureVersion": 0, 744 | "blockNumber": "16731714" 745 | }, 746 | "buy": { 747 | "order": { 748 | "trader": "0x14b6e5f84da2febd85d92dd9c2d4aa633cc65e30", 749 | "side": 0, 750 | "matchingPolicy": "0x0000000000b92d5d043faf7cecf7e2ee6aaed232", 751 | "collection": "0x39ee2c7b3cb80254225884ca001f57118c8f21b6", 752 | "tokenId": "0", 753 | "amount": "1", 754 | "paymentToken": "0x0000000000a39bb272e79075ade125fd351887ac", 755 | "price": "2130000000000000000", 756 | "listingTime": "1677644848", 757 | "expirationTime": "1709180849", 758 | "fees": [], 759 | "salt": "71987771138744249662594339974857147058", 760 | "extraParams": "0x01" 761 | }, 762 | "v": 27, 763 | "r": "0xc63f4bca4a5fb3802f7f956ad1358e5cf312646d1569f9b16543840444a12e68", 764 | "s": "0x1b95f680e148e81ce067764c48d25e68a9df2e2cda9b5c1ffe0e714fb1d126ec", 765 | "extraSignature": "0x000000000000000000000000000000000000000000000000000000000000001ca531657f4514cdffa5eff876ba26c236805c48116a9e9b4befc7c0ee8bf190c757ee01dace8d955ccda3dcee4e58c650dc0b3e29b8e9d3caa5a401144874530e", 766 | "signatureVersion": 0, 767 | "blockNumber": "16731714" 768 | } 769 | } 770 | ``` 771 | 772 | #### 5.3.2 分析交易方向 773 | 774 | sell 订单中 trader 地址与发起这笔交易的地址是一样的,因此这是一笔通过 Blur Bid 的交易。 775 | 776 | 因为这说明 buy 这个订单是首先生成的,也就是说先有地址为 0x14b6e5f84da2febd85d92dd9c2d4aa633cc65e30 的账户提出了一个报价,然后才有 0x56a6ff5eca020a8ffc67fe7682887ccae12ac2d3 的账户接受了这个报价,然后 0x56a6ff5eca020a8ffc67fe7682887ccae12ac2d3 这个账户调用 BlurExchange 进行成单的。 777 | 778 | 因此 Bid 订单就是 buy 这个参数中的订单。 779 | 780 | #### 5.3.3 分析 Bid 订单 781 | 782 | Bid 订单的参数中有两个方面需要特殊注意。 783 | 784 | ##### 5.3.3.1 MatchingPolicy 785 | 786 | 该订单中的 matchingPolicy 地址为 0x0000000000b92d5d043faf7cecf7e2ee6aaed232,是 SafeCollectionBidPolicyERC721 的交易策略。 787 | 788 | 我们上面了解过,这种交易策略不对 token id 进行校验而且调用该策略的 canMatchMakerAsk 方法直接 revert。这也正好应对了 Blur Bid 的交易要求。 789 | 790 | 因此该订单中的 tokenId 为 0,不是表示要购买 token id 为 0 的订单。 791 | 792 | ##### 5.3.3.1 Oracle Authentication 793 | 794 | 该订单中 extraParams 是 0x01,正好满足 Oracle Authorization 的条件。因此该订单需要进行 Oracle Authorization。 795 | 796 | ```solidity 797 | // 如果订单的 extraParams 中有数据,且第一个元素是 1 表示需要进行 Oracle Authorization 798 | if (order.order.extraParams.length > 0 && order.order.extraParams[0] == 0x01) { 799 | ... 800 | } 801 | ``` 802 | 803 | 我们在上面提到过 Oracle Authentication 这一步骤中校验的签名是通过链下 Blur 中心化服务器上生成的。如果提交 Bid 订单的用户在该订单成交之前告诉 Blur 取消该订单,则 Blur 就不再生成签名。这样一来这个 Bid 订单就无法在进行成交了。这也是为什么 Blur Bid 能不消耗 gas 进行取消的原因。 804 | 805 | ## 6. Opensea 和 版税 806 | 807 | 提到 Opensea 和 Blur 的版税,那可是一个相当精彩的故事。我们先来梳理一下事情的来龙去脉。 808 | 809 | 1. 2022 年 11 月,OpenSea 实施了一项新政策:如果项目方想要收取版税必须集成 Opensea 的链上版税强制执行工具。这个工具的本质是一个黑名单。集成该工具的 NFT 无法在一些零版税或者低版税的交易平台上交易。Blur 当时实行的是 0 版税 0 手续费的政策,因此也在这个黑名单中。(这个工具的具体实现可以参考我之前写过的这篇[文章](https://github.com/cryptochou/opensea-creator-fees))。 810 | 2. Opensea 实时这一策略的目的是司马昭之心路人皆知了,摆明了是针对 Blur 的。不过这一策略被证明是有效的。比如 Yuga 的 Sewer Pass 等新系列都选择与 OpenSea 结盟并阻止在 Blur 上的交易。 811 | 3. 这时候 Blur 承诺对新的 NFT 项目收取版税,并要求 Opensea 将他们从黑名单中移出。 812 | 4. 然而,OpenSea 回复说,其政策要求对所有 NFT项目征收版税,而不仅仅是实施黑名单的新 NFT项目。Blur 突围失败。 813 | 5. 这时候大多数的创作者都站队了 Opensea。 814 | 6. 本以为事情就这样下去了,然而精彩的来了。Blur 直接利用 Opensea 的 Seaport 合约创建了一个新的交易系统。对于集成了黑名单工具并且将 Blur 拉黑的 NFT 项目,Blur 直接通过 Seaport 进行交易。其他的 NFT项目则继续走 BlurExchange 进行交易(具体逻辑见下图)。你 Opensea 总不能把 Seaport 也拉进黑名单吧。Blur 突围成功。 815 | 7. Blur 的反击。随着 Blur 的空投,Blur 上的交易量大幅超过了 Opensea。这时候如果一个新的 NFT 项目把 Blur 加入黑名单的话就无法在 BlurExchange 上进行 Bid 了。而 Bid 在空投中计算积分的很重要的一步。这时候越来越多的 NFT 项目取消了 Blur 的黑名单。 816 | 8. Blur 的进一步反击。Blur 发出声明如果将 OpenSea 加入到黑名单中则可以在 Blur 上收取全部版税。同时可以增加空投奖励。与此同时,呼吁大家不要将 OpenSea 和 Blur 放到黑名单中。并向 Opensea 喊话取消 Blur 的黑名单。 817 | 9. Opensea 妥协了,将 Blur 移出了黑名单。并宣布限时 0 手续费的活动。 818 | 10. Blur win! 819 | 820 | ![source: https://twitter.com/pandajackson42/status/1620081586237767680/photo/1](FnuuanrXEAAYQch.jpeg) 821 | 822 | Twitter 上的 Panda Jackson 的几个配图很生动了描述了 Opensea 当前的处境。 823 | 824 | ![](FnutRuVWAAAOaVY.jpeg) 825 | ![](FnuuepzWAAE4Hx4.png) 826 | ![](FnuunmuXkAAS-bssq.jpeg) 827 | 828 | ## 7. 总结 829 | 830 | 整体看来 Blur 给人的感觉还是挺简洁的。 831 | 832 | 批量签名、预言机签名这些新功能会有有很大的应用空间。 833 | 834 | 目前 Blur 应该还是在很早期的阶段,毕竟他只支持了 ERC721 的限价单的交易方式。不支持 ERC1155 的交易,也不支持拍卖功能。当然这些应该都在他们的开发计划中。通过 MatchingPolicy 可以很方便的添加新的交易策略。这一点跟 BendDao 的 Execution Strategy 很像。猜测大概率是借鉴过来的。(关于 BendDao 更多的信息可以查看我的另一篇文章:[BendDAO-analysis](https://github.com/cryptochou/BendDAO-analysis#execution-strategy%E6%89%A7%E8%A1%8C%E7%AD%96%E7%95%A5)) 835 | 836 | 虽然是很早起的阶段,但是 Blur 目前的交易量大有赶超 Opensea 之势。应该是明确的空投预期起到了很大的作用。毕竟天下苦 Opensea 久矣。🤣 837 | 838 | --- 839 | 840 | 2023 03-03 Updata 841 | 842 | 5 个月过去了,Blur 的合约添加了一些新的功能,我这里重新梳理了一下 BlurExchange 合约。之前没怎么在意的 Oracle Authorization 没想到被 Blur 玩出了这么个玩法。 843 | 844 | 从目前来看可以说 Blur 是超越 Opensea 了,不论是从热度还是交易量来看。这期间他突破 Opensea 构筑的马奇诺防线的骚操作也让人直呼厉害。同时 Blur Bid 的功能也为 NFT 交易市场注入了大量的流动性。应该来说 Blur 为 NFT 交易市场带来了新的活力。 845 | 846 | 当然 Opensea 也并非一无是处。首先 Opensea 的界面相对 Blur 来说还是更容易让 NFT 新手的接受的。其次 Seaport 合约也可以称为是 NFT 基础建设级别的工具,支撑起了 ensvision 等一众垂直领域的 NFT 交易市场。这点也是很值得称道的。 847 | 848 | ## 8. 参考 849 | 850 | 1. https://twitter.com/pandajackson42/status/1620081518575235073 851 | 2. https://mirror.xyz/blurdao.eth/vYOjzk4cQCQ7AtuJWWiZPoNZ04YKQmTMsos0NNq_hYs 852 | 853 | 如果感觉本文对您有帮助的话,欢迎打赏:0x1E1eFeb696Bc8F3336852D9FB2487FE6590362BF。 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | -------------------------------------------------------------------------------- /contracts/BlurExchange/BlurExchange.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 7 | 8 | import "./lib/ReentrancyGuarded.sol"; 9 | import "./lib/EIP712.sol"; 10 | import "./lib/MerkleVerifier.sol"; 11 | import "./interfaces/IBlurExchange.sol"; 12 | import "./interfaces/IExecutionDelegate.sol"; 13 | import "./interfaces/IPolicyManager.sol"; 14 | import "./interfaces/IMatchingPolicy.sol"; 15 | import { 16 | Side, 17 | SignatureVersion, 18 | AssetType, 19 | Fee, 20 | Order, 21 | Input 22 | } from "./lib/OrderStructs.sol"; 23 | 24 | /** 25 | * @title BlurExchange 26 | * @dev Core Blur exchange contract 27 | */ 28 | contract BlurExchange is IBlurExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, UUPSUpgradeable { 29 | 30 | /* Auth */ 31 | uint256 public isOpen; 32 | 33 | modifier whenOpen() { 34 | require(isOpen == 1, "Closed"); 35 | _; 36 | } 37 | 38 | event Opened(); 39 | event Closed(); 40 | 41 | function open() external onlyOwner { 42 | isOpen = 1; 43 | emit Opened(); 44 | } 45 | function close() external onlyOwner { 46 | isOpen = 0; 47 | emit Closed(); 48 | } 49 | 50 | // required by the OZ UUPS module 51 | function _authorizeUpgrade(address) internal override onlyOwner {} 52 | 53 | 54 | /* Constants */ 55 | string public constant NAME = "Blur Exchange"; 56 | string public constant VERSION = "1.0"; 57 | uint256 public constant INVERSE_BASIS_POINT = 10_000; 58 | address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 59 | 60 | 61 | /* Variables */ 62 | IExecutionDelegate public executionDelegate; 63 | IPolicyManager public policyManager; 64 | address public oracle; 65 | uint256 public blockRange; 66 | 67 | 68 | /* Storage */ 69 | mapping(bytes32 => bool) public cancelledOrFilled; 70 | mapping(address => uint256) public nonces; 71 | 72 | 73 | /* Events */ 74 | event OrdersMatched( 75 | address indexed maker, 76 | address indexed taker, 77 | Order sell, 78 | bytes32 sellHash, 79 | Order buy, 80 | bytes32 buyHash 81 | ); 82 | 83 | event OrderCancelled(bytes32 hash); 84 | event NonceIncremented(address indexed trader, uint256 newNonce); 85 | 86 | event NewExecutionDelegate(IExecutionDelegate indexed executionDelegate); 87 | event NewPolicyManager(IPolicyManager indexed policyManager); 88 | event NewOracle(address indexed oracle); 89 | event NewBlockRange(uint256 blockRange); 90 | 91 | constructor() { 92 | _disableInitializers(); 93 | } 94 | 95 | /* Constructor (for ERC1967) */ 96 | function initialize( 97 | IExecutionDelegate _executionDelegate, 98 | IPolicyManager _policyManager, 99 | address _oracle, 100 | uint _blockRange 101 | ) external initializer { 102 | __Ownable_init(); 103 | isOpen = 1; 104 | 105 | DOMAIN_SEPARATOR = _hashDomain(EIP712Domain({ 106 | name : NAME, 107 | version : VERSION, 108 | chainId : block.chainid, 109 | verifyingContract : address(this) 110 | })); 111 | 112 | executionDelegate = _executionDelegate; 113 | policyManager = _policyManager; 114 | oracle = _oracle; 115 | blockRange = _blockRange; 116 | } 117 | 118 | 119 | /* External Functions */ 120 | 121 | /** 122 | * @dev Match two orders, ensuring validity of the match, and execute all associated state transitions. Protected against reentrancy by a contract-global lock. 123 | * @param sell Sell input 124 | * @param buy Buy input 125 | */ 126 | function execute(Input calldata sell, Input calldata buy) 127 | external 128 | payable 129 | reentrancyGuard 130 | whenOpen 131 | { 132 | require(sell.order.side == Side.Sell); 133 | 134 | bytes32 sellHash = _hashOrder(sell.order, nonces[sell.order.trader]); 135 | bytes32 buyHash = _hashOrder(buy.order, nonces[buy.order.trader]); 136 | 137 | require(_validateOrderParameters(sell.order, sellHash), "Sell has invalid parameters"); 138 | require(_validateOrderParameters(buy.order, buyHash), "Buy has invalid parameters"); 139 | 140 | require(_validateSignatures(sell, sellHash), "Sell failed authorization"); 141 | require(_validateSignatures(buy, buyHash), "Buy failed authorization"); 142 | 143 | (uint256 price, uint256 tokenId, uint256 amount, AssetType assetType) = _canMatchOrders(sell.order, buy.order); 144 | 145 | /* Mark orders as filled. */ 146 | cancelledOrFilled[sellHash] = true; 147 | cancelledOrFilled[buyHash] = true; 148 | 149 | _executeFundsTransfer( 150 | sell.order.trader, 151 | buy.order.trader, 152 | sell.order.paymentToken, 153 | sell.order.fees, 154 | price 155 | ); 156 | _executeTokenTransfer( 157 | sell.order.collection, 158 | sell.order.trader, 159 | buy.order.trader, 160 | tokenId, 161 | amount, 162 | assetType 163 | ); 164 | 165 | emit OrdersMatched( 166 | sell.order.listingTime <= buy.order.listingTime ? sell.order.trader : buy.order.trader, 167 | sell.order.listingTime > buy.order.listingTime ? sell.order.trader : buy.order.trader, 168 | sell.order, 169 | sellHash, 170 | buy.order, 171 | buyHash 172 | ); 173 | } 174 | 175 | /** 176 | * @dev Cancel an order, preventing it from being matched. Must be called by the trader of the order 177 | * @param order Order to cancel 178 | */ 179 | function cancelOrder(Order calldata order) public { 180 | /* Assert sender is authorized to cancel order. */ 181 | require(msg.sender == order.trader); 182 | 183 | bytes32 hash = _hashOrder(order, nonces[order.trader]); 184 | 185 | require(cancelledOrFilled[hash] == false, "Order already cancelled or filled"); 186 | 187 | if (!cancelledOrFilled[hash]) { 188 | /* Mark order as cancelled, preventing it from being matched. */ 189 | cancelledOrFilled[hash] = true; 190 | emit OrderCancelled(hash); 191 | } 192 | } 193 | 194 | /** 195 | * @dev Cancel multiple orders 196 | * @param orders Orders to cancel 197 | */ 198 | function cancelOrders(Order[] calldata orders) external { 199 | for (uint8 i = 0; i < orders.length; i++) { 200 | cancelOrder(orders[i]); 201 | } 202 | } 203 | 204 | /** 205 | * @dev Cancel all current orders for a user, preventing them from being matched. Must be called by the trader of the order 206 | */ 207 | function incrementNonce() external { 208 | nonces[msg.sender] += 1; 209 | emit NonceIncremented(msg.sender, nonces[msg.sender]); 210 | } 211 | 212 | 213 | /* Setters */ 214 | 215 | function setExecutionDelegate(IExecutionDelegate _executionDelegate) 216 | external 217 | onlyOwner 218 | { 219 | require(address(_executionDelegate) != address(0), "Address cannot be zero"); 220 | executionDelegate = _executionDelegate; 221 | emit NewExecutionDelegate(executionDelegate); 222 | } 223 | 224 | function setPolicyManager(IPolicyManager _policyManager) 225 | external 226 | onlyOwner 227 | { 228 | require(address(_policyManager) != address(0), "Address cannot be zero"); 229 | policyManager = _policyManager; 230 | emit NewPolicyManager(policyManager); 231 | } 232 | 233 | function setOracle(address _oracle) 234 | external 235 | onlyOwner 236 | { 237 | require(_oracle != address(0), "Address cannot be zero"); 238 | oracle = _oracle; 239 | emit NewOracle(oracle); 240 | } 241 | 242 | function setBlockRange(uint256 _blockRange) 243 | external 244 | onlyOwner 245 | { 246 | blockRange = _blockRange; 247 | emit NewBlockRange(blockRange); 248 | } 249 | 250 | 251 | /* Internal Functions */ 252 | 253 | /** 254 | * @dev Verify the validity of the order parameters 255 | * @param order order 256 | * @param orderHash hash of order 257 | */ 258 | function _validateOrderParameters(Order calldata order, bytes32 orderHash) 259 | internal 260 | view 261 | returns (bool) 262 | { 263 | return ( 264 | /* Order must have a trader. */ 265 | (order.trader != address(0)) && 266 | /* Order must not be cancelled or filled. */ 267 | (cancelledOrFilled[orderHash] == false) && 268 | /* Order must be settleable. */ 269 | _canSettleOrder(order.listingTime, order.expirationTime) 270 | ); 271 | } 272 | 273 | /** 274 | * @dev Check if the order can be settled at the current timestamp 275 | * @param listingTime order listing time 276 | * @param expirationTime order expiration time 277 | */ 278 | function _canSettleOrder(uint256 listingTime, uint256 expirationTime) 279 | view 280 | internal 281 | returns (bool) 282 | { 283 | return (listingTime < block.timestamp) && (expirationTime == 0 || block.timestamp < expirationTime); 284 | } 285 | 286 | /** 287 | * @dev Verify the validity of the signatures 288 | * @param order order 289 | * @param orderHash hash of order 290 | */ 291 | function _validateSignatures(Input calldata order, bytes32 orderHash) 292 | internal 293 | view 294 | returns (bool) 295 | { 296 | 297 | if (order.order.trader == msg.sender) { 298 | return true; 299 | } 300 | 301 | /* Check user authorization. */ 302 | if ( 303 | !_validateUserAuthorization( 304 | orderHash, 305 | order.order.trader, 306 | order.v, 307 | order.r, 308 | order.s, 309 | order.signatureVersion, 310 | order.extraSignature 311 | ) 312 | ) { 313 | return false; 314 | } 315 | 316 | if (order.order.expirationTime == 0) { 317 | /* Check oracle authorization. */ 318 | require(block.number - order.blockNumber < blockRange, "Signed block number out of range"); 319 | if ( 320 | !_validateOracleAuthorization( 321 | orderHash, 322 | order.signatureVersion, 323 | order.extraSignature, 324 | order.blockNumber 325 | ) 326 | ) { 327 | return false; 328 | } 329 | } 330 | 331 | return true; 332 | } 333 | 334 | /** 335 | * @dev Verify the validity of the user signature 336 | * @param orderHash hash of the order 337 | * @param trader order trader who should be the signer 338 | * @param v v 339 | * @param r r 340 | * @param s s 341 | * @param signatureVersion signature version 342 | * @param extraSignature packed merkle path 343 | */ 344 | function _validateUserAuthorization( 345 | bytes32 orderHash, 346 | address trader, 347 | uint8 v, 348 | bytes32 r, 349 | bytes32 s, 350 | SignatureVersion signatureVersion, 351 | bytes calldata extraSignature 352 | ) internal view returns (bool) { 353 | bytes32 hashToSign; 354 | if (signatureVersion == SignatureVersion.Single) { 355 | /* Single-listing authentication: Order signed by trader */ 356 | hashToSign = _hashToSign(orderHash); 357 | } else if (signatureVersion == SignatureVersion.Bulk) { 358 | /* Bulk-listing authentication: Merkle root of orders signed by trader */ 359 | (bytes32[] memory merklePath) = abi.decode(extraSignature, (bytes32[])); 360 | 361 | bytes32 computedRoot = MerkleVerifier._computeRoot(orderHash, merklePath); 362 | hashToSign = _hashToSignRoot(computedRoot); 363 | } 364 | 365 | return _verify(trader, hashToSign, v, r, s); 366 | } 367 | 368 | /** 369 | * @dev Verify the validity of oracle signature 370 | * @param orderHash hash of the order 371 | * @param signatureVersion signature version 372 | * @param extraSignature packed oracle signature 373 | * @param blockNumber block number used in oracle signature 374 | */ 375 | function _validateOracleAuthorization( 376 | bytes32 orderHash, 377 | SignatureVersion signatureVersion, 378 | bytes calldata extraSignature, 379 | uint256 blockNumber 380 | ) internal view returns (bool) { 381 | bytes32 oracleHash = _hashToSignOracle(orderHash, blockNumber); 382 | 383 | uint8 v; bytes32 r; bytes32 s; 384 | if (signatureVersion == SignatureVersion.Single) { 385 | (v, r, s) = abi.decode(extraSignature, (uint8, bytes32, bytes32)); 386 | } else if (signatureVersion == SignatureVersion.Bulk) { 387 | /* If the signature was a bulk listing the merkle path must be unpacked before the oracle signature. */ 388 | (bytes32[] memory merklePath, uint8 _v, bytes32 _r, bytes32 _s) = abi.decode(extraSignature, (bytes32[], uint8, bytes32, bytes32)); 389 | v = _v; r = _r; s = _s; 390 | } 391 | 392 | return _verify(oracle, oracleHash, v, r, s); 393 | } 394 | 395 | /** 396 | * @dev Verify ECDSA signature 397 | * @param signer Expected signer 398 | * @param digest Signature preimage 399 | * @param v v 400 | * @param r r 401 | * @param s s 402 | */ 403 | function _verify( 404 | address signer, 405 | bytes32 digest, 406 | uint8 v, 407 | bytes32 r, 408 | bytes32 s 409 | ) internal pure returns (bool) { 410 | require(v == 27 || v == 28, "Invalid v parameter"); 411 | address recoveredSigner = ecrecover(digest, v, r, s); 412 | if (recoveredSigner == address(0)) { 413 | return false; 414 | } else { 415 | return signer == recoveredSigner; 416 | } 417 | } 418 | 419 | /** 420 | * @dev Call the matching policy to check orders can be matched and get execution parameters 421 | * @param sell sell order 422 | * @param buy buy order 423 | */ 424 | function _canMatchOrders(Order calldata sell, Order calldata buy) 425 | internal 426 | view 427 | returns (uint256 price, uint256 tokenId, uint256 amount, AssetType assetType) 428 | { 429 | bool canMatch; 430 | if (sell.listingTime <= buy.listingTime) { 431 | /* Seller is maker. */ 432 | require(policyManager.isPolicyWhitelisted(sell.matchingPolicy), "Policy is not whitelisted"); 433 | (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(sell.matchingPolicy).canMatchMakerAsk(sell, buy); 434 | } else { 435 | /* Buyer is maker. */ 436 | require(policyManager.isPolicyWhitelisted(buy.matchingPolicy), "Policy is not whitelisted"); 437 | (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(buy.matchingPolicy).canMatchMakerBid(buy, sell); 438 | } 439 | require(canMatch, "Orders cannot be matched"); 440 | 441 | return (price, tokenId, amount, assetType); 442 | } 443 | 444 | /** 445 | * @dev Execute all ERC20 token / ETH transfers associated with an order match (fees and buyer => seller transfer) 446 | * @param seller seller 447 | * @param buyer buyer 448 | * @param paymentToken payment token 449 | * @param fees fees 450 | * @param price price 451 | */ 452 | function _executeFundsTransfer( 453 | address seller, 454 | address buyer, 455 | address paymentToken, 456 | Fee[] calldata fees, 457 | uint256 price 458 | ) internal { 459 | if (paymentToken == address(0) && msg.sender == buyer) { 460 | require(msg.value == price, "Message value doesn't equal matching price"); 461 | } else { 462 | require(msg.value == 0, "ETH should not be sent"); 463 | } 464 | 465 | /* Take fee. */ 466 | uint256 receiveAmount = _transferFees(fees, paymentToken, buyer, price); 467 | 468 | /* Transfer remainder to seller. */ 469 | _transferTo(paymentToken, buyer, seller, receiveAmount); 470 | } 471 | 472 | /** 473 | * @dev Charge a fee in ETH or WETH 474 | * @param fees fees to distribute 475 | * @param paymentToken address of token to pay in 476 | * @param from address to charge fees 477 | * @param price price of token 478 | */ 479 | function _transferFees( 480 | Fee[] calldata fees, 481 | address paymentToken, 482 | address from, 483 | uint256 price 484 | ) internal returns (uint256) { 485 | uint256 totalFee = 0; 486 | for (uint8 i = 0; i < fees.length; i++) { 487 | uint256 fee = (price * fees[i].rate) / INVERSE_BASIS_POINT; 488 | _transferTo(paymentToken, from, fees[i].recipient, fee); 489 | totalFee += fee; 490 | } 491 | 492 | require(totalFee <= price, "Total amount of fees are more than the price"); 493 | 494 | /* Amount that will be received by seller. */ 495 | uint256 receiveAmount = price - totalFee; 496 | return (receiveAmount); 497 | } 498 | 499 | /** 500 | * @dev Transfer amount in ETH or WETH 501 | * @param paymentToken address of token to pay in 502 | * @param from token sender 503 | * @param to token recipient 504 | * @param amount amount to transfer 505 | */ 506 | function _transferTo( 507 | address paymentToken, 508 | address from, 509 | address to, 510 | uint256 amount 511 | ) internal { 512 | if (amount == 0) { 513 | return; 514 | } 515 | 516 | if (paymentToken == address(0)) { 517 | /* Transfer funds in ETH. */ 518 | require(to != address(0), "Transfer to zero address"); 519 | (bool success,) = payable(to).call{value: amount}(""); 520 | require(success, "ETH transfer failed"); 521 | } else if (paymentToken == WETH) { 522 | /* Transfer funds in WETH. */ 523 | executionDelegate.transferERC20(WETH, from, to, amount); 524 | } else { 525 | revert("Invalid payment token"); 526 | } 527 | } 528 | 529 | /** 530 | * @dev Execute call through delegate proxy 531 | * @param collection collection contract address 532 | * @param from seller address 533 | * @param to buyer address 534 | * @param tokenId tokenId 535 | * @param assetType asset type of the token 536 | */ 537 | function _executeTokenTransfer( 538 | address collection, 539 | address from, 540 | address to, 541 | uint256 tokenId, 542 | uint256 amount, 543 | AssetType assetType 544 | ) internal { 545 | /* Assert collection exists. */ 546 | require(_exists(collection), "Collection does not exist"); 547 | 548 | /* Call execution delegate. */ 549 | if (assetType == AssetType.ERC721) { 550 | executionDelegate.transferERC721(collection, from, to, tokenId); 551 | } else if (assetType == AssetType.ERC1155) { 552 | executionDelegate.transferERC1155(collection, from, to, tokenId, amount); 553 | } 554 | } 555 | 556 | /** 557 | * @dev Determine if the given address exists 558 | * @param what address to check 559 | */ 560 | function _exists(address what) 561 | internal 562 | view 563 | returns (bool) 564 | { 565 | uint size; 566 | assembly { 567 | size := extcodesize(what) 568 | } 569 | return size > 0; 570 | } 571 | } 572 | -------------------------------------------------------------------------------- /contracts/BlurExchange/BlurExchange_new.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 7 | 8 | import "./lib/ReentrancyGuarded.sol"; 9 | import "./lib/EIP712.sol"; 10 | import "./lib/MerkleVerifier.sol"; 11 | import "./interfaces/IBlurExchange.sol"; 12 | import "./interfaces/IBlurPool.sol"; 13 | import "./interfaces/IExecutionDelegate.sol"; 14 | import "./interfaces/IPolicyManager.sol"; 15 | import "./interfaces/IMatchingPolicy.sol"; 16 | import { 17 | Side, 18 | SignatureVersion, 19 | AssetType, 20 | Fee, 21 | Order, 22 | Input, 23 | Execution 24 | } from "./lib/OrderStructs.sol"; 25 | 26 | /** 27 | * @title BlurExchange 28 | * @dev Core Blur exchange contract 29 | */ 30 | contract BlurExchange is IBlurExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, UUPSUpgradeable { 31 | 32 | /* Auth */ 33 | uint256 public isOpen; 34 | 35 | modifier whenOpen() { 36 | require(isOpen == 1, "Closed"); 37 | _; 38 | } 39 | 40 | modifier setupExecution() { 41 | require(!isInternal, "Unsafe call"); // add redundant re-entrancy check for clarity 42 | remainingETH = msg.value; 43 | isInternal = true; 44 | _; 45 | remainingETH = 0; 46 | isInternal = false; 47 | } 48 | 49 | modifier internalCall() { 50 | require(isInternal, "Unsafe call"); 51 | _; 52 | } 53 | 54 | event Opened(); 55 | event Closed(); 56 | 57 | function open() external onlyOwner { 58 | isOpen = 1; 59 | emit Opened(); 60 | } 61 | function close() external onlyOwner { 62 | isOpen = 0; 63 | emit Closed(); 64 | } 65 | 66 | // required by the OZ UUPS module 67 | function _authorizeUpgrade(address) internal override onlyOwner {} 68 | 69 | 70 | /* Constants */ 71 | string public constant NAME = "Blur Exchange"; 72 | string public constant VERSION = "1.0"; 73 | uint256 public constant INVERSE_BASIS_POINT = 10_000; 74 | address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 75 | address public constant POOL = 0x0000000000A39bb272e79075ade125fd351887Ac; 76 | uint256 private constant MAX_FEE_RATE = 250; 77 | 78 | 79 | /* Variables */ 80 | IExecutionDelegate public executionDelegate; 81 | IPolicyManager public policyManager; 82 | address public oracle; 83 | uint256 public blockRange; 84 | 85 | /* Storage */ 86 | mapping(bytes32 => bool) public cancelledOrFilled; 87 | mapping(address => uint256) public nonces; 88 | 89 | bool public isInternal = false; 90 | uint256 public remainingETH = 0; 91 | 92 | 93 | /* Governance Variables */ 94 | uint256 public feeRate; 95 | address public feeRecipient; 96 | 97 | address public governor; 98 | 99 | 100 | /* Events */ 101 | event OrdersMatched( 102 | address indexed maker, 103 | address indexed taker, 104 | Order sell, 105 | bytes32 sellHash, 106 | Order buy, 107 | bytes32 buyHash 108 | ); 109 | 110 | event OrderCancelled(bytes32 hash); 111 | event NonceIncremented(address indexed trader, uint256 newNonce); 112 | 113 | event NewExecutionDelegate(IExecutionDelegate indexed executionDelegate); 114 | event NewPolicyManager(IPolicyManager indexed policyManager); 115 | event NewOracle(address indexed oracle); 116 | event NewBlockRange(uint256 blockRange); 117 | event NewFeeRate(uint256 feeRate); 118 | event NewFeeRecipient(address feeRecipient); 119 | event NewGovernor(address governor); 120 | 121 | constructor() { 122 | _disableInitializers(); 123 | } 124 | 125 | /* Constructor (for ERC1967) */ 126 | function initialize( 127 | IExecutionDelegate _executionDelegate, 128 | IPolicyManager _policyManager, 129 | address _oracle, 130 | uint _blockRange 131 | ) external initializer { 132 | __Ownable_init(); 133 | isOpen = 1; 134 | 135 | DOMAIN_SEPARATOR = _hashDomain(EIP712Domain({ 136 | name : NAME, 137 | version : VERSION, 138 | chainId : block.chainid, 139 | verifyingContract : address(this) 140 | })); 141 | 142 | executionDelegate = _executionDelegate; 143 | policyManager = _policyManager; 144 | oracle = _oracle; 145 | blockRange = _blockRange; 146 | } 147 | 148 | /* External Functions */ 149 | /** 150 | * @dev _execute wrapper 151 | * @param sell Sell input 152 | * @param buy Buy input 153 | */ 154 | function execute(Input calldata sell, Input calldata buy) 155 | external 156 | payable 157 | whenOpen 158 | setupExecution 159 | { 160 | _execute(sell, buy); 161 | _returnDust(); 162 | } 163 | 164 | /** 165 | * @dev Bulk execute multiple matches 166 | * @param executions Potential buy/sell matches 167 | */ 168 | function bulkExecute(Execution[] calldata executions) 169 | external 170 | payable 171 | whenOpen 172 | setupExecution 173 | { 174 | /* 175 | REFERENCE 176 | uint256 executionsLength = executions.length; 177 | for (uint8 i=0; i < executionsLength; i++) { 178 | bytes memory data = abi.encodeWithSelector(this._execute.selector, executions[i].sell, executions[i].buy); 179 | (bool success,) = address(this).delegatecall(data); 180 | } 181 | _returnDust(remainingETH); 182 | */ 183 | uint256 executionsLength = executions.length; 184 | 185 | if (executionsLength == 0) { 186 | revert("No orders to execute"); 187 | } 188 | for (uint8 i = 0; i < executionsLength; i++) { 189 | assembly { 190 | let memPointer := mload(0x40) 191 | 192 | let order_location := calldataload(add(executions.offset, mul(i, 0x20))) 193 | let order_pointer := add(executions.offset, order_location) 194 | 195 | let size 196 | switch eq(add(i, 0x01), executionsLength) 197 | case 1 { 198 | size := sub(calldatasize(), order_pointer) 199 | } 200 | default { 201 | let next_order_location := calldataload(add(executions.offset, mul(add(i, 0x01), 0x20))) 202 | let next_order_pointer := add(executions.offset, next_order_location) 203 | size := sub(next_order_pointer, order_pointer) 204 | } 205 | 206 | mstore(memPointer, 0xe04d94ae00000000000000000000000000000000000000000000000000000000) // _execute 207 | calldatacopy(add(0x04, memPointer), order_pointer, size) 208 | // must be put in separate transaction to bypass failed executions 209 | // must be put in delegatecall to maintain the authorization from the caller 210 | let result := delegatecall(gas(), address(), memPointer, add(size, 0x04), 0, 0) 211 | } 212 | } 213 | _returnDust(); 214 | } 215 | 216 | /** 217 | * @dev Match two orders, ensuring validity of the match, and execute all associated state transitions. Must be called internally. 218 | * @param sell Sell input 219 | * @param buy Buy input 220 | */ 221 | function _execute(Input calldata sell, Input calldata buy) 222 | public 223 | payable 224 | internalCall 225 | reentrancyGuard // move re-entrancy check for clarity 226 | { 227 | require(sell.order.side == Side.Sell); 228 | 229 | bytes32 sellHash = _hashOrder(sell.order, nonces[sell.order.trader]); 230 | bytes32 buyHash = _hashOrder(buy.order, nonces[buy.order.trader]); 231 | 232 | require(_validateOrderParameters(sell.order, sellHash), "Sell has invalid parameters"); 233 | require(_validateOrderParameters(buy.order, buyHash), "Buy has invalid parameters"); 234 | 235 | require(_validateSignatures(sell, sellHash), "Sell failed authorization"); 236 | require(_validateSignatures(buy, buyHash), "Buy failed authorization"); 237 | 238 | (uint256 price, uint256 tokenId, uint256 amount, AssetType assetType) = _canMatchOrders(sell.order, buy.order); 239 | 240 | /* Mark orders as filled. */ 241 | cancelledOrFilled[sellHash] = true; 242 | cancelledOrFilled[buyHash] = true; 243 | 244 | _executeFundsTransfer( 245 | sell.order.trader, 246 | buy.order.trader, 247 | sell.order.paymentToken, 248 | sell.order.fees, 249 | buy.order.fees, 250 | price 251 | ); 252 | _executeTokenTransfer( 253 | sell.order.collection, 254 | sell.order.trader, 255 | buy.order.trader, 256 | tokenId, 257 | amount, 258 | assetType 259 | ); 260 | 261 | emit OrdersMatched( 262 | sell.order.listingTime <= buy.order.listingTime ? sell.order.trader : buy.order.trader, 263 | sell.order.listingTime > buy.order.listingTime ? sell.order.trader : buy.order.trader, 264 | sell.order, 265 | sellHash, 266 | buy.order, 267 | buyHash 268 | ); 269 | } 270 | 271 | /** 272 | * @dev Cancel an order, preventing it from being matched. Must be called by the trader of the order 273 | * @param order Order to cancel 274 | */ 275 | function cancelOrder(Order calldata order) public { 276 | /* Assert sender is authorized to cancel order. */ 277 | require(msg.sender == order.trader, "Not sent by trader"); 278 | 279 | bytes32 hash = _hashOrder(order, nonces[order.trader]); 280 | 281 | require(!cancelledOrFilled[hash], "Order cancelled or filled"); 282 | 283 | /* Mark order as cancelled, preventing it from being matched. */ 284 | cancelledOrFilled[hash] = true; 285 | emit OrderCancelled(hash); 286 | } 287 | 288 | /** 289 | * @dev Cancel multiple orders 290 | * @param orders Orders to cancel 291 | */ 292 | function cancelOrders(Order[] calldata orders) external { 293 | for (uint8 i = 0; i < orders.length; i++) { 294 | cancelOrder(orders[i]); 295 | } 296 | } 297 | 298 | /** 299 | * @dev Cancel all current orders for a user, preventing them from being matched. Must be called by the trader of the order 300 | */ 301 | function incrementNonce() external { 302 | nonces[msg.sender] += 1; 303 | emit NonceIncremented(msg.sender, nonces[msg.sender]); 304 | } 305 | 306 | 307 | /* Setters */ 308 | 309 | function setExecutionDelegate(IExecutionDelegate _executionDelegate) 310 | external 311 | onlyOwner 312 | { 313 | require(address(_executionDelegate) != address(0), "Address cannot be zero"); 314 | executionDelegate = _executionDelegate; 315 | emit NewExecutionDelegate(executionDelegate); 316 | } 317 | 318 | function setPolicyManager(IPolicyManager _policyManager) 319 | external 320 | onlyOwner 321 | { 322 | require(address(_policyManager) != address(0), "Address cannot be zero"); 323 | policyManager = _policyManager; 324 | emit NewPolicyManager(policyManager); 325 | } 326 | 327 | function setOracle(address _oracle) 328 | external 329 | onlyOwner 330 | { 331 | require(_oracle != address(0), "Address cannot be zero"); 332 | oracle = _oracle; 333 | emit NewOracle(oracle); 334 | } 335 | 336 | function setBlockRange(uint256 _blockRange) 337 | external 338 | onlyOwner 339 | { 340 | blockRange = _blockRange; 341 | emit NewBlockRange(blockRange); 342 | } 343 | 344 | function setGovernor(address _governor) 345 | external 346 | onlyOwner 347 | { 348 | governor = _governor; 349 | emit NewGovernor(governor); 350 | } 351 | 352 | function setFeeRate(uint256 _feeRate) 353 | external 354 | { 355 | require(msg.sender == governor, "Fee rate can only be set by governor"); 356 | require(_feeRate <= MAX_FEE_RATE, "Fee cannot be more than 2.5%"); 357 | feeRate = _feeRate; 358 | emit NewFeeRate(feeRate); 359 | } 360 | 361 | function setFeeRecipient(address _feeRecipient) 362 | external 363 | onlyOwner 364 | { 365 | feeRecipient = _feeRecipient; 366 | emit NewFeeRecipient(feeRecipient); 367 | } 368 | 369 | 370 | /* Internal Functions */ 371 | 372 | /** 373 | * @dev Verify the validity of the order parameters 374 | * @param order order 375 | * @param orderHash hash of order 376 | */ 377 | function _validateOrderParameters(Order calldata order, bytes32 orderHash) 378 | internal 379 | view 380 | returns (bool) 381 | { 382 | return ( 383 | /* Order must have a trader. */ 384 | (order.trader != address(0)) && 385 | /* Order must not be cancelled or filled. */ 386 | (!cancelledOrFilled[orderHash]) && 387 | /* Order must be settleable. */ 388 | (order.listingTime < block.timestamp) && 389 | (block.timestamp < order.expirationTime) 390 | ); 391 | } 392 | 393 | /** 394 | * @dev Verify the validity of the signatures 395 | * @param order order 396 | * @param orderHash hash of order 397 | */ 398 | function _validateSignatures(Input calldata order, bytes32 orderHash) 399 | internal 400 | view 401 | returns (bool) 402 | { 403 | 404 | if (order.order.extraParams.length > 0 && order.order.extraParams[0] == 0x01) { 405 | /* Check oracle authorization. */ 406 | require(block.number - order.blockNumber < blockRange, "Signed block number out of range"); 407 | if ( 408 | !_validateOracleAuthorization( 409 | orderHash, 410 | order.signatureVersion, 411 | order.extraSignature, 412 | order.blockNumber 413 | ) 414 | ) { 415 | return false; 416 | } 417 | } 418 | 419 | if (order.order.trader == msg.sender) { 420 | return true; 421 | } 422 | 423 | /* Check user authorization. */ 424 | if ( 425 | !_validateUserAuthorization( 426 | orderHash, 427 | order.order.trader, 428 | order.v, 429 | order.r, 430 | order.s, 431 | order.signatureVersion, 432 | order.extraSignature 433 | ) 434 | ) { 435 | return false; 436 | } 437 | 438 | return true; 439 | } 440 | 441 | /** 442 | * @dev Verify the validity of the user signature 443 | * @param orderHash hash of the order 444 | * @param trader order trader who should be the signer 445 | * @param v v 446 | * @param r r 447 | * @param s s 448 | * @param signatureVersion signature version 449 | * @param extraSignature packed merkle path 450 | */ 451 | function _validateUserAuthorization( 452 | bytes32 orderHash, 453 | address trader, 454 | uint8 v, 455 | bytes32 r, 456 | bytes32 s, 457 | SignatureVersion signatureVersion, 458 | bytes calldata extraSignature 459 | ) internal view returns (bool) { 460 | bytes32 hashToSign; 461 | if (signatureVersion == SignatureVersion.Single) { 462 | /* Single-listing authentication: Order signed by trader */ 463 | hashToSign = _hashToSign(orderHash); 464 | } else if (signatureVersion == SignatureVersion.Bulk) { 465 | /* Bulk-listing authentication: Merkle root of orders signed by trader */ 466 | (bytes32[] memory merklePath) = abi.decode(extraSignature, (bytes32[])); 467 | 468 | bytes32 computedRoot = MerkleVerifier._computeRoot(orderHash, merklePath); 469 | hashToSign = _hashToSignRoot(computedRoot); 470 | } 471 | 472 | return _verify(trader, hashToSign, v, r, s); 473 | } 474 | 475 | /** 476 | * @dev Verify the validity of oracle signature 477 | * @param orderHash hash of the order 478 | * @param signatureVersion signature version 479 | * @param extraSignature packed oracle signature 480 | * @param blockNumber block number used in oracle signature 481 | */ 482 | function _validateOracleAuthorization( 483 | bytes32 orderHash, 484 | SignatureVersion signatureVersion, 485 | bytes calldata extraSignature, 486 | uint256 blockNumber 487 | ) internal view returns (bool) { 488 | bytes32 oracleHash = _hashToSignOracle(orderHash, blockNumber); 489 | 490 | uint8 v; bytes32 r; bytes32 s; 491 | if (signatureVersion == SignatureVersion.Single) { 492 | assembly { 493 | v := calldataload(extraSignature.offset) 494 | r := calldataload(add(extraSignature.offset, 0x20)) 495 | s := calldataload(add(extraSignature.offset, 0x40)) 496 | } 497 | /* 498 | REFERENCE 499 | (v, r, s) = abi.decode(extraSignature, (uint8, bytes32, bytes32)); 500 | */ 501 | } else if (signatureVersion == SignatureVersion.Bulk) { 502 | /* If the signature was a bulk listing the merkle path must be unpacked before the oracle signature. */ 503 | assembly { 504 | v := calldataload(add(extraSignature.offset, 0x20)) 505 | r := calldataload(add(extraSignature.offset, 0x40)) 506 | s := calldataload(add(extraSignature.offset, 0x60)) 507 | } 508 | /* 509 | REFERENCE 510 | uint8 _v, bytes32 _r, bytes32 _s; 511 | (bytes32[] memory merklePath, uint8 _v, bytes32 _r, bytes32 _s) = abi.decode(extraSignature, (bytes32[], uint8, bytes32, bytes32)); 512 | v = _v; r = _r; s = _s; 513 | */ 514 | } 515 | 516 | return _verify(oracle, oracleHash, v, r, s); 517 | } 518 | 519 | /** 520 | * @dev Verify ECDSA signature 521 | * @param signer Expected signer 522 | * @param digest Signature preimage 523 | * @param v v 524 | * @param r r 525 | * @param s s 526 | */ 527 | function _verify( 528 | address signer, 529 | bytes32 digest, 530 | uint8 v, 531 | bytes32 r, 532 | bytes32 s 533 | ) internal pure returns (bool) { 534 | require(v == 27 || v == 28, "Invalid v parameter"); 535 | address recoveredSigner = ecrecover(digest, v, r, s); 536 | if (recoveredSigner == address(0)) { 537 | return false; 538 | } else { 539 | return signer == recoveredSigner; 540 | } 541 | } 542 | 543 | /** 544 | * @dev Call the matching policy to check orders can be matched and get execution parameters 545 | * @param sell sell order 546 | * @param buy buy order 547 | */ 548 | function _canMatchOrders(Order calldata sell, Order calldata buy) 549 | internal 550 | view 551 | returns (uint256 price, uint256 tokenId, uint256 amount, AssetType assetType) 552 | { 553 | bool canMatch; 554 | if (sell.listingTime <= buy.listingTime) { 555 | /* Seller is maker. */ 556 | require(policyManager.isPolicyWhitelisted(sell.matchingPolicy), "Policy is not whitelisted"); 557 | (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(sell.matchingPolicy).canMatchMakerAsk(sell, buy); 558 | } else { 559 | /* Buyer is maker. */ 560 | require(policyManager.isPolicyWhitelisted(buy.matchingPolicy), "Policy is not whitelisted"); 561 | (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(buy.matchingPolicy).canMatchMakerBid(buy, sell); 562 | } 563 | require(canMatch, "Orders cannot be matched"); 564 | 565 | return (price, tokenId, amount, assetType); 566 | } 567 | 568 | /** 569 | * @dev Execute all ERC20 token / ETH transfers associated with an order match (fees and buyer => seller transfer) 570 | * @param seller seller 571 | * @param buyer buyer 572 | * @param paymentToken payment token 573 | * @param sellerFees seller fees 574 | * @param buyerFees buyer fees 575 | * @param price price 576 | */ 577 | function _executeFundsTransfer( 578 | address seller, 579 | address buyer, 580 | address paymentToken, 581 | Fee[] calldata sellerFees, 582 | Fee[] calldata buyerFees, 583 | uint256 price 584 | ) internal { 585 | if (paymentToken == address(0)) { 586 | require(msg.sender == buyer, "Cannot use ETH"); 587 | require(remainingETH >= price, "Insufficient value"); 588 | remainingETH -= price; 589 | } 590 | 591 | /* Take fee. */ 592 | uint256 sellerFeesPaid = _transferFees(sellerFees, paymentToken, buyer, price, true); 593 | uint256 buyerFeesPaid = _transferFees(buyerFees, paymentToken, buyer, price, false); 594 | if (paymentToken == address(0)) { 595 | /* Need to account for buyer fees paid on top of the price. */ 596 | remainingETH -= buyerFeesPaid; 597 | } 598 | 599 | /* Transfer remainder to seller. */ 600 | _transferTo(paymentToken, buyer, seller, price - sellerFeesPaid); 601 | } 602 | 603 | /** 604 | * @dev Charge a fee in ETH or WETH 605 | * @param fees fees to distribute 606 | * @param paymentToken address of token to pay in 607 | * @param from address to charge fees 608 | * @param price price of token 609 | * @return total fees paid 610 | */ 611 | function _transferFees( 612 | Fee[] calldata fees, 613 | address paymentToken, 614 | address from, 615 | uint256 price, 616 | bool protocolFee 617 | ) internal returns (uint256) { 618 | uint256 totalFee = 0; 619 | 620 | /* Take protocol fee if enabled. */ 621 | if (feeRate > 0 && protocolFee) { 622 | uint256 fee = (price * feeRate) / INVERSE_BASIS_POINT; 623 | _transferTo(paymentToken, from, feeRecipient, fee); 624 | totalFee += fee; 625 | } 626 | 627 | /* Take order fees. */ 628 | for (uint8 i = 0; i < fees.length; i++) { 629 | uint256 fee = (price * fees[i].rate) / INVERSE_BASIS_POINT; 630 | _transferTo(paymentToken, from, fees[i].recipient, fee); 631 | totalFee += fee; 632 | } 633 | 634 | require(totalFee <= price, "Fees are more than the price"); 635 | 636 | return totalFee; 637 | } 638 | 639 | /** 640 | * @dev Transfer amount in ETH or WETH 641 | * @param paymentToken address of token to pay in 642 | * @param from token sender 643 | * @param to token recipient 644 | * @param amount amount to transfer 645 | */ 646 | function _transferTo( 647 | address paymentToken, 648 | address from, 649 | address to, 650 | uint256 amount 651 | ) internal { 652 | if (amount == 0) { 653 | return; 654 | } 655 | 656 | if (paymentToken == address(0)) { 657 | /* Transfer funds in ETH. */ 658 | require(to != address(0), "Transfer to zero address"); 659 | (bool success,) = payable(to).call{value: amount}(""); 660 | require(success, "ETH transfer failed"); 661 | } else if (paymentToken == POOL) { 662 | /* Transfer Pool funds. */ 663 | bool success = IBlurPool(POOL).transferFrom(from, to, amount); 664 | require(success, "Pool transfer failed"); 665 | } else if (paymentToken == WETH) { 666 | /* Transfer funds in WETH. */ 667 | executionDelegate.transferERC20(WETH, from, to, amount); 668 | } else { 669 | revert("Invalid payment token"); 670 | } 671 | } 672 | 673 | /** 674 | * @dev Execute call through delegate proxy 675 | * @param collection collection contract address 676 | * @param from seller address 677 | * @param to buyer address 678 | * @param tokenId tokenId 679 | * @param assetType asset type of the token 680 | */ 681 | function _executeTokenTransfer( 682 | address collection, 683 | address from, 684 | address to, 685 | uint256 tokenId, 686 | uint256 amount, 687 | AssetType assetType 688 | ) internal { 689 | /* Call execution delegate. */ 690 | if (assetType == AssetType.ERC721) { 691 | executionDelegate.transferERC721(collection, from, to, tokenId); 692 | } else if (assetType == AssetType.ERC1155) { 693 | executionDelegate.transferERC1155(collection, from, to, tokenId, amount); 694 | } 695 | } 696 | 697 | /** 698 | * @dev Return remaining ETH sent to bulkExecute or execute 699 | */ 700 | function _returnDust() private { 701 | uint256 _remainingETH = remainingETH; 702 | assembly { 703 | if gt(_remainingETH, 0) { 704 | let callStatus := call( 705 | gas(), 706 | caller(), 707 | _remainingETH, 708 | 0, 709 | 0, 710 | 0, 711 | 0 712 | ) 713 | if iszero(callStatus) { 714 | revert(0, 0) 715 | } 716 | } 717 | } 718 | } 719 | } 720 | -------------------------------------------------------------------------------- /contracts/BlurExchange/ExecutionDelegate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | import "@openzeppelin/contracts/utils/Address.sol"; 9 | 10 | import {IExecutionDelegate} from "./interfaces/IExecutionDelegate.sol"; 11 | 12 | /** 13 | * @title ExecutionDelegate 14 | * @dev Proxy contract to manage user token approvals 15 | */ 16 | contract ExecutionDelegate is IExecutionDelegate, Ownable { 17 | 18 | using Address for address; 19 | 20 | mapping(address => bool) public contracts; 21 | mapping(address => bool) public revokedApproval; 22 | 23 | modifier approvedContract() { 24 | require(contracts[msg.sender], "Contract is not approved to make transfers"); 25 | _; 26 | } 27 | 28 | event ApproveContract(address indexed _contract); 29 | event DenyContract(address indexed _contract); 30 | 31 | event RevokeApproval(address indexed user); 32 | event GrantApproval(address indexed user); 33 | 34 | /** 35 | * @dev Approve contract to call transfer functions 36 | * @param _contract address of contract to approve 37 | */ 38 | function approveContract(address _contract) onlyOwner external { 39 | contracts[_contract] = true; 40 | emit ApproveContract(_contract); 41 | } 42 | 43 | /** 44 | * @dev Revoke approval of contract to call transfer functions 45 | * @param _contract address of contract to revoke approval 46 | */ 47 | function denyContract(address _contract) onlyOwner external { 48 | contracts[_contract] = false; 49 | emit DenyContract(_contract); 50 | } 51 | 52 | /** 53 | * @dev Block contract from making transfers on-behalf of a specific user 54 | */ 55 | function revokeApproval() external { 56 | revokedApproval[msg.sender] = true; 57 | emit RevokeApproval(msg.sender); 58 | } 59 | 60 | /** 61 | * @dev Allow contract to make transfers on-behalf of a specific user 62 | */ 63 | function grantApproval() external { 64 | revokedApproval[msg.sender] = false; 65 | emit GrantApproval(msg.sender); 66 | } 67 | 68 | /** 69 | * @dev Transfer ERC721 token using `transferFrom` 70 | * @param collection address of the collection 71 | * @param from address of the sender 72 | * @param to address of the recipient 73 | * @param tokenId tokenId 74 | */ 75 | function transferERC721Unsafe(address collection, address from, address to, uint256 tokenId) 76 | approvedContract 77 | external 78 | { 79 | require(revokedApproval[from] == false, "User has revoked approval"); 80 | IERC721(collection).transferFrom(from, to, tokenId); 81 | } 82 | 83 | /** 84 | * @dev Transfer ERC721 token using `safeTransferFrom` 85 | * @param collection address of the collection 86 | * @param from address of the sender 87 | * @param to address of the recipient 88 | * @param tokenId tokenId 89 | */ 90 | function transferERC721(address collection, address from, address to, uint256 tokenId) 91 | approvedContract 92 | external 93 | { 94 | require(revokedApproval[from] == false, "User has revoked approval"); 95 | IERC721(collection).safeTransferFrom(from, to, tokenId); 96 | } 97 | 98 | /** 99 | * @dev Transfer ERC1155 token using `safeTransferFrom` 100 | * @param collection address of the collection 101 | * @param from address of the sender 102 | * @param to address of the recipient 103 | * @param tokenId tokenId 104 | * @param amount amount 105 | */ 106 | function transferERC1155(address collection, address from, address to, uint256 tokenId, uint256 amount) 107 | approvedContract 108 | external 109 | { 110 | require(revokedApproval[from] == false, "User has revoked approval"); 111 | IERC1155(collection).safeTransferFrom(from, to, tokenId, amount, ""); 112 | } 113 | 114 | /** 115 | * @dev Transfer ERC20 token 116 | * @param token address of the token 117 | * @param from address of the sender 118 | * @param to address of the recipient 119 | * @param amount amount 120 | */ 121 | function transferERC20(address token, address from, address to, uint256 amount) 122 | approvedContract 123 | external 124 | { 125 | require(revokedApproval[from] == false, "User has revoked approval"); 126 | bytes memory data = abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount); 127 | bytes memory returndata = token.functionCall(data); 128 | if (returndata.length > 0) { 129 | require(abi.decode(returndata, (bool)), "ERC20 transfer failed"); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /contracts/BlurExchange/interfaces/IBlurExchange.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {Input, Order} from "../lib/OrderStructs.sol"; 5 | import "./IExecutionDelegate.sol"; 6 | import "./IPolicyManager.sol"; 7 | 8 | interface IBlurExchange { 9 | function nonces(address) external view returns (uint256); 10 | 11 | function close() external; 12 | 13 | function initialize( 14 | IExecutionDelegate _executionDelegate, 15 | IPolicyManager _policyManager, 16 | address _oracle, 17 | uint _blockRange 18 | ) external; 19 | function setExecutionDelegate(IExecutionDelegate _executionDelegate) external; 20 | 21 | function setPolicyManager(IPolicyManager _policyManager) external; 22 | 23 | function setOracle(address _oracle) external; 24 | 25 | function setBlockRange(uint256 _blockRange) external; 26 | 27 | function cancelOrder(Order calldata order) external; 28 | 29 | function cancelOrders(Order[] calldata orders) external; 30 | 31 | function incrementNonce() external; 32 | 33 | function execute(Input calldata sell, Input calldata buy) 34 | external 35 | payable; 36 | } 37 | -------------------------------------------------------------------------------- /contracts/BlurExchange/interfaces/IBlurPool.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.17; 2 | 3 | interface IBlurPool { 4 | event Transfer(address indexed from, address indexed to, uint256 amount); 5 | 6 | function totalSupply() external view returns (uint256); 7 | function balanceOf(address user) external view returns (uint256); 8 | 9 | function deposit() external payable; 10 | function withdraw(uint256) external; 11 | 12 | function transferFrom(address from, address to, uint256 amount) 13 | external 14 | returns (bool); 15 | } 16 | -------------------------------------------------------------------------------- /contracts/BlurExchange/interfaces/IExecutionDelegate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | interface IExecutionDelegate { 5 | function approveContract(address _contract) external; 6 | function denyContract(address _contract) external; 7 | function revokeApproval() external; 8 | function grantApproval() external; 9 | 10 | function transferERC721Unsafe(address collection, address from, address to, uint256 tokenId) external; 11 | 12 | function transferERC721(address collection, address from, address to, uint256 tokenId) external; 13 | 14 | function transferERC1155(address collection, address from, address to, uint256 tokenId, uint256 amount) external; 15 | 16 | function transferERC20(address token, address from, address to, uint256 amount) external; 17 | } 18 | -------------------------------------------------------------------------------- /contracts/BlurExchange/interfaces/IMatchingPolicy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {Order, AssetType} from "../lib/OrderStructs.sol"; 5 | 6 | interface IMatchingPolicy { 7 | function canMatchMakerAsk(Order calldata makerAsk, Order calldata takerBid) 8 | external 9 | view 10 | returns ( 11 | bool, 12 | uint256, 13 | uint256, 14 | uint256, 15 | AssetType 16 | ); 17 | 18 | function canMatchMakerBid(Order calldata makerBid, Order calldata takerAsk) 19 | external 20 | view 21 | returns ( 22 | bool, 23 | uint256, 24 | uint256, 25 | uint256, 26 | AssetType 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /contracts/BlurExchange/interfaces/IPolicyManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | interface IPolicyManager { 5 | function addPolicy(address policy) external; 6 | 7 | function removePolicy(address policy) external; 8 | 9 | function isPolicyWhitelisted(address policy) external view returns (bool); 10 | 11 | function viewWhitelistedPolicies(uint256 cursor, uint256 size) external view returns (address[] memory, uint256); 12 | 13 | function viewCountWhitelistedPolicies() external view returns (uint256); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/BlurExchange/lib/EIP712.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {Order, Fee} from "./OrderStructs.sol"; 5 | 6 | /** 7 | * @title EIP712 8 | * @dev Contains all of the order hashing functions for EIP712 compliant signatures 9 | */ 10 | contract EIP712 { 11 | 12 | struct EIP712Domain { 13 | string name; 14 | string version; 15 | uint256 chainId; 16 | address verifyingContract; 17 | } 18 | 19 | /* Order typehash for EIP 712 compatibility. */ 20 | bytes32 constant public FEE_TYPEHASH = keccak256( 21 | "Fee(uint16 rate,address recipient)" 22 | ); 23 | bytes32 constant public ORDER_TYPEHASH = keccak256( 24 | "Order(address trader,uint8 side,address matchingPolicy,address collection,uint256 tokenId,uint256 amount,address paymentToken,uint256 price,uint256 listingTime,uint256 expirationTime,Fee[] fees,uint256 salt,bytes extraParams,uint256 nonce)Fee(uint16 rate,address recipient)" 25 | ); 26 | bytes32 constant public ORACLE_ORDER_TYPEHASH = keccak256( 27 | "OracleOrder(Order order,uint256 blockNumber)Fee(uint16 rate,address recipient)Order(address trader,uint8 side,address matchingPolicy,address collection,uint256 tokenId,uint256 amount,address paymentToken,uint256 price,uint256 listingTime,uint256 expirationTime,Fee[] fees,uint256 salt,bytes extraParams,uint256 nonce)" 28 | ); 29 | bytes32 constant public ROOT_TYPEHASH = keccak256( 30 | "Root(bytes32 root)" 31 | ); 32 | 33 | bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256( 34 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 35 | ); 36 | 37 | bytes32 DOMAIN_SEPARATOR; 38 | 39 | function _hashDomain(EIP712Domain memory eip712Domain) 40 | internal 41 | pure 42 | returns (bytes32) 43 | { 44 | return keccak256( 45 | abi.encode( 46 | EIP712DOMAIN_TYPEHASH, 47 | keccak256(bytes(eip712Domain.name)), 48 | keccak256(bytes(eip712Domain.version)), 49 | eip712Domain.chainId, 50 | eip712Domain.verifyingContract 51 | ) 52 | ); 53 | } 54 | 55 | function _hashFee(Fee calldata fee) 56 | internal 57 | pure 58 | returns (bytes32) 59 | { 60 | return keccak256( 61 | abi.encode( 62 | FEE_TYPEHASH, 63 | fee.rate, 64 | fee.recipient 65 | ) 66 | ); 67 | } 68 | 69 | function _packFees(Fee[] calldata fees) 70 | internal 71 | pure 72 | returns (bytes32) 73 | { 74 | bytes32[] memory feeHashes = new bytes32[]( 75 | fees.length 76 | ); 77 | for (uint256 i = 0; i < fees.length; i++) { 78 | feeHashes[i] = _hashFee(fees[i]); 79 | } 80 | return keccak256(abi.encodePacked(feeHashes)); 81 | } 82 | 83 | 84 | function _hashOrder(Order calldata order, uint256 nonce) 85 | internal 86 | pure 87 | returns (bytes32) 88 | { 89 | return keccak256( 90 | bytes.concat( 91 | abi.encode( 92 | ORDER_TYPEHASH, 93 | order.trader, 94 | order.side, 95 | order.matchingPolicy, 96 | order.collection, 97 | order.tokenId, 98 | order.amount, 99 | order.paymentToken, 100 | order.price, 101 | order.listingTime, 102 | order.expirationTime, 103 | _packFees(order.fees), 104 | order.salt, 105 | keccak256(order.extraParams) 106 | ), 107 | abi.encode(nonce) 108 | ) 109 | ); 110 | } 111 | 112 | function _hashToSign(bytes32 orderHash) 113 | internal 114 | view 115 | returns (bytes32 hash) 116 | { 117 | return keccak256(abi.encodePacked( 118 | "\x19\x01", 119 | DOMAIN_SEPARATOR, 120 | orderHash 121 | )); 122 | } 123 | 124 | function _hashToSignRoot(bytes32 root) 125 | internal 126 | view 127 | returns (bytes32 hash) 128 | { 129 | return keccak256(abi.encodePacked( 130 | "\x19\x01", 131 | DOMAIN_SEPARATOR, 132 | keccak256(abi.encode( 133 | ROOT_TYPEHASH, 134 | root 135 | )) 136 | )); 137 | } 138 | 139 | function _hashToSignOracle(bytes32 orderHash, uint256 blockNumber) 140 | internal 141 | view 142 | returns (bytes32 hash) 143 | { 144 | return keccak256(abi.encodePacked( 145 | "\x19\x01", 146 | DOMAIN_SEPARATOR, 147 | keccak256(abi.encode( 148 | ORACLE_ORDER_TYPEHASH, 149 | orderHash, 150 | blockNumber 151 | )) 152 | )); 153 | } 154 | 155 | uint256[44] private __gap; 156 | } 157 | -------------------------------------------------------------------------------- /contracts/BlurExchange/lib/MerkleVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | /** 5 | * @title MerkleVerifier 6 | * @dev Utility functions for Merkle tree computations 7 | */ 8 | library MerkleVerifier { 9 | error InvalidProof(); 10 | 11 | /** 12 | * @dev Verify the merkle proof 13 | * @param leaf leaf 14 | * @param root root 15 | * @param proof proof 16 | */ 17 | function _verifyProof( 18 | bytes32 leaf, 19 | bytes32 root, 20 | bytes32[] memory proof 21 | ) public pure { 22 | bytes32 computedRoot = _computeRoot(leaf, proof); 23 | if (computedRoot != root) { 24 | revert InvalidProof(); 25 | } 26 | } 27 | 28 | /** 29 | * @dev Compute the merkle root 30 | * @param leaf leaf 31 | * @param proof proof 32 | */ 33 | function _computeRoot( 34 | bytes32 leaf, 35 | bytes32[] memory proof 36 | ) public pure returns (bytes32) { 37 | bytes32 computedHash = leaf; 38 | for (uint256 i = 0; i < proof.length; i++) { 39 | bytes32 proofElement = proof[i]; 40 | computedHash = _hashPair(computedHash, proofElement); 41 | } 42 | return computedHash; 43 | } 44 | 45 | function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { 46 | return a < b ? _efficientHash(a, b) : _efficientHash(b, a); 47 | } 48 | 49 | function _efficientHash( 50 | bytes32 a, 51 | bytes32 b 52 | ) private pure returns (bytes32 value) { 53 | assembly { 54 | mstore(0x00, a) 55 | mstore(0x20, b) 56 | value := keccak256(0x00, 0x40) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /contracts/BlurExchange/lib/OrderStructs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | enum Side { Buy, Sell } 5 | enum SignatureVersion { Single, Bulk } 6 | enum AssetType { ERC721, ERC1155 } 7 | 8 | struct Fee { 9 | uint16 rate; 10 | address payable recipient; 11 | } 12 | 13 | struct Order { 14 | address trader; 15 | Side side; 16 | address matchingPolicy; 17 | address collection; 18 | uint256 tokenId; 19 | uint256 amount; 20 | address paymentToken; 21 | uint256 price; 22 | uint256 listingTime; 23 | /* Order expiration timestamp - 0 for oracle cancellations. */ 24 | uint256 expirationTime; 25 | Fee[] fees; 26 | uint256 salt; 27 | bytes extraParams; 28 | } 29 | 30 | struct Input { 31 | Order order; 32 | uint8 v; 33 | bytes32 r; 34 | bytes32 s; 35 | bytes extraSignature; 36 | SignatureVersion signatureVersion; 37 | uint256 blockNumber; 38 | } 39 | 40 | struct Execution { 41 | Input sell; 42 | Input buy; 43 | } 44 | -------------------------------------------------------------------------------- /contracts/BlurExchange/lib/ReentrancyGuarded.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | /** 5 | * @title ReentrancyGuarded 6 | * @dev Protections for reentrancy attacks 7 | */ 8 | contract ReentrancyGuarded { 9 | 10 | bool private reentrancyLock = false; 11 | 12 | /* Prevent a contract function from being reentrant-called. */ 13 | modifier reentrancyGuard { 14 | require(!reentrancyLock, "Reentrancy detected"); 15 | reentrancyLock = true; 16 | _; 17 | reentrancyLock = false; 18 | } 19 | 20 | uint256[49] private __gap; 21 | } 22 | -------------------------------------------------------------------------------- /contracts/BlurPool/BlurPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 6 | 7 | import "./interfaces/IBlurPool.sol"; 8 | 9 | /** 10 | * @title BlurPool 11 | * @dev ETH pool; funds can only be transferred by Exchange or Swap 12 | */ 13 | contract BlurPool is IBlurPool, OwnableUpgradeable, UUPSUpgradeable { 14 | address private constant EXCHANGE = 0x000000000000Ad05Ccc4F10045630fb830B95127; 15 | address private constant SWAP = 0x39da41747a83aeE658334415666f3EF92DD0D541; 16 | 17 | mapping(address => uint256) private _balances; 18 | 19 | string public constant name = 'Blur Pool'; 20 | string constant symbol = ''; 21 | 22 | // required by the OZ UUPS module 23 | function _authorizeUpgrade(address) internal override onlyOwner {} 24 | 25 | constructor() { 26 | _disableInitializers(); 27 | } 28 | 29 | /* Constructor (for ERC1967) */ 30 | function initialize() external initializer { 31 | __Ownable_init(); 32 | } 33 | 34 | function decimals() external pure returns (uint8) { 35 | return 18; 36 | } 37 | 38 | function totalSupply() external view returns (uint256) { 39 | return address(this).balance; 40 | } 41 | 42 | function balanceOf(address user) external view returns (uint256) { 43 | return _balances[user]; 44 | } 45 | 46 | /** 47 | * @dev receive deposit function 48 | */ 49 | receive() external payable { 50 | deposit(); 51 | } 52 | 53 | /** 54 | * @dev deposit ETH into pool 55 | */ 56 | function deposit() public payable { 57 | _balances[msg.sender] += msg.value; 58 | emit Transfer(address(0), msg.sender, msg.value); 59 | } 60 | 61 | /** 62 | * @dev withdraw ETH from pool 63 | * @param amount Amount to withdraw 64 | */ 65 | function withdraw(uint256 amount) external { 66 | require(_balances[msg.sender] >= amount, "Insufficient funds"); 67 | _balances[msg.sender] -= amount; 68 | (bool success,) = payable(msg.sender).call{value: amount}(""); 69 | require(success, "Transfer failed"); 70 | emit Transfer(msg.sender, address(0), amount); 71 | } 72 | 73 | /** 74 | * @dev transferFrom Transfer balances within pool; only callable by Swap and Exchange 75 | * @param from Pool fund sender 76 | * @param to Pool fund recipient 77 | * @param amount Amount to transfer 78 | */ 79 | function transferFrom(address from, address to, uint256 amount) 80 | external 81 | returns (bool) 82 | { 83 | if (msg.sender != EXCHANGE && msg.sender != SWAP) { 84 | revert('Unauthorized transfer'); 85 | } 86 | _transfer(from, to, amount); 87 | 88 | return true; 89 | } 90 | 91 | function _transfer(address from, address to, uint256 amount) private { 92 | require(to != address(0), "Cannot transfer to 0 address"); 93 | require(_balances[from] >= amount, "Insufficient balance"); 94 | _balances[from] -= amount; 95 | _balances[to] += amount; 96 | 97 | emit Transfer(from, to, amount); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /contracts/BlurPool/interfaces/IBlurPool.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.17; 2 | 3 | interface IBlurPool { 4 | event Transfer(address indexed from, address indexed to, uint256 amount); 5 | 6 | function totalSupply() external view returns (uint256); 7 | function balanceOf(address user) external view returns (uint256); 8 | 9 | function deposit() external payable; 10 | function withdraw(uint256) external; 11 | 12 | function transferFrom(address from, address to, uint256 amount) 13 | external 14 | returns (bool); 15 | } 16 | -------------------------------------------------------------------------------- /contracts/BlurSwap/BlurSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "./utils/ReentrancyGuard.sol"; 7 | import "./markets/MarketRegistry.sol"; 8 | import "./SpecialTransferHelper.sol"; 9 | import "./interfaces/IERC20.sol"; 10 | import "./interfaces/IERC721.sol"; 11 | import "./interfaces/IERC1155.sol"; 12 | 13 | contract BlurSwap is SpecialTransferHelper, Ownable, ReentrancyGuard { 14 | 15 | struct OpenseaTrades { 16 | uint256 value; 17 | bytes tradeData; 18 | } 19 | 20 | struct ERC20Details { 21 | address[] tokenAddrs; 22 | uint256[] amounts; 23 | } 24 | 25 | struct ERC1155Details { 26 | address tokenAddr; 27 | uint256[] ids; 28 | uint256[] amounts; 29 | } 30 | 31 | struct ConverstionDetails { 32 | bytes conversionData; 33 | } 34 | 35 | struct AffiliateDetails { 36 | address affiliate; 37 | bool isActive; 38 | } 39 | 40 | struct SponsoredMarket { 41 | uint256 marketId; 42 | bool isActive; 43 | } 44 | 45 | address public constant GOV = 0xcD0313FD7CCa5648d2948c42C320Ba50CD0E6cB6; 46 | address public guardian; 47 | address public converter; 48 | address public punkProxy; 49 | uint256 public baseFees; 50 | bool public openForTrades; 51 | bool public openForFreeTrades; 52 | MarketRegistry public marketRegistry; 53 | AffiliateDetails[] public affiliates; 54 | SponsoredMarket[] public sponsoredMarkets; 55 | 56 | modifier isOpenForTrades() { 57 | require(openForTrades, "trades not allowed"); 58 | _; 59 | } 60 | 61 | modifier isOpenForFreeTrades() { 62 | require(openForFreeTrades, "free trades not allowed"); 63 | _; 64 | } 65 | 66 | constructor(address _marketRegistry, address _guardian) { 67 | marketRegistry = MarketRegistry(_marketRegistry); 68 | guardian = _guardian; 69 | baseFees = 0; 70 | openForTrades = true; 71 | openForFreeTrades = true; 72 | } 73 | 74 | function setUp() external onlyOwner { 75 | // Create CryptoPunk Proxy 76 | IWrappedPunk(0xb7F7F6C52F2e2fdb1963Eab30438024864c313F6).registerProxy(); 77 | punkProxy = IWrappedPunk(0xb7F7F6C52F2e2fdb1963Eab30438024864c313F6).proxyInfo(address(this)); 78 | 79 | // approve wrapped mooncats rescue to Acclimated​MoonCats contract 80 | IERC721(0x7C40c393DC0f283F318791d746d894DdD3693572).setApprovalForAll(0xc3f733ca98E0daD0386979Eb96fb1722A1A05E69, true); 81 | } 82 | 83 | // @audit This function is used to approve specific tokens to specific market contracts with high volume. 84 | // This is done in very rare cases for the gas optimization purposes. 85 | function setOneTimeApproval(IERC20 token, address operator, uint256 amount) external onlyOwner { 86 | token.approve(operator, amount); 87 | } 88 | 89 | function updateGuardian(address _guardian) external onlyOwner { 90 | guardian = _guardian; 91 | } 92 | 93 | function addAffiliate(address _affiliate) external onlyOwner { 94 | affiliates.push(AffiliateDetails(_affiliate, true)); 95 | } 96 | 97 | function updateAffiliate(uint256 _affiliateIndex, address _affiliate, bool _IsActive) external onlyOwner { 98 | affiliates[_affiliateIndex] = AffiliateDetails(_affiliate, _IsActive); 99 | } 100 | 101 | function addSponsoredMarket(uint256 _marketId) external onlyOwner { 102 | sponsoredMarkets.push(SponsoredMarket(_marketId, true)); 103 | } 104 | 105 | function updateSponsoredMarket(uint256 _marketIndex, uint256 _marketId, bool _isActive) external onlyOwner { 106 | sponsoredMarkets[_marketIndex] = SponsoredMarket(_marketId, _isActive); 107 | } 108 | 109 | function setBaseFees(uint256 _baseFees) external onlyOwner { 110 | baseFees = _baseFees; 111 | } 112 | 113 | function setOpenForTrades(bool _openForTrades) external onlyOwner { 114 | openForTrades = _openForTrades; 115 | } 116 | 117 | function setOpenForFreeTrades(bool _openForFreeTrades) external onlyOwner { 118 | openForFreeTrades = _openForFreeTrades; 119 | } 120 | 121 | // @audit we will setup a system that will monitor the contract for any leftover 122 | // assets. In case any asset is leftover, the system should be able to trigger this 123 | // function to close all the trades until the leftover assets are rescued. 124 | function closeAllTrades() external { 125 | require(_msgSender() == guardian); 126 | openForTrades = false; 127 | openForFreeTrades = false; 128 | } 129 | 130 | function setConverter(address _converter) external onlyOwner { 131 | converter = _converter; 132 | } 133 | 134 | function setMarketRegistry(MarketRegistry _marketRegistry) external onlyOwner { 135 | marketRegistry = _marketRegistry; 136 | } 137 | 138 | function _transferEth(address _to, uint256 _amount) internal { 139 | bool callStatus; 140 | assembly { 141 | // Transfer the ETH and store if it succeeded or not. 142 | callStatus := call(gas(), _to, _amount, 0, 0, 0, 0) 143 | } 144 | require(callStatus, "_transferEth: Eth transfer failed"); 145 | } 146 | 147 | function _collectFee(uint256[2] memory feeDetails) internal { 148 | require(feeDetails[1] >= baseFees, "Insufficient fee"); 149 | if (feeDetails[1] > 0) { 150 | AffiliateDetails memory affiliateDetails = affiliates[feeDetails[0]]; 151 | affiliateDetails.isActive 152 | ? _transferEth(affiliateDetails.affiliate, feeDetails[1]) 153 | : _transferEth(GOV, feeDetails[1]); 154 | } 155 | } 156 | 157 | function _checkCallResult(bool _success) internal pure { 158 | if (!_success) { 159 | // Copy revert reason from call 160 | assembly { 161 | returndatacopy(0, 0, returndatasize()) 162 | revert(0, returndatasize()) 163 | } 164 | } 165 | } 166 | 167 | function _transferFromHelper( 168 | ERC20Details memory erc20Details, 169 | SpecialTransferHelper.ERC721Details[] memory erc721Details, 170 | ERC1155Details[] memory erc1155Details 171 | ) internal { 172 | // transfer ERC20 tokens from the sender to this contract 173 | for (uint256 i = 0; i < erc20Details.tokenAddrs.length; i++) { 174 | erc20Details.tokenAddrs[i].call(abi.encodeWithSelector(0x23b872dd, msg.sender, address(this), erc20Details.amounts[i])); 175 | } 176 | 177 | // transfer ERC721 tokens from the sender to this contract 178 | for (uint256 i = 0; i < erc721Details.length; i++) { 179 | // accept CryptoPunks 180 | if (erc721Details[i].tokenAddr == 0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB) { 181 | _acceptCryptoPunk(erc721Details[i]); 182 | } 183 | // accept Mooncat 184 | else if (erc721Details[i].tokenAddr == 0x60cd862c9C687A9dE49aecdC3A99b74A4fc54aB6) { 185 | _acceptMoonCat(erc721Details[i]); 186 | } 187 | // default 188 | else { 189 | for (uint256 j = 0; j < erc721Details[i].ids.length; j++) { 190 | IERC721(erc721Details[i].tokenAddr).transferFrom( 191 | _msgSender(), 192 | address(this), 193 | erc721Details[i].ids[j] 194 | ); 195 | } 196 | } 197 | } 198 | 199 | // transfer ERC1155 tokens from the sender to this contract 200 | for (uint256 i = 0; i < erc1155Details.length; i++) { 201 | IERC1155(erc1155Details[i].tokenAddr).safeBatchTransferFrom( 202 | _msgSender(), 203 | address(this), 204 | erc1155Details[i].ids, 205 | erc1155Details[i].amounts, 206 | "" 207 | ); 208 | } 209 | } 210 | 211 | function _conversionHelper( 212 | ConverstionDetails[] memory _converstionDetails 213 | ) internal { 214 | for (uint256 i = 0; i < _converstionDetails.length; i++) { 215 | // convert to desired asset 216 | (bool success, ) = converter.delegatecall(_converstionDetails[i].conversionData); 217 | // check if the call passed successfully 218 | _checkCallResult(success); 219 | } 220 | } 221 | 222 | function _trade( 223 | MarketRegistry.TradeDetails[] memory _tradeDetails 224 | ) internal { 225 | for (uint256 i = 0; i < _tradeDetails.length; i++) { 226 | // get market details 227 | (address _proxy, bool _isLib, bool _isActive) = marketRegistry.markets(_tradeDetails[i].marketId); 228 | // market should be active 229 | require(_isActive, "_trade: InActive Market"); 230 | // execute trade 231 | if (_proxy == 0x7Be8076f4EA4A4AD08075C2508e481d6C946D12b || _proxy == 0x7f268357A8c2552623316e2562D90e642bB538E5) { 232 | _proxy.call{value:_tradeDetails[i].value}(_tradeDetails[i].tradeData); 233 | } else { 234 | (bool success, ) = _isLib 235 | ? _proxy.delegatecall(_tradeDetails[i].tradeData) 236 | : _proxy.call{value:_tradeDetails[i].value}(_tradeDetails[i].tradeData); 237 | // check if the call passed successfully 238 | _checkCallResult(success); 239 | } 240 | } 241 | } 242 | 243 | // function _tradeSponsored( 244 | // MarketRegistry.TradeDetails[] memory _tradeDetails, 245 | // uint256 sponsoredMarketId 246 | // ) internal returns (bool isSponsored) { 247 | // for (uint256 i = 0; i < _tradeDetails.length; i++) { 248 | // // check if the trade is for the sponsored market 249 | // if (_tradeDetails[i].marketId == sponsoredMarketId) { 250 | // isSponsored = true; 251 | // } 252 | // // get market details 253 | // (address _proxy, bool _isLib, bool _isActive) = marketRegistry.markets(_tradeDetails[i].marketId); 254 | // // market should be active 255 | // require(_isActive, "_trade: InActive Market"); 256 | // // execute trade 257 | // if (_proxy == 0x7Be8076f4EA4A4AD08075C2508e481d6C946D12b) { 258 | // _proxy.call{value:_tradeDetails[i].value}(_tradeDetails[i].tradeData); 259 | // } else { 260 | // (bool success, ) = _isLib 261 | // ? _proxy.delegatecall(_tradeDetails[i].tradeData) 262 | // : _proxy.call{value:_tradeDetails[i].value}(_tradeDetails[i].tradeData); 263 | // // check if the call passed successfully 264 | // _checkCallResult(success); 265 | // } 266 | // } 267 | // } 268 | 269 | function _returnDust(address[] memory _tokens) internal { 270 | // return remaining ETH (if any) 271 | assembly { 272 | if gt(selfbalance(), 0) { 273 | let callStatus := call( 274 | gas(), 275 | caller(), 276 | selfbalance(), 277 | 0, 278 | 0, 279 | 0, 280 | 0 281 | ) 282 | } 283 | } 284 | // return remaining tokens (if any) 285 | for (uint256 i = 0; i < _tokens.length; i++) { 286 | if (IERC20(_tokens[i]).balanceOf(address(this)) > 0) { 287 | _tokens[i].call(abi.encodeWithSelector(0xa9059cbb, msg.sender, IERC20(_tokens[i]).balanceOf(address(this)))); 288 | } 289 | } 290 | } 291 | 292 | function batchBuyFromOpenSea( 293 | OpenseaTrades[] memory openseaTrades 294 | ) payable external nonReentrant { 295 | // execute trades 296 | for (uint256 i = 0; i < openseaTrades.length; i++) { 297 | // execute trade 298 | address(0x7Be8076f4EA4A4AD08075C2508e481d6C946D12b).call{value:openseaTrades[i].value}(openseaTrades[i].tradeData); 299 | } 300 | 301 | // return remaining ETH (if any) 302 | assembly { 303 | if gt(selfbalance(), 0) { 304 | let callStatus := call( 305 | gas(), 306 | caller(), 307 | selfbalance(), 308 | 0, 309 | 0, 310 | 0, 311 | 0 312 | ) 313 | } 314 | } 315 | } 316 | 317 | function batchBuyWithETH( 318 | MarketRegistry.TradeDetails[] memory tradeDetails 319 | ) payable external nonReentrant { 320 | // execute trades 321 | _trade(tradeDetails); 322 | 323 | // return remaining ETH (if any) 324 | assembly { 325 | if gt(selfbalance(), 0) { 326 | let callStatus := call( 327 | gas(), 328 | caller(), 329 | selfbalance(), 330 | 0, 331 | 0, 332 | 0, 333 | 0 334 | ) 335 | } 336 | } 337 | } 338 | 339 | function batchBuyWithERC20s( 340 | ERC20Details memory erc20Details, 341 | MarketRegistry.TradeDetails[] memory tradeDetails, 342 | ConverstionDetails[] memory converstionDetails, 343 | address[] memory dustTokens 344 | ) payable external nonReentrant { 345 | // transfer ERC20 tokens from the sender to this contract 346 | for (uint256 i = 0; i < erc20Details.tokenAddrs.length; i++) { 347 | erc20Details.tokenAddrs[i].call(abi.encodeWithSelector(0x23b872dd, msg.sender, address(this), erc20Details.amounts[i])); 348 | } 349 | 350 | // Convert any assets if needed 351 | _conversionHelper(converstionDetails); 352 | 353 | // execute trades 354 | _trade(tradeDetails); 355 | 356 | // return dust tokens (if any) 357 | _returnDust(dustTokens); 358 | } 359 | 360 | // swaps any combination of ERC-20/721/1155 361 | // User needs to approve assets before invoking swap 362 | // WARNING: DO NOT SEND TOKENS TO THIS FUNCTION DIRECTLY!!! 363 | function multiAssetSwap( 364 | ERC20Details memory erc20Details, 365 | SpecialTransferHelper.ERC721Details[] memory erc721Details, 366 | ERC1155Details[] memory erc1155Details, 367 | ConverstionDetails[] memory converstionDetails, 368 | MarketRegistry.TradeDetails[] memory tradeDetails, 369 | address[] memory dustTokens, 370 | uint256[2] memory feeDetails // [affiliateIndex, ETH fee in Wei] 371 | ) payable external isOpenForTrades nonReentrant { 372 | // collect fees 373 | _collectFee(feeDetails); 374 | 375 | // transfer all tokens 376 | _transferFromHelper( 377 | erc20Details, 378 | erc721Details, 379 | erc1155Details 380 | ); 381 | 382 | // Convert any assets if needed 383 | _conversionHelper(converstionDetails); 384 | 385 | // execute trades 386 | _trade(tradeDetails); 387 | 388 | // return dust tokens (if any) 389 | _returnDust(dustTokens); 390 | } 391 | 392 | // Utility function that is used for free swaps for sponsored markets 393 | // WARNING: DO NOT SEND TOKENS TO THIS FUNCTION DIRECTLY!!! 394 | // function multiAssetSwapWithoutFee( 395 | // ERC20Details memory erc20Details, 396 | // SpecialTransferHelper.ERC721Details[] memory erc721Details, 397 | // ERC1155Details[] memory erc1155Details, 398 | // ConverstionDetails[] memory converstionDetails, 399 | // MarketRegistry.TradeDetails[] memory tradeDetails, 400 | // address[] memory dustTokens, 401 | // uint256 sponsoredMarketIndex 402 | // ) payable external isOpenForFreeTrades nonReentrant { 403 | // // fetch the marketId of the sponsored market 404 | // SponsoredMarket memory sponsoredMarket = sponsoredMarkets[sponsoredMarketIndex]; 405 | // // check if the market is active 406 | // require(sponsoredMarket.isActive, "multiAssetSwapWithoutFee: InActive sponsored market"); 407 | // 408 | // // transfer all tokens 409 | // _transferFromHelper( 410 | // erc20Details, 411 | // erc721Details, 412 | // erc1155Details 413 | // ); 414 | // 415 | // // Convert any assets if needed 416 | // _conversionHelper(converstionDetails); 417 | // 418 | // // execute trades 419 | // bool isSponsored = _tradeSponsored(tradeDetails, sponsoredMarket.marketId); 420 | // 421 | // // check if the trades include the sponsored market 422 | // require(isSponsored, "multiAssetSwapWithoutFee: trades do not include sponsored market"); 423 | // 424 | // // return dust tokens (if any) 425 | // _returnDust(dustTokens); 426 | // } 427 | 428 | function onERC1155Received( 429 | address, 430 | address, 431 | uint256, 432 | uint256, 433 | bytes calldata 434 | ) public virtual returns (bytes4) { 435 | return this.onERC1155Received.selector; 436 | } 437 | 438 | function onERC1155BatchReceived( 439 | address, 440 | address, 441 | uint256[] calldata, 442 | uint256[] calldata, 443 | bytes calldata 444 | ) public virtual returns (bytes4) { 445 | return this.onERC1155BatchReceived.selector; 446 | } 447 | 448 | function onERC721Received( 449 | address, 450 | address, 451 | uint256, 452 | bytes calldata 453 | ) external virtual returns (bytes4) { 454 | return 0x150b7a02; 455 | } 456 | 457 | // Used by ERC721BasicToken.sol 458 | function onERC721Received( 459 | address, 460 | uint256, 461 | bytes calldata 462 | ) external virtual returns (bytes4) { 463 | return 0xf0b9e5ba; 464 | } 465 | 466 | function supportsInterface(bytes4 interfaceId) 467 | external 468 | virtual 469 | view 470 | returns (bool) 471 | { 472 | return interfaceId == this.supportsInterface.selector; 473 | } 474 | 475 | receive() external payable {} 476 | 477 | // Emergency function: In case any ETH get stuck in the contract unintentionally 478 | // Only owner can retrieve the asset balance to a recipient address 479 | function rescueETH(address recipient) onlyOwner external { 480 | _transferEth(recipient, address(this).balance); 481 | } 482 | 483 | // Emergency function: In case any ERC20 tokens get stuck in the contract unintentionally 484 | // Only owner can retrieve the asset balance to a recipient address 485 | function rescueERC20(address asset, address recipient) onlyOwner external { 486 | asset.call(abi.encodeWithSelector(0xa9059cbb, recipient, IERC20(asset).balanceOf(address(this)))); 487 | } 488 | 489 | // Emergency function: In case any ERC721 tokens get stuck in the contract unintentionally 490 | // Only owner can retrieve the asset balance to a recipient address 491 | function rescueERC721(address asset, uint256[] calldata ids, address recipient) onlyOwner external { 492 | for (uint256 i = 0; i < ids.length; i++) { 493 | IERC721(asset).transferFrom(address(this), recipient, ids[i]); 494 | } 495 | } 496 | 497 | // Emergency function: In case any ERC1155 tokens get stuck in the contract unintentionally 498 | // Only owner can retrieve the asset balance to a recipient address 499 | function rescueERC1155(address asset, uint256[] calldata ids, uint256[] calldata amounts, address recipient) onlyOwner external { 500 | for (uint256 i = 0; i < ids.length; i++) { 501 | IERC1155(asset).safeTransferFrom(address(this), recipient, ids[i], amounts[i], ""); 502 | } 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /contracts/BlurSwap/SpecialTransferHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | import "@openzeppelin/contracts/utils/Context.sol"; 6 | import "./interfaces/ICryptoPunks.sol"; 7 | import "./interfaces/IWrappedPunk.sol"; 8 | import "./interfaces/IMoonCatsRescue.sol"; 9 | 10 | contract SpecialTransferHelper is Context { 11 | 12 | struct ERC721Details { 13 | address tokenAddr; 14 | address[] to; 15 | uint256[] ids; 16 | } 17 | 18 | function _uintToBytes5(uint256 id) 19 | internal 20 | pure 21 | returns (bytes5 slicedDataBytes5) 22 | { 23 | bytes memory _bytes = new bytes(32); 24 | assembly { 25 | mstore(add(_bytes, 32), id) 26 | } 27 | 28 | bytes memory tempBytes; 29 | 30 | assembly { 31 | // Get a location of some free memory and store it in tempBytes as 32 | // Solidity does for memory variables. 33 | tempBytes := mload(0x40) 34 | 35 | // The first word of the slice result is potentially a partial 36 | // word read from the original array. To read it, we calculate 37 | // the length of that partial word and start copying that many 38 | // bytes into the array. The first word we copy will start with 39 | // data we don't care about, but the last `lengthmod` bytes will 40 | // land at the beginning of the contents of the new array. When 41 | // we're done copying, we overwrite the full first word with 42 | // the actual length of the slice. 43 | let lengthmod := and(5, 31) 44 | 45 | // The multiplication in the next line is necessary 46 | // because when slicing multiples of 32 bytes (lengthmod == 0) 47 | // the following copy loop was copying the origin's length 48 | // and then ending prematurely not copying everything it should. 49 | let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) 50 | let end := add(mc, 5) 51 | 52 | for { 53 | // The multiplication in the next line has the same exact purpose 54 | // as the one above. 55 | let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), 27) 56 | } lt(mc, end) { 57 | mc := add(mc, 0x20) 58 | cc := add(cc, 0x20) 59 | } { 60 | mstore(mc, mload(cc)) 61 | } 62 | 63 | mstore(tempBytes, 5) 64 | 65 | //update free-memory pointer 66 | //allocating the array padded to 32 bytes like the compiler does now 67 | mstore(0x40, and(add(mc, 31), not(31))) 68 | } 69 | 70 | assembly { 71 | slicedDataBytes5 := mload(add(tempBytes, 32)) 72 | } 73 | } 74 | 75 | 76 | function _acceptMoonCat(ERC721Details memory erc721Details) internal { 77 | for (uint256 i = 0; i < erc721Details.ids.length; i++) { 78 | bytes5 catId = _uintToBytes5(erc721Details.ids[i]); 79 | address owner = IMoonCatsRescue(erc721Details.tokenAddr).catOwners(catId); 80 | require(owner == _msgSender(), "_acceptMoonCat: invalid mooncat owner"); 81 | IMoonCatsRescue(erc721Details.tokenAddr).acceptAdoptionOffer(catId); 82 | } 83 | } 84 | 85 | function _transferMoonCat(ERC721Details memory erc721Details) internal { 86 | for (uint256 i = 0; i < erc721Details.ids.length; i++) { 87 | IMoonCatsRescue(erc721Details.tokenAddr).giveCat(_uintToBytes5(erc721Details.ids[i]), erc721Details.to[i]); 88 | } 89 | } 90 | 91 | function _acceptCryptoPunk(ERC721Details memory erc721Details) internal { 92 | for (uint256 i = 0; i < erc721Details.ids.length; i++) { 93 | address owner = ICryptoPunks(erc721Details.tokenAddr).punkIndexToAddress(erc721Details.ids[i]); 94 | require(owner == _msgSender(), "_acceptCryptoPunk: invalid punk owner"); 95 | ICryptoPunks(erc721Details.tokenAddr).buyPunk(erc721Details.ids[i]); 96 | } 97 | } 98 | 99 | function _transferCryptoPunk(ERC721Details memory erc721Details) internal { 100 | for (uint256 i = 0; i < erc721Details.ids.length; i++) { 101 | ICryptoPunks(erc721Details.tokenAddr).transferPunk(erc721Details.to[i], erc721Details.ids[i]); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /contracts/BlurSwap/interfaces/ICryptoPunks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | interface ICryptoPunks { 6 | function punkIndexToAddress(uint index) external view returns(address owner); 7 | function offerPunkForSaleToAddress(uint punkIndex, uint minSalePriceInWei, address toAddress) external; 8 | function buyPunk(uint punkIndex) external payable; 9 | function transferPunk(address to, uint punkIndex) external; 10 | } 11 | -------------------------------------------------------------------------------- /contracts/BlurSwap/interfaces/IERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | interface IERC1155 { 6 | function safeTransferFrom( 7 | address from, 8 | address to, 9 | uint256 id, 10 | uint256 amount, 11 | bytes memory data 12 | ) external; 13 | 14 | function safeBatchTransferFrom( 15 | address from, 16 | address to, 17 | uint256[] memory ids, 18 | uint256[] memory amounts, 19 | bytes memory data 20 | ) external; 21 | 22 | function balanceOf(address _owner, uint256 _id) external view returns (uint256); 23 | } 24 | -------------------------------------------------------------------------------- /contracts/BlurSwap/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | interface IERC20 { 6 | /** 7 | * @dev Returns the amount of tokens owned by `account`. 8 | */ 9 | function balanceOf(address account) external view returns (uint256); 10 | 11 | /** 12 | * @dev Moves `amount` tokens from the caller's account to `recipient`. 13 | * 14 | * Returns a boolean value indicating whether the operation succeeded. 15 | * 16 | * Emits a {Transfer} event. 17 | */ 18 | function transfer(address recipient, uint256 amount) external returns (bool); 19 | 20 | /** 21 | * @dev Moves `amount` tokens from `sender` to `recipient` using the 22 | * allowance mechanism. `amount` is then deducted from the caller's 23 | * allowance. 24 | * 25 | * Returns a boolean value indicating whether the operation succeeded. 26 | * 27 | * Emits a {Transfer} event. 28 | */ 29 | function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); 30 | 31 | /** 32 | * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. 33 | * 34 | * Returns a boolean value indicating whether the operation succeeded. 35 | * 36 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 37 | * that someone may use both the old and the new allowance by unfortunate 38 | * transaction ordering. One possible solution to mitigate this race 39 | * condition is to first reduce the spender's allowance to 0 and set the 40 | * desired value afterwards: 41 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 42 | * 43 | * Emits an {Approval} event. 44 | */ 45 | function approve(address spender, uint256 amount) external returns (bool); 46 | 47 | /** 48 | * @dev Returns the remaining number of tokens that `spender` will be 49 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 50 | * zero by default. 51 | * 52 | * This value changes when {approve} or {transferFrom} are called. 53 | */ 54 | function allowance(address owner, address spender) external view returns (uint256); 55 | } 56 | -------------------------------------------------------------------------------- /contracts/BlurSwap/interfaces/IERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | interface IERC721 { 6 | /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE 7 | /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE 8 | /// THEY MAY BE PERMANENTLY LOST 9 | /// @dev Throws unless `msg.sender` is the current owner, an authorized 10 | /// operator, or the approved address for this NFT. Throws if `_from` is 11 | /// not the current owner. Throws if `_to` is the zero address. Throws if 12 | /// `_tokenId` is not a valid NFT. 13 | /// @param _from The current owner of the NFT 14 | /// @param _to The new owner 15 | /// @param _tokenId The NFT to transfer 16 | function transferFrom(address _from, address _to, uint256 _tokenId) external payable; 17 | 18 | function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) external; 19 | 20 | function setApprovalForAll(address operator, bool approved) external; 21 | 22 | function approve(address to, uint256 tokenId) external; 23 | 24 | function isApprovedForAll(address owner, address operator) external view returns (bool); 25 | 26 | function balanceOf(address _owner) external view returns (uint256); 27 | } 28 | -------------------------------------------------------------------------------- /contracts/BlurSwap/interfaces/IMoonCatsRescue.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | interface IMoonCatsRescue { 6 | function acceptAdoptionOffer(bytes5 catId) payable external; 7 | function makeAdoptionOfferToAddress(bytes5 catId, uint price, address to) external; 8 | function giveCat(bytes5 catId, address to) external; 9 | function catOwners(bytes5 catId) external view returns(address); 10 | function rescueOrder(uint256 rescueIndex) external view returns(bytes5 catId); 11 | } 12 | -------------------------------------------------------------------------------- /contracts/BlurSwap/interfaces/IWrappedPunk.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | interface IWrappedPunk { 6 | /** 7 | * @dev Mints a wrapped punk 8 | */ 9 | function mint(uint256 punkIndex) external; 10 | 11 | /** 12 | * @dev Burns a specific wrapped punk 13 | */ 14 | function burn(uint256 punkIndex) external; 15 | 16 | /** 17 | * @dev Registers proxy 18 | */ 19 | function registerProxy() external; 20 | 21 | /** 22 | * @dev Gets proxy address 23 | */ 24 | function proxyInfo(address user) external view returns (address); 25 | } 26 | -------------------------------------------------------------------------------- /contracts/BlurSwap/markets/MarketRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | contract MarketRegistry is Ownable { 9 | 10 | struct TradeDetails { 11 | uint256 marketId; 12 | uint256 value; 13 | bytes tradeData; 14 | } 15 | 16 | struct Market { 17 | address proxy; 18 | bool isLib; 19 | bool isActive; 20 | } 21 | 22 | Market[] public markets; 23 | 24 | constructor(address[] memory proxies, bool[] memory isLibs) { 25 | for (uint256 i = 0; i < proxies.length; i++) { 26 | markets.push(Market(proxies[i], isLibs[i], true)); 27 | } 28 | } 29 | 30 | function addMarket(address proxy, bool isLib) external onlyOwner { 31 | markets.push(Market(proxy, isLib, true)); 32 | } 33 | 34 | function setMarketStatus(uint256 marketId, bool newStatus) external onlyOwner { 35 | Market storage market = markets[marketId]; 36 | market.isActive = newStatus; 37 | } 38 | 39 | function setMarketProxy(uint256 marketId, address newProxy, bool isLib) external onlyOwner { 40 | Market storage market = markets[marketId]; 41 | market.proxy = newProxy; 42 | market.isLib = isLib; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contracts/BlurSwap/utils/ReentrancyGuard.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.13; 4 | 5 | /// @notice Gas optimized reentrancy protection for smart contracts. 6 | /// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) 7 | abstract contract ReentrancyGuard { 8 | uint256 private reentrancyStatus = 1; 9 | 10 | modifier nonReentrant() { 11 | require(reentrancyStatus == 1, "REENTRANCY"); 12 | 13 | reentrancyStatus = 2; 14 | 15 | _; 16 | 17 | reentrancyStatus = 1; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/PolicyManager/PolicyManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 | import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 6 | 7 | import {IPolicyManager} from "./interfaces/IPolicyManager.sol"; 8 | 9 | /** 10 | * @title PolicyManager 11 | * @dev Manages the policy whitelist for the Blur exchange 12 | */ 13 | contract PolicyManager is IPolicyManager, Ownable { 14 | using EnumerableSet for EnumerableSet.AddressSet; 15 | 16 | EnumerableSet.AddressSet private _whitelistedPolicies; 17 | 18 | event PolicyRemoved(address indexed policy); 19 | event PolicyWhitelisted(address indexed policy); 20 | 21 | /** 22 | * @notice Add matching policy 23 | * @param policy address of policy to add 24 | */ 25 | function addPolicy(address policy) external override onlyOwner { 26 | require(!_whitelistedPolicies.contains(policy), "Already whitelisted"); 27 | _whitelistedPolicies.add(policy); 28 | 29 | emit PolicyWhitelisted(policy); 30 | } 31 | 32 | /** 33 | * @notice Remove matching policy 34 | * @param policy address of policy to remove 35 | */ 36 | function removePolicy(address policy) external override onlyOwner { 37 | require(_whitelistedPolicies.contains(policy), "Not whitelisted"); 38 | _whitelistedPolicies.remove(policy); 39 | 40 | emit PolicyRemoved(policy); 41 | } 42 | 43 | /** 44 | * @notice Returns if a policy has been added 45 | * @param policy address of the policy to check 46 | */ 47 | function isPolicyWhitelisted(address policy) external view override returns (bool) { 48 | return _whitelistedPolicies.contains(policy); 49 | } 50 | 51 | /** 52 | * @notice View number of whitelisted policies 53 | */ 54 | function viewCountWhitelistedPolicies() external view override returns (uint256) { 55 | return _whitelistedPolicies.length(); 56 | } 57 | 58 | /** 59 | * @notice See whitelisted policies 60 | * @param cursor cursor 61 | * @param size size 62 | */ 63 | function viewWhitelistedPolicies(uint256 cursor, uint256 size) 64 | external 65 | view 66 | override 67 | returns (address[] memory, uint256) 68 | { 69 | uint256 length = size; 70 | 71 | if (length > _whitelistedPolicies.length() - cursor) { 72 | length = _whitelistedPolicies.length() - cursor; 73 | } 74 | 75 | address[] memory whitelistedPolicies = new address[](length); 76 | 77 | for (uint256 i = 0; i < length; i++) { 78 | whitelistedPolicies[i] = _whitelistedPolicies.at(cursor + i); 79 | } 80 | 81 | return (whitelistedPolicies, cursor + length); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /contracts/PolicyManager/interfaces/IMatchingPolicy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {Order, AssetType} from "../lib/OrderStructs.sol"; 5 | 6 | interface IMatchingPolicy { 7 | function canMatchMakerAsk(Order calldata makerAsk, Order calldata takerBid) 8 | external 9 | view 10 | returns ( 11 | bool, 12 | uint256, 13 | uint256, 14 | uint256, 15 | AssetType 16 | ); 17 | 18 | function canMatchMakerBid(Order calldata makerBid, Order calldata takerAsk) 19 | external 20 | view 21 | returns ( 22 | bool, 23 | uint256, 24 | uint256, 25 | uint256, 26 | AssetType 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /contracts/PolicyManager/interfaces/IPolicyManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | interface IPolicyManager { 5 | function addPolicy(address policy) external; 6 | 7 | function removePolicy(address policy) external; 8 | 9 | function isPolicyWhitelisted(address policy) external view returns (bool); 10 | 11 | function viewWhitelistedPolicies(uint256 cursor, uint256 size) external view returns (address[] memory, uint256); 12 | 13 | function viewCountWhitelistedPolicies() external view returns (uint256); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/PolicyManager/lib/OrderStructs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | enum Side { Buy, Sell } 5 | enum SignatureVersion { Single, Bulk } 6 | enum AssetType { ERC721, ERC1155 } 7 | 8 | struct Fee { 9 | uint16 rate; 10 | address payable recipient; 11 | } 12 | 13 | struct Order { 14 | address trader; 15 | Side side; 16 | address matchingPolicy; 17 | address collection; 18 | uint256 tokenId; 19 | uint256 amount; 20 | address paymentToken; 21 | uint256 price; 22 | uint256 listingTime; 23 | /* Order expiration timestamp - 0 for oracle cancellations. */ 24 | uint256 expirationTime; 25 | Fee[] fees; 26 | uint256 salt; 27 | bytes extraParams; 28 | } 29 | 30 | struct Input { 31 | Order order; 32 | uint8 v; 33 | bytes32 r; 34 | bytes32 s; 35 | bytes extraSignature; 36 | SignatureVersion signatureVersion; 37 | uint256 blockNumber; 38 | } 39 | 40 | struct Execution { 41 | Input sell; 42 | Input buy; 43 | } 44 | -------------------------------------------------------------------------------- /contracts/PolicyManager/matchingPolicies/SafeCollectionBidPolicyERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {Order, AssetType} from "../lib/OrderStructs.sol"; 5 | import {IMatchingPolicy} from "../interfaces/IMatchingPolicy.sol"; 6 | 7 | /** 8 | * @title SafeCollectionBidPolicyERC721 9 | * @dev Policy for matching orders where buyer will purchase any NON-SUSPICIOUS token from a collection 10 | */ 11 | contract SafeCollectionBidPolicyERC721 is IMatchingPolicy { 12 | function canMatchMakerAsk(Order calldata makerAsk, Order calldata takerBid) 13 | external 14 | pure 15 | override 16 | returns ( 17 | bool, 18 | uint256, 19 | uint256, 20 | uint256, 21 | AssetType 22 | ) 23 | { 24 | revert("Cannot be matched"); 25 | } 26 | 27 | function canMatchMakerBid(Order calldata makerBid, Order calldata takerAsk) 28 | external 29 | pure 30 | override 31 | returns ( 32 | bool, 33 | uint256, 34 | uint256, 35 | uint256, 36 | AssetType 37 | ) 38 | { 39 | return ( 40 | (makerBid.side != takerAsk.side) && 41 | (makerBid.paymentToken == takerAsk.paymentToken) && 42 | (makerBid.collection == takerAsk.collection) && 43 | (makerBid.extraParams.length > 0 && makerBid.extraParams[0] == "\x01") && 44 | (takerAsk.extraParams.length > 0 && takerAsk.extraParams[0] == "\x01") && 45 | (makerBid.amount == 1) && 46 | (takerAsk.amount == 1) && 47 | (makerBid.matchingPolicy == takerAsk.matchingPolicy) && 48 | (makerBid.price == takerAsk.price), 49 | makerBid.price, 50 | takerAsk.tokenId, 51 | 1, 52 | AssetType.ERC721 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/PolicyManager/matchingPolicies/StandardPolicyERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {Order, AssetType} from "../lib/OrderStructs.sol"; 5 | import {IMatchingPolicy} from "../interfaces/IMatchingPolicy.sol"; 6 | 7 | /** 8 | * @title StandardPolicyERC721 9 | * @dev Policy for matching orders at a fixed price for a specific ERC721 tokenId 10 | */ 11 | contract StandardPolicyERC721 is IMatchingPolicy { 12 | function canMatchMakerAsk(Order calldata makerAsk, Order calldata takerBid) 13 | external 14 | pure 15 | override 16 | returns ( 17 | bool, 18 | uint256, 19 | uint256, 20 | uint256, 21 | AssetType 22 | ) 23 | { 24 | return ( 25 | (makerAsk.side != takerBid.side) && 26 | (makerAsk.paymentToken == takerBid.paymentToken) && 27 | (makerAsk.collection == takerBid.collection) && 28 | (makerAsk.tokenId == takerBid.tokenId) && 29 | (makerAsk.amount == 1) && 30 | (takerBid.amount == 1) && 31 | (makerAsk.matchingPolicy == takerBid.matchingPolicy) && 32 | (makerAsk.price == takerBid.price), 33 | makerAsk.price, 34 | makerAsk.tokenId, 35 | 1, 36 | AssetType.ERC721 37 | ); 38 | } 39 | 40 | function canMatchMakerBid(Order calldata makerBid, Order calldata takerAsk) 41 | external 42 | pure 43 | override 44 | returns ( 45 | bool, 46 | uint256, 47 | uint256, 48 | uint256, 49 | AssetType 50 | ) 51 | { 52 | return ( 53 | (makerBid.side != takerAsk.side) && 54 | (makerBid.paymentToken == takerAsk.paymentToken) && 55 | (makerBid.collection == takerAsk.collection) && 56 | (makerBid.tokenId == takerAsk.tokenId) && 57 | (makerBid.amount == 1) && 58 | (takerAsk.amount == 1) && 59 | (makerBid.matchingPolicy == takerAsk.matchingPolicy) && 60 | (makerBid.price == takerAsk.price), 61 | makerBid.price, 62 | makerBid.tokenId, 63 | 1, 64 | AssetType.ERC721 65 | ); 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /contracts/PolicyManager/matchingPolicies/StandardPolicyERC721_1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {Order, AssetType} from "../lib/OrderStructs.sol"; 5 | import {IMatchingPolicy} from "../interfaces/IMatchingPolicy.sol"; 6 | 7 | /** 8 | * @title StandardPolicyERC721 9 | * @dev Policy for matching orders at a fixed price for a specific ERC721 tokenId (requires oracle authorization on both orders) 10 | */ 11 | contract StandardPolicyERC721 is IMatchingPolicy { 12 | function canMatchMakerAsk(Order calldata makerAsk, Order calldata takerBid) 13 | external 14 | pure 15 | override 16 | returns ( 17 | bool, 18 | uint256, 19 | uint256, 20 | uint256, 21 | AssetType 22 | ) 23 | { 24 | return ( 25 | (makerAsk.side != takerBid.side) && 26 | (makerAsk.paymentToken == takerBid.paymentToken) && 27 | (makerAsk.collection == takerBid.collection) && 28 | (makerAsk.tokenId == takerBid.tokenId) && 29 | (makerAsk.extraParams.length > 0 && makerAsk.extraParams[0] == "\x01") && 30 | (takerBid.extraParams.length > 0 && takerBid.extraParams[0] == "\x01") && 31 | (makerAsk.amount == 1) && 32 | (takerBid.amount == 1) && 33 | (makerAsk.matchingPolicy == takerBid.matchingPolicy) && 34 | (makerAsk.price == takerBid.price), 35 | makerAsk.price, 36 | makerAsk.tokenId, 37 | 1, 38 | AssetType.ERC721 39 | ); 40 | } 41 | 42 | function canMatchMakerBid(Order calldata makerBid, Order calldata takerAsk) 43 | external 44 | pure 45 | override 46 | returns ( 47 | bool, 48 | uint256, 49 | uint256, 50 | uint256, 51 | AssetType 52 | ) 53 | { 54 | return ( 55 | (makerBid.side != takerAsk.side) && 56 | (makerBid.paymentToken == takerAsk.paymentToken) && 57 | (makerBid.collection == takerAsk.collection) && 58 | (makerBid.tokenId == takerAsk.tokenId) && 59 | (makerBid.extraParams.length > 0 && makerBid.extraParams[0] == "\x01") && 60 | (takerAsk.extraParams.length > 0 && takerAsk.extraParams[0] == "\x01") && 61 | (makerBid.amount == 1) && 62 | (takerAsk.amount == 1) && 63 | (makerBid.matchingPolicy == takerAsk.matchingPolicy) && 64 | (makerBid.price == takerAsk.price), 65 | makerBid.price, 66 | makerBid.tokenId, 67 | 1, 68 | AssetType.ERC721 69 | ); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /exchange_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteWizardJ/blur-analysis/caddf818d627c5b4a001cfd82850feff0bc31d80/exchange_architecture.png -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox"); 2 | 3 | /** @type import('hardhat/config').HardhatUserConfig */ 4 | module.exports = { 5 | solidity: { 6 | compilers: [{ 7 | version: "0.8.17", 8 | settings: { 9 | optimizer: { 10 | enabled: true, 11 | runs: 200, 12 | }, 13 | } 14 | }, { 15 | version: "0.8.13", 16 | settings: { 17 | optimizer: { 18 | enabled: true, 19 | runs: 200, 20 | }, 21 | } 22 | }], 23 | 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /mainnet-0x39da41747a83aee658334415666f3ef92dd0d541.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | UmlClassDiagram 11 | 12 | 13 | 14 | 1 15 | 16 | <<Abstract>> 17 | Ownable 18 | 19 | Private: 20 |   _owner: address 21 | 22 | Internal: 23 |    _transferOwnership(newOwner: address) 24 | Public: 25 |    <<event>> OwnershipTransferred(previousOwner: address, newOwner: address) 26 |    <<modifier>> onlyOwner() 27 |    constructor() 28 |    owner(): address 29 |    renounceOwnership() 30 |    transferOwnership(newOwner: address) 31 | 32 | 33 | 34 | 8 35 | 36 | <<Abstract>> 37 | Context 38 | 39 | 40 | 41 | Internal: 42 |    _msgSender(): address 43 |    _msgData(): bytes 44 | 45 | 46 | 47 | 0 48 | 49 | BlurSwap 50 | 51 | Public: 52 |   GOV: address 53 |   guardian: address 54 |   converter: address 55 |   punkProxy: address 56 |   baseFees: uint256 57 |   openForTrades: bool 58 |   openForFreeTrades: bool 59 |   marketRegistry: MarketRegistry 60 |   affiliates: AffiliateDetails[] 61 |   sponsoredMarkets: SponsoredMarket[] 62 | 63 | Internal: 64 |    _transferEth(_to: address, _amount: uint256) 65 |    _collectFee(feeDetails: uint256[]) 66 |    _checkCallResult(_success: bool) 67 |    _transferFromHelper(erc20Details: ERC20Details, erc721Details: SpecialTransferHelper.ERC721Details[], erc1155Details: ERC1155Details[]) 68 |    _conversionHelper(_converstionDetails: ConverstionDetails[]) 69 |    _trade(_tradeDetails: MarketRegistry.TradeDetails[]) 70 |    _returnDust(_tokens: address[]) 71 | External: 72 |    <<payable>> batchBuyFromOpenSea(openseaTrades: OpenseaTrades[]) 73 |    <<payable>> batchBuyWithETH(tradeDetails: MarketRegistry.TradeDetails[]) 74 |    <<payable>> batchBuyWithERC20s(erc20Details: ERC20Details, tradeDetails: MarketRegistry.TradeDetails[], converstionDetails: ConverstionDetails[], dustTokens: address[]) 75 |    <<payable>> multiAssetSwap(erc20Details: ERC20Details, erc721Details: SpecialTransferHelper.ERC721Details[], erc1155Details: ERC1155Details[], converstionDetails: ConverstionDetails[], tradeDetails: MarketRegistry.TradeDetails[], dustTokens: address[], feeDetails: uint256[]) 76 |    <<payable>> null() 77 |    setUp() 78 |    setOneTimeApproval(token: IERC20, operator: address, amount: uint256) 79 |    updateGuardian(_guardian: address) 80 |    addAffiliate(_affiliate: address) 81 |    updateAffiliate(_affiliateIndex: uint256, _affiliate: address, _IsActive: bool) 82 |    addSponsoredMarket(_marketId: uint256) 83 |    updateSponsoredMarket(_marketIndex: uint256, _marketId: uint256, _isActive: bool) 84 |    setBaseFees(_baseFees: uint256) 85 |    setOpenForTrades(_openForTrades: bool) 86 |    setOpenForFreeTrades(_openForFreeTrades: bool) 87 |    closeAllTrades() 88 |    setConverter(_converter: address) 89 |    setMarketRegistry(_marketRegistry: MarketRegistry) 90 |    onERC721Received(address, address, uint256, bytes): bytes4 91 |    onERC721Received(address, uint256, bytes): bytes4 92 |    supportsInterface(interfaceId: bytes4): bool 93 |    rescueETH(recipient: address) 94 |    rescueERC20(asset: address, recipient: address) 95 |    rescueERC721(asset: address, ids: uint256[], recipient: address) 96 |    rescueERC1155(asset: address, ids: uint256[], amounts: uint256[], recipient: address) 97 | Public: 98 |    <<modifier>> isOpenForTrades() 99 |    <<modifier>> isOpenForFreeTrades() 100 |    constructor(_marketRegistry: address, _guardian: address) 101 |    onERC1155Received(address, address, uint256, uint256, bytes): bytes4 102 |    onERC1155BatchReceived(address, address, uint256[], uint256[], bytes): bytes4 103 | 104 | 105 | 106 | 0struct0 107 | 108 | <<struct>> 109 | OpenseaTrades 110 | 111 | value: uint256 112 | tradeData: bytes 113 | 114 | 115 | 116 | 0struct0->0 117 | 118 | 119 | 120 | 121 | 122 | 0struct1 123 | 124 | <<struct>> 125 | ERC20Details 126 | 127 | tokenAddrs: address[] 128 | amounts: uint256[] 129 | 130 | 131 | 132 | 0struct1->0 133 | 134 | 135 | 136 | 137 | 138 | 0struct2 139 | 140 | <<struct>> 141 | ERC1155Details 142 | 143 | tokenAddr: address 144 | ids: uint256[] 145 | amounts: uint256[] 146 | 147 | 148 | 149 | 0struct2->0 150 | 151 | 152 | 153 | 154 | 155 | 0struct3 156 | 157 | <<struct>> 158 | ConverstionDetails 159 | 160 | conversionData: bytes 161 | 162 | 163 | 164 | 0struct3->0 165 | 166 | 167 | 168 | 169 | 170 | 0struct4 171 | 172 | <<struct>> 173 | AffiliateDetails 174 | 175 | affiliate: address 176 | isActive: bool 177 | 178 | 179 | 180 | 0struct4->0 181 | 182 | 183 | 184 | 185 | 186 | 0struct5 187 | 188 | <<struct>> 189 | SponsoredMarket 190 | 191 | marketId: uint256 192 | isActive: bool 193 | 194 | 195 | 196 | 0struct5->0 197 | 198 | 199 | 200 | 201 | 202 | 4 203 | 204 | SpecialTransferHelper 205 | 206 | 207 | 208 | Internal: 209 |    _uintToBytes5(id: uint256): (slicedDataBytes5: bytes5) 210 |    _acceptMoonCat(erc721Details: ERC721Details) 211 |    _transferMoonCat(erc721Details: ERC721Details) 212 |    _acceptCryptoPunk(erc721Details: ERC721Details) 213 |    _transferCryptoPunk(erc721Details: ERC721Details) 214 | 215 | 216 | 217 | 4struct0 218 | 219 | <<struct>> 220 | ERC721Details 221 | 222 | tokenAddr: address 223 | to: address[] 224 | ids: uint256[] 225 | 226 | 227 | 228 | 4struct0->4 229 | 230 | 231 | 232 | 233 | 234 | 9 235 | 236 | <<Interface>> 237 | ICryptoPunks 238 | 239 | 240 | 241 | External: 242 |     punkIndexToAddress(index: uint): (owner: address) 243 |     offerPunkForSaleToAddress(punkIndex: uint, minSalePriceInWei: uint, toAddress: address) 244 |     buyPunk(punkIndex: uint) 245 |     transferPunk(to: address, punkIndex: uint) 246 | 247 | 248 | 249 | 7 250 | 251 | <<Interface>> 252 | IERC1155 253 | 254 | 255 | 256 | External: 257 |     safeTransferFrom(from: address, to: address, id: uint256, amount: uint256, data: bytes) 258 |     safeBatchTransferFrom(from: address, to: address, ids: uint256[], amounts: uint256[], data: bytes) 259 |     balanceOf(_owner: address, _id: uint256): uint256 260 | 261 | 262 | 263 | 5 264 | 265 | <<Interface>> 266 | IERC20 267 | 268 | 269 | 270 | External: 271 |     balanceOf(account: address): uint256 272 |     transfer(recipient: address, amount: uint256): bool 273 |     transferFrom(sender: address, recipient: address, amount: uint256): bool 274 |     approve(spender: address, amount: uint256): bool 275 |     allowance(owner: address, spender: address): uint256 276 | 277 | 278 | 279 | 6 280 | 281 | <<Interface>> 282 | IERC721 283 | 284 | 285 | 286 | External: 287 |     transferFrom(_from: address, _to: address, _tokenId: uint256) 288 |     safeTransferFrom(from: address, to: address, tokenId: uint256, _data: bytes) 289 |     setApprovalForAll(operator: address, approved: bool) 290 |     approve(to: address, tokenId: uint256) 291 |     isApprovedForAll(owner: address, operator: address): bool 292 |     balanceOf(_owner: address): uint256 293 | 294 | 295 | 296 | 11 297 | 298 | <<Interface>> 299 | IMoonCatsRescue 300 | 301 | 302 | 303 | External: 304 |     acceptAdoptionOffer(catId: bytes5) 305 |     makeAdoptionOfferToAddress(catId: bytes5, price: uint, to: address) 306 |     giveCat(catId: bytes5, to: address) 307 |     catOwners(catId: bytes5): address 308 |     rescueOrder(rescueIndex: uint256): (catId: bytes5) 309 | 310 | 311 | 312 | 10 313 | 314 | <<Interface>> 315 | IWrappedPunk 316 | 317 | 318 | 319 | External: 320 |     mint(punkIndex: uint256) 321 |     burn(punkIndex: uint256) 322 |     registerProxy() 323 |     proxyInfo(user: address): address 324 | 325 | 326 | 327 | 3 328 | 329 | MarketRegistry 330 | 331 | Public: 332 |   markets: Market[] 333 | 334 | External: 335 |    addMarket(proxy: address, isLib: bool) 336 |    setMarketStatus(marketId: uint256, newStatus: bool) 337 |    setMarketProxy(marketId: uint256, newProxy: address, isLib: bool) 338 | Public: 339 |    constructor(proxies: address[], isLibs: bool[]) 340 | 341 | 342 | 343 | 3struct0 344 | 345 | <<struct>> 346 | TradeDetails 347 | 348 | marketId: uint256 349 | value: uint256 350 | tradeData: bytes 351 | 352 | 353 | 354 | 3struct0->3 355 | 356 | 357 | 358 | 359 | 360 | 3struct1 361 | 362 | <<struct>> 363 | Market 364 | 365 | proxy: address 366 | isLib: bool 367 | isActive: bool 368 | 369 | 370 | 371 | 3struct1->3 372 | 373 | 374 | 375 | 376 | 377 | 2 378 | 379 | <<Abstract>> 380 | ReentrancyGuard 381 | 382 | Private: 383 |   reentrancyStatus: uint256 384 | 385 | Public: 386 |    <<modifier>> nonReentrant() 387 | 388 | 389 | 390 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blur-analysis", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/cryptochou/blur-analysis.git", 6 | "author": "jukes ", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "@ethersproject/abi": "^5.4.7", 10 | "@ethersproject/providers": "^5.4.7", 11 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.0", 12 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 13 | "@nomicfoundation/hardhat-toolbox": "^2.0.0", 14 | "@nomiclabs/hardhat-ethers": "^2.0.0", 15 | "@nomiclabs/hardhat-etherscan": "^3.0.0", 16 | "@typechain/ethers-v5": "^10.1.0", 17 | "@typechain/hardhat": "^6.1.2", 18 | "chai": "^4.2.0", 19 | "ethers": "^5.4.7", 20 | "hardhat": "^2.12.7", 21 | "hardhat-gas-reporter": "^1.0.8", 22 | "solidity-coverage": "^0.8.0", 23 | "typechain": "^8.1.0" 24 | }, 25 | "dependencies": { 26 | "@openzeppelin/contracts": "^4.8.1", 27 | "@openzeppelin/contracts-upgradeable": "^4.8.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | // We require the Hardhat Runtime Environment explicitly here. This is optional 2 | // but useful for running the script in a standalone fashion through `node