├── audit-report ├── Salus_XRGB-X404_Audit_Report-v1.0.pdf └── PeckShield_XRGB-X404_Audit_Report-v1.0.pdf ├── test ├── emptyrun.coverage.ts ├── helpers │ ├── errors.ts │ ├── constants.ts │ └── utils.ts ├── __setup.spec.ts ├── X404Hub │ └── createX404.spec.ts └── X404 │ ├── depositNFT.spec.ts │ └── redeemNFT.spec.ts ├── .gitignore ├── tsconfig.json ├── contracts ├── lib │ ├── ERC20Events.sol │ ├── ERC721Events.sol │ ├── DataTypes.sol │ ├── Errors.sol │ ├── Events.sol │ ├── LibCalculatePair.sol │ └── DoubleEndedQueue.sol ├── interfaces │ ├── IUniswapV2Router.sol │ ├── IPeripheryImmutableState.sol │ ├── IX404Hub.sol │ ├── IUniswapV3PoolState.sol │ └── IERC404.sol ├── storage │ ├── X404HubStorage.sol │ └── X404Storage.sol ├── mock │ ├── BlueChipNFT.sol │ ├── MockUniswapV2Router02.sol │ └── MockUniswapV3.sol ├── X404Hub.sol ├── X404.sol └── ERC404.sol ├── package.json ├── README.md ├── deploy ├── 02-create-X404.ts └── 00-deploy-X404Hub_spec.ts ├── hardhat.config.ts └── scripts └── deploy-utils.ts /audit-report/Salus_XRGB-X404_Audit_Report-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XRGB/xrgb-x404/HEAD/audit-report/Salus_XRGB-X404_Audit_Report-v1.0.pdf -------------------------------------------------------------------------------- /test/emptyrun.coverage.ts: -------------------------------------------------------------------------------- 1 | import { makeSuiteCleanRoom } from './__setup.spec'; 2 | 3 | makeSuiteCleanRoom('Empty Run for Coverage', function () {}); 4 | -------------------------------------------------------------------------------- /audit-report/PeckShield_XRGB-X404_Audit_Report-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XRGB/xrgb-x404/HEAD/audit-report/PeckShield_XRGB-X404_Audit_Report-v1.0.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | .openzeppelin -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/lib/ERC20Events.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | library ERC20Events { 5 | event Approval( 6 | address indexed owner, 7 | address indexed spender, 8 | uint256 value 9 | ); 10 | event Transfer(address indexed from, address indexed to, uint256 amount); 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-project", 3 | "devDependencies": { 4 | "@nomicfoundation/hardhat-toolbox": "^4.0.0", 5 | "@openzeppelin/contracts": "^5.0.1", 6 | "@openzeppelin/contracts-upgradeable": "^5.0.1", 7 | "@openzeppelin/hardhat-upgrades": "^3.0.4", 8 | "dotenv": "^16.4.5", 9 | "hardhat": "^2.20.1", 10 | "hardhat-deploy": "^0.11.45" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample Hardhat Project 2 | 3 | This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a script that deploys that contract. 4 | 5 | Try running some of the following tasks: 6 | 7 | ```shell 8 | npx hardhat help 9 | npx hardhat test 10 | REPORT_GAS=true npx hardhat test 11 | npx hardhat node 12 | npx hardhat run scripts/deploy.ts 13 | ``` 14 | -------------------------------------------------------------------------------- /test/helpers/errors.ts: -------------------------------------------------------------------------------- 1 | export const ERRORS = { 2 | NotBlueChipNFT: "NotBlueChipNFT", 3 | InvaildRedeemMaxDeadline: "InvaildRedeemMaxDeadline", 4 | EmergencyClose: "EmergencyClose", 5 | X404NotCreate: "X404NotCreate", 6 | InvalidDeadLine: "InvalidDeadLine", 7 | InvalidNFTAddress: "InvalidNFTAddress", 8 | InvalidLength: "InvalidLength", 9 | NFTCannotRedeem: "NFTCannotRedeem", 10 | }; 11 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Router.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity >=0.6.2; 3 | 4 | interface IUniswapV2Router { 5 | function factory() external view returns (address); 6 | 7 | function WETH() external view returns (address); 8 | 9 | function getAmountsOut( 10 | uint amountIn, 11 | address[] calldata path 12 | ) external view returns (uint[] memory amounts); 13 | } 14 | -------------------------------------------------------------------------------- /test/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; 2 | export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 3 | export const BYTES32_ZERO_ADDRESS = '0x0000000000000000000000000000000000000000000000000000000000000000' 4 | 5 | // Fetched from $npx hardhat node, account # 7 6 | export const SIGN_PRIVATEKEY = ''; 7 | export const HARDHAT_CHAINID = 31337; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IPeripheryImmutableState.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | /// @title Immutable state 5 | /// @notice Functions that return immutable state of the router 6 | interface IPeripheryImmutableState { 7 | /// @return Returns the address of the Uniswap V3 factory 8 | function factory() external view returns (address); 9 | 10 | /// @return Returns the address of WETH9 11 | function WETH9() external view returns (address); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/IX404Hub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | import {DataTypes} from "../lib/DataTypes.sol"; 4 | 5 | interface IX404Hub { 6 | function _parameters() 7 | external 8 | view 9 | returns (address blueChipNft, address creator, uint256 deadline); 10 | 11 | function owner() external view returns (address owner); 12 | 13 | function getSwapRouter() 14 | external 15 | view 16 | returns (DataTypes.SwapRouter[] memory); 17 | } 18 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV3PoolState.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | interface IUniswapV3PoolState { 5 | function slot0() 6 | external 7 | view 8 | returns ( 9 | uint160 sqrtPriceX96, 10 | int24 tick, 11 | uint16 observationIndex, 12 | uint16 observationCardinality, 13 | uint16 observationCardinalityNext, 14 | uint8 feeProtocol, 15 | bool unlocked 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /contracts/lib/ERC721Events.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | library ERC721Events { 5 | event ApprovalForAll( 6 | address indexed owner, 7 | address indexed operator, 8 | bool approved 9 | ); 10 | event Approval( 11 | address indexed owner, 12 | address indexed spender, 13 | uint256 indexed id 14 | ); 15 | event Transfer( 16 | address indexed from, 17 | address indexed to, 18 | uint256 indexed id 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /contracts/storage/X404HubStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | import {DataTypes} from "../lib/DataTypes.sol"; 4 | 5 | abstract contract X404HubStorage { 6 | DataTypes.CreateX404Parameters public _parameters; 7 | bool internal _bNoPermission; 8 | bool internal _bEmergencyClose; 9 | uint256 public _redeemMaxDeadline; 10 | DataTypes.SwapRouter[] public _swapRouterAddr; 11 | mapping(address => bool) public _blueChipNftContract; 12 | mapping(address => address) public _x404Contract; 13 | } 14 | -------------------------------------------------------------------------------- /contracts/mock/BlueChipNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.17; 4 | import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | 6 | contract BlueChipNFT is ERC721 { 7 | uint256 public tokenId; 8 | 9 | constructor() ERC721("BlueChip", "BCN") {} 10 | 11 | function mint() public { 12 | _safeMint(msg.sender, tokenId++); 13 | } 14 | 15 | function _baseURI() internal view virtual override returns (string memory) { 16 | return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /contracts/lib/DataTypes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.17; 4 | 5 | /** 6 | * @title DataTypes 7 | * @author Tomo Protocol 8 | * 9 | * @notice A standard library of data types used throughout the XRGB. 10 | */ 11 | library DataTypes { 12 | struct CreateX404Parameters { 13 | address nftContractAddr; 14 | address creator; 15 | uint256 redeemMaxDeadline; 16 | } 17 | 18 | struct SwapRouter { 19 | bool bV2orV3; 20 | address routerAddr; 21 | address uniswapV3NonfungiblePositionManager; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/mock/MockUniswapV2Router02.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | import {IUniswapV2Router} from "../interfaces/IUniswapV2Router.sol"; 4 | 5 | contract MockUniswapV2Router02 is IUniswapV2Router { 6 | address public immutable override factory; 7 | address public immutable override WETH; 8 | 9 | constructor(address _factory, address _WETH) { 10 | factory = _factory; 11 | WETH = _WETH; 12 | } 13 | 14 | function getAmountsOut( 15 | uint amountIn, 16 | address[] calldata path 17 | ) external view returns (uint[] memory amounts) {} 18 | } 19 | -------------------------------------------------------------------------------- /contracts/lib/Errors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.17; 4 | 5 | library Errors { 6 | error InvalidLength(); 7 | error OnlyCallByFactory(); 8 | error NotBlueChipNFT(); 9 | error X404NotCreate(); 10 | error CantBeZeroAddress(); 11 | error X404SwapV3FactoryMismatch(); 12 | error InvalidNFTAddress(); 13 | error InvalidDeadLine(); 14 | error NFTCannotRedeem(); 15 | error RemoveFailed(); 16 | error EmergencyClose(); 17 | error InvaildRedeemMaxDeadline(); 18 | error MsgValueNotEnough(); 19 | error SendETHFailed(); 20 | error RedeemFeeTooHigh(); 21 | } 22 | -------------------------------------------------------------------------------- /contracts/mock/MockUniswapV3.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {IPeripheryImmutableState} from "../interfaces/IPeripheryImmutableState.sol"; 5 | 6 | /// @title Immutable state 7 | /// @notice Immutable state used by periphery contracts 8 | abstract contract PeripheryImmutableState is IPeripheryImmutableState { 9 | /// @inheritdoc IPeripheryImmutableState 10 | address public immutable override factory; 11 | /// @inheritdoc IPeripheryImmutableState 12 | address public immutable override WETH9; 13 | 14 | constructor(address _factory, address _WETH9) { 15 | factory = _factory; 16 | WETH9 = _WETH9; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /contracts/lib/Events.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | library Events { 5 | event X404Created( 6 | address indexed addr, 7 | address indexed blueChipNftAddr, 8 | address indexed creator 9 | ); 10 | 11 | event X404DepositNFT( 12 | address indexed caller, 13 | address indexed from, 14 | uint256 indexed tokenId, 15 | uint256 redeemDeadline 16 | ); 17 | 18 | event X404RedeemNFT( 19 | address indexed redeemer, 20 | address indexed depositor, 21 | uint256 indexed tokenId 22 | ); 23 | 24 | event SetContractURI(string indexed contractURI); 25 | event SetTokenURI(string indexed tokenURI); 26 | event SetRedeemFee(uint256 indexed redeemFee); 27 | } 28 | -------------------------------------------------------------------------------- /contracts/storage/X404Storage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 4 | 5 | abstract contract X404Storage { 6 | string public contractURI; 7 | string public baseURI; 8 | uint256 public redeemFee; 9 | 10 | //if not redeem your nft before deadline, others who hold units token maybe can get your nft, if no one get, you also can get your origin nft. 11 | uint256 public maxRedeemDeadline; 12 | 13 | struct NFTDepositInfo { 14 | address caller; 15 | address oriOwner; 16 | uint256 redeemDeadline; 17 | } 18 | 19 | EnumerableSet.UintSet internal tokenIdSet; 20 | mapping(uint256 => NFTDepositInfo) public nftDepositInfo; 21 | } 22 | -------------------------------------------------------------------------------- /deploy/02-create-X404.ts: -------------------------------------------------------------------------------- 1 | /* Imports: Internal */ 2 | import { DeployFunction } from 'hardhat-deploy/dist/types' 3 | import { ethers } from 'hardhat'; 4 | import { BlueChipNFT__factory, X404Hub__factory } from '../typechain-types'; 5 | import { isHardhatNode } from '../scripts/deploy-utils'; 6 | 7 | const deployFn: DeployFunction = async (hre) => { 8 | //if(await isHardhatNode(hre)){ 9 | const [ deployer, owner ] = await ethers.getSigners(); 10 | 11 | const proxyAddress = "0x16be924A3AF57E1c293818894810b591aDFf82b1" 12 | 13 | const blueChipNFTAddress = "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D" 14 | 15 | const x404Hub = X404Hub__factory.connect(proxyAddress) 16 | const tokenUri = '' 17 | 18 | const tx = await x404Hub.connect(deployer).createX404(blueChipNFTAddress) 19 | tx.wait(); 20 | console.log("createX404 successful.") 21 | 22 | const x404Addr = await x404Hub.connect(deployer)._x404Contract(blueChipNFTAddress) 23 | console.log("x404Addr: ", x404Addr) 24 | 25 | const tx2 = await x404Hub.connect(deployer).setTokenURI(blueChipNFTAddress, tokenUri) 26 | tx2.wait(); 27 | console.log("setTokenURI successful.") 28 | } 29 | 30 | // This is kept during an upgrade. So no upgrade tag. 31 | deployFn.tags = ['CreateX404'] 32 | 33 | export default deployFn 34 | -------------------------------------------------------------------------------- /contracts/lib/LibCalculatePair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | import {DataTypes} from "./DataTypes.sol"; 4 | import {IUniswapV3PoolState} from "../interfaces/IUniswapV3PoolState.sol"; 5 | import {IUniswapV2Router} from "../interfaces/IUniswapV2Router.sol"; 6 | import {IPeripheryImmutableState} from "../interfaces/IPeripheryImmutableState.sol"; 7 | 8 | library LibCalculatePair { 9 | function _getUniswapV2Pair( 10 | address uniswapV2Factory_, 11 | address tokenA, 12 | address tokenB 13 | ) internal pure returns (address) { 14 | (address token0, address token1) = tokenA < tokenB 15 | ? (tokenA, tokenB) 16 | : (tokenB, tokenA); 17 | return 18 | address( 19 | uint160( 20 | uint256( 21 | keccak256( 22 | abi.encodePacked( 23 | hex"ff", 24 | uniswapV2Factory_, 25 | keccak256(abi.encodePacked(token0, token1)), 26 | hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" 27 | ) 28 | ) 29 | ) 30 | ) 31 | ); 32 | } 33 | 34 | function _getUniswapV3Pair( 35 | address uniswapV3Factory_, 36 | address tokenA, 37 | address tokenB, 38 | uint24 fee_ 39 | ) internal pure returns (address) { 40 | (address token0, address token1) = tokenA < tokenB 41 | ? (tokenA, tokenB) 42 | : (tokenB, tokenA); 43 | return 44 | address( 45 | uint160( 46 | uint256( 47 | keccak256( 48 | abi.encodePacked( 49 | hex"ff", 50 | uniswapV3Factory_, 51 | keccak256(abi.encode(token0, token1, fee_)), 52 | hex"e34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" 53 | ) 54 | ) 55 | ) 56 | ) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /deploy/00-deploy-X404Hub_spec.ts: -------------------------------------------------------------------------------- 1 | /* Imports: Internal */ 2 | import { DeployFunction } from 'hardhat-deploy/dist/types' 3 | import { ZeroAddress } from 'ethers'; 4 | import { ethers, upgrades } from 'hardhat'; 5 | import { X404Hub__factory } from '../typechain-types'; 6 | 7 | const deployFn: DeployFunction = async (hre) => { 8 | const [ deployer, owner ] = await ethers.getSigners(); 9 | 10 | const swapRouterArray = [ 11 | //eth-mainnet 12 | { 13 | bV2orV3: true, 14 | routerAddr: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', 15 | uniswapV3NonfungiblePositionManager: ZeroAddress, 16 | }, 17 | { 18 | bV2orV3: false, 19 | routerAddr: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', 20 | uniswapV3NonfungiblePositionManager: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88', 21 | }, 22 | ]; 23 | 24 | const maxRedeemDeadline = 180 * 24 * 60 * 60; 25 | const X404Hub = await ethers.getContractFactory("X404Hub"); 26 | const proxy = await upgrades.deployProxy(X404Hub, [deployer.address, maxRedeemDeadline, swapRouterArray]); 27 | await proxy.waitForDeployment() 28 | 29 | const proxyAddress = await proxy.getAddress() 30 | console.log("proxy address: ", proxyAddress) 31 | console.log("admin address: ", await upgrades.erc1967.getAdminAddress(proxyAddress)) 32 | console.log("implement address: ", await upgrades.erc1967.getImplementationAddress(proxyAddress)) 33 | 34 | const blue_chip_addresses = [ 35 | '0xBd3531dA5CF5857e7CfAA92426877b022e612cf8', //PudgyPenguins (PPG) 36 | '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', //BoredApeYachtClub (BAYC) 37 | '0x60E4d786628Fea6478F785A6d7e704777c86a7c6', //MutantApeYachtClub (MAYC) C URL 38 | '0x524cAB2ec69124574082676e6F654a18df49A048', //LilPudgys (LP) C URL 39 | '0x8821BeE2ba0dF28761AffF119D66390D594CD280', //DeGods (DEGODS) upgrade contract & C URL 40 | '0x23581767a106ae21c074b2276D25e5C3e136a68b', //Moonbirds (MOONBIRD) C URL 41 | '0xED5AF388653567Af2F388E6224dC7C4b3241C544', //Azuki (AZUKI) 42 | '0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e', //Doodles (DOODLE) 43 | '0x59468516a8259058baD1cA5F8f4BFF190d30E066', //Invisible Friends (INVSBLE) 44 | '0x5Af0D9827E0c53E4799BB226655A1de152A425a5' //Milady (MIL) 45 | ] 46 | const x404Hub = X404Hub__factory.connect(proxyAddress) 47 | await x404Hub.connect(deployer).setBlueChipNftContract(blue_chip_addresses, true) 48 | console.log("setBlueChipNftContract successful.") 49 | } 50 | 51 | // This is kept during an upgrade. So no upgrade tag. 52 | deployFn.tags = ['DeployX404Hub'] 53 | 54 | export default deployFn 55 | -------------------------------------------------------------------------------- /test/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { hexlify, keccak256, Contract, toUtf8Bytes, TransactionReceipt, TransactionResponse } from 'ethers'; 3 | import { encode } from '@ethersproject/rlp' 4 | import { expect } from 'chai'; 5 | import { HARDHAT_CHAINID } from './constants'; 6 | import hre from 'hardhat'; 7 | import { eventsLib } from '../__setup.spec'; 8 | import { Events } from '../../typechain-types'; 9 | 10 | export function getChainId(): number { 11 | return hre.network.config.chainId || HARDHAT_CHAINID; 12 | } 13 | 14 | export function computeContractAddress(deployerAddress: string, nonce: number): string { 15 | const hexNonce = hexlify(nonce.toString()); 16 | return '0x' + keccak256(encode([deployerAddress, hexNonce])).substr(26); 17 | } 18 | 19 | export function findEvent( 20 | receipt: TransactionReceipt, 21 | name: string, 22 | eventContract: Events = eventsLib, 23 | emitterAddress?: string 24 | ) { 25 | const events = receipt.logs; 26 | 27 | if (events != undefined) { 28 | // match name from list of events in eventContract, when found, compute the sigHash 29 | let sigHash: string | undefined; 30 | if(eventContract.interface.hasEvent(name)) { 31 | const contractEvent = eventContract.interface.getEvent("X404Created") 32 | sigHash = contractEvent.topicHash 33 | } 34 | // Throw if the sigHash was not found 35 | if (!sigHash) { 36 | console.log( 37 | `Event "${name}" not found in provided contract (default: Events libary). \nAre you sure you're using the right contract?` 38 | ); 39 | } 40 | 41 | for (const emittedEvent of events) { 42 | // If we find one with the correct sighash, check if it is the one we're looking for 43 | if (emittedEvent.topics[0] == sigHash) { 44 | // If an emitter address is passed, validate that this is indeed the correct emitter, if not, continue 45 | if (emitterAddress) { 46 | if (emittedEvent.address != emitterAddress) continue; 47 | } 48 | const event = eventContract.interface.parseLog(emittedEvent); 49 | return event; 50 | } 51 | } 52 | // Throw if the event args were not expected or the event was not found in the logs 53 | console.log( 54 | `Event "${name}" not found emitted by "${emitterAddress}" in given transaction log` 55 | ); 56 | } else { 57 | console.log('No events were emitted'); 58 | } 59 | } 60 | 61 | export async function waitForTx( 62 | tx: Promise | TransactionResponse, 63 | skipCheck = false 64 | ): Promise { 65 | if (!skipCheck) await expect(tx).to.not.be.reverted; 66 | return (await (await tx).wait())!; 67 | } 68 | 69 | export async function getTimestamp(): Promise { 70 | const blockNumber = await hre.ethers.provider.send('eth_blockNumber', []); 71 | const block = await hre.ethers.provider.send('eth_getBlockByNumber', [blockNumber, false]); 72 | return block.timestamp; 73 | } 74 | 75 | export async function setNextBlockTimestamp(timestamp: number): Promise { 76 | await hre.ethers.provider.send('evm_setNextBlockTimestamp', [timestamp]); 77 | } 78 | 79 | let snapshotId: string = '0x1'; 80 | export async function takeSnapshot() { 81 | snapshotId = await hre.ethers.provider.send('evm_snapshot', []); 82 | } 83 | 84 | export async function revertToSnapshot() { 85 | await hre.ethers.provider.send('evm_revert', [snapshotId]); 86 | } -------------------------------------------------------------------------------- /contracts/interfaces/IERC404.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; 5 | 6 | interface IERC404 is IERC165 { 7 | error NotFound(); 8 | error InvalidTokenId(); 9 | error NoAvaiableTokenID(); 10 | error AlreadyExists(); 11 | error InvalidRecipient(); 12 | error InvalidSender(); 13 | error InvalidSpender(); 14 | error InvalidOperator(); 15 | error UnsafeRecipient(); 16 | error RecipientIsERC721TransferExempt(); 17 | error Unauthorized(); 18 | error InsufficientAllowance(); 19 | error DecimalsTooLow(); 20 | error PermitDeadlineExpired(); 21 | error InvalidSigner(); 22 | error InvalidApproval(); 23 | error OwnedIndexOverflow(); 24 | error MintLimitReached(); 25 | error InvalidExemption(); 26 | 27 | function name() external view returns (string memory); 28 | 29 | function symbol() external view returns (string memory); 30 | 31 | function decimals() external view returns (uint8); 32 | 33 | function totalSupply() external view returns (uint256); 34 | 35 | function erc20TotalSupply() external view returns (uint256); 36 | 37 | function erc721TotalSupply() external view returns (uint256); 38 | 39 | function balanceOf(address owner_) external view returns (uint256); 40 | 41 | function erc721BalanceOf(address owner_) external view returns (uint256); 42 | 43 | function erc20BalanceOf(address owner_) external view returns (uint256); 44 | 45 | function erc721TransferExempt( 46 | address account_ 47 | ) external view returns (bool); 48 | 49 | function isApprovedForAll( 50 | address owner_, 51 | address operator_ 52 | ) external view returns (bool); 53 | 54 | function allowance( 55 | address owner_, 56 | address spender_ 57 | ) external view returns (uint256); 58 | 59 | function owned(address owner_) external view returns (uint256[] memory); 60 | 61 | function ownerOf(uint256 id_) external view returns (address erc721Owner); 62 | 63 | function tokenURI(uint256 id_) external view returns (string memory); 64 | 65 | function approve( 66 | address spender_, 67 | uint256 valueOrId_ 68 | ) external returns (bool); 69 | 70 | function erc20Approve( 71 | address spender_, 72 | uint256 value_ 73 | ) external returns (bool); 74 | 75 | function erc721Approve(address spender_, uint256 id_) external; 76 | 77 | function setApprovalForAll(address operator_, bool approved_) external; 78 | 79 | function transferFrom( 80 | address from_, 81 | address to_, 82 | uint256 valueOrId_ 83 | ) external returns (bool); 84 | 85 | function erc20TransferFrom( 86 | address from_, 87 | address to_, 88 | uint256 value_ 89 | ) external returns (bool); 90 | 91 | function erc721TransferFrom( 92 | address from_, 93 | address to_, 94 | uint256 id_ 95 | ) external; 96 | 97 | function transfer(address to_, uint256 amount_) external returns (bool); 98 | 99 | function setSelfERC721TransferExempt(bool state_) external; 100 | 101 | function safeTransferFrom(address from_, address to_, uint256 id_) external; 102 | 103 | function safeTransferFrom( 104 | address from_, 105 | address to_, 106 | uint256 id_, 107 | bytes calldata data_ 108 | ) external; 109 | } 110 | -------------------------------------------------------------------------------- /test/__setup.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { expect } from 'chai'; 3 | import { Signer, ZeroAddress, AbiCoder } from 'ethers'; 4 | import { ethers, upgrades } from 'hardhat'; 5 | import { 6 | X404Hub__factory, 7 | X404Hub, 8 | Events, 9 | Events__factory, 10 | MockUniswapV2Router02__factory 11 | } from '../typechain-types'; 12 | import { 13 | revertToSnapshot, 14 | takeSnapshot 15 | } from './helpers/utils'; 16 | import { LibCaculatePair__factory } from '../typechain-types/factories/contracts/lib/LibCaculatePair__factory'; 17 | 18 | export let accounts: Signer[]; 19 | export let deployer: Signer; 20 | export let owner: Signer; 21 | export let user: Signer; 22 | export let userTwo: Signer; 23 | export let deployerAddress: string; 24 | export let ownerAddress: string; 25 | export let userAddress: string; 26 | export let userTwoAddress: string; 27 | export let x404Hub: X404Hub; 28 | export let eventsLib: Events; 29 | export let abiCoder = AbiCoder.defaultAbiCoder(); 30 | 31 | export const decimals = 18; 32 | export let yestoday = parseInt((new Date().getTime() / 1000 ).toFixed(0)) - 24 * 3600 33 | export let now = parseInt((new Date().getTime() / 1000 ).toFixed(0)) 34 | export let tomorrow2 = parseInt((new Date().getTime() / 1000 ).toFixed(0)) + 2 * 24 * 3600 35 | export let tomorrow = parseInt((new Date().getTime() / 1000 ).toFixed(0)) + 24 * 3600 36 | 37 | export function makeSuiteCleanRoom(name: string, tests: () => void) { 38 | describe(name, () => { 39 | beforeEach(async function () { 40 | await takeSnapshot(); 41 | }); 42 | tests(); 43 | afterEach(async function () { 44 | await revertToSnapshot(); 45 | }); 46 | }); 47 | } 48 | 49 | before(async function () { 50 | abiCoder = AbiCoder.defaultAbiCoder(); 51 | accounts = await ethers.getSigners(); 52 | deployer = accounts[0]; 53 | owner = accounts[3]; 54 | user = accounts[1]; 55 | userTwo = accounts[2]; 56 | 57 | deployerAddress = await deployer.getAddress(); 58 | userAddress = await user.getAddress(); 59 | userTwoAddress = await userTwo.getAddress(); 60 | ownerAddress = await owner.getAddress(); 61 | 62 | const factoryAddr = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" 63 | const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 64 | const routerContract = await new MockUniswapV2Router02__factory(deployer).deploy(factoryAddr, WETH); 65 | const routerAddr = await routerContract.getAddress(); 66 | 67 | const swapRouterArray = [ 68 | { 69 | bV2orV3: true, 70 | routerAddr: routerAddr, 71 | uniswapV3NonfungiblePositionManager: ZeroAddress, 72 | }, 73 | ]; 74 | 75 | const X404Hub = await ethers.getContractFactory("X404Hub"); 76 | const proxy = await upgrades.deployProxy(X404Hub, [ownerAddress, 24 * 60 * 60, swapRouterArray], { unsafeAllowLinkedLibraries: true }); 77 | const proxyAddress = await proxy.getAddress() 78 | console.log("proxy address: ", proxyAddress) 79 | console.log("admin address: ", await upgrades.erc1967.getAdminAddress(proxyAddress)) 80 | console.log("implement address: ", await upgrades.erc1967.getImplementationAddress(proxyAddress)) 81 | x404Hub = X404Hub__factory.connect(proxyAddress) 82 | await expect(x404Hub.connect(deployer).setBlueChipNftContract([ownerAddress], true)).to.be.reverted 83 | await expect(x404Hub.connect(deployer).setSwapRouter([])).to.be.reverted 84 | await expect(x404Hub.connect(deployer).setNewRedeemDeadline(10000)).to.be.reverted 85 | await expect(x404Hub.connect(deployer).setContractURI(ownerAddress, "as")).to.be.reverted 86 | 87 | 88 | eventsLib = await new Events__factory(deployer).deploy(); 89 | }); 90 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | import "@openzeppelin/hardhat-upgrades"; 4 | import * as dotenv from 'dotenv' 5 | import 'hardhat-deploy' 6 | dotenv.config() 7 | 8 | const deployer = process.env.DEPLOY_PRIVATE_KEY || '0x' + '11'.repeat(32) 9 | const owner = process.env.OWNER_PRIVATE_KEY || '0x' + '11'.repeat(32) 10 | const BASE_BLOCK_EXPLORER_KEY = process.env.BASE_BLOCK_EXPLORER_KEY || ''; 11 | const ETHEREUM_BLOCK_EXPLORER_KEY = process.env.ETHEREUM_BLOCK_EXPLORER_KEY || ''; 12 | const LINEA_BLOCK_EXPLORER_KEY = process.env.LINEA_BLOCK_EXPLORER_KEY || ''; 13 | const BNB_BLOCK_EXPLORER_KEY = process.env.BNB_BLOCK_EXPLORER_KEY || ''; 14 | 15 | const config: HardhatUserConfig = { 16 | solidity: { 17 | compilers: [ 18 | { 19 | version: '0.8.20', 20 | settings: { 21 | optimizer: { 22 | enabled: true, 23 | runs: 20, 24 | details: { 25 | yul: true, 26 | }, 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | networks: { 33 | hardhat: { 34 | gas: 29000000, 35 | }, 36 | bsc: { 37 | chainId: 56, 38 | url: process.env.BSC_MAINNET_RPC_URL || '', 39 | accounts: [deployer, owner], 40 | }, 41 | linea_mainnet: { 42 | chainId: 59144, 43 | url: process.env.LINEA_RPC_URL || '', 44 | accounts: [deployer, owner], 45 | }, 46 | linea_testnet: { 47 | chainId: 59140, 48 | url: process.env.LINEA_TESTNET_RPC_URL || '', 49 | accounts: [deployer, owner], 50 | }, 51 | baseMain: { 52 | chainId: 8453, 53 | url: process.env.BASE_MAIN_RPC_URL || '', 54 | accounts: [deployer, owner], 55 | }, 56 | baseSepolia: { 57 | chainId: 84532, 58 | url: process.env.BASE_TEST_RPC_URL || '', 59 | accounts: [deployer, owner], 60 | }, 61 | mainnet: { 62 | chainId: 1, 63 | url: process.env.ETH_MAINNET_RPC_URL || '', 64 | accounts: [deployer, owner], 65 | }, 66 | goerli: { 67 | chainId: 5, 68 | url: process.env.GOERLI_RPC_URL || '', 69 | accounts: [deployer, owner], 70 | }, 71 | sepolia: { 72 | chainId: 11155111, 73 | url: process.env.SEPOLIA_RPC_URL || '', 74 | accounts: [deployer, owner], 75 | }, 76 | bscTestnet: { 77 | chainId: 97, 78 | url: process.env.BSC_TESETNET_RPC_URL || '', 79 | accounts: [deployer, owner], 80 | }, 81 | }, 82 | gasReporter: { 83 | enabled: false, 84 | }, 85 | namedAccounts: { 86 | deployer: { 87 | default: 0, 88 | }, 89 | owner: { 90 | default: 1, 91 | } 92 | }, 93 | etherscan: { 94 | apiKey: { 95 | goerli: ETHEREUM_BLOCK_EXPLORER_KEY, 96 | baseSepolia: BASE_BLOCK_EXPLORER_KEY, 97 | baseMainnet: BASE_BLOCK_EXPLORER_KEY, 98 | linea_mainnet: LINEA_BLOCK_EXPLORER_KEY, 99 | linea_tesetnet: LINEA_BLOCK_EXPLORER_KEY, 100 | bscTestnet: BNB_BLOCK_EXPLORER_KEY, 101 | bsc: BNB_BLOCK_EXPLORER_KEY, 102 | mainnet: ETHEREUM_BLOCK_EXPLORER_KEY, 103 | sepolia: ETHEREUM_BLOCK_EXPLORER_KEY 104 | }, 105 | customChains: [ 106 | { 107 | network: "linea_mainnet", 108 | chainId: 59144, 109 | urls: { 110 | apiURL: "https://api.lineascan.build/api", 111 | browserURL: "https://lineascan.build/" 112 | } 113 | }, 114 | { 115 | network: "linea_tesetnet", 116 | chainId: 59140, 117 | urls: { 118 | apiURL: "https://api-goerli.lineascan.build/api", 119 | browserURL: "https://goerli.lineascan.build/" 120 | } 121 | }, 122 | { 123 | network: "baseMainnet", 124 | chainId: 8453, 125 | urls: { 126 | apiURL: "https://api.basescan.org/api", 127 | browserURL: "https://basescan.org" 128 | } 129 | }, 130 | { 131 | network: "baseSepolia", 132 | chainId: 84532, 133 | urls: { 134 | apiURL: "https://api-sepolia.basescan.org/api", 135 | browserURL: "https://sepolia.basescan.org/" 136 | } 137 | } 138 | ] 139 | } 140 | }; 141 | 142 | export default config; 143 | -------------------------------------------------------------------------------- /test/X404Hub/createX404.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | makeSuiteCleanRoom,deployer, x404Hub, owner, deployerAddress, user 4 | } from '../__setup.spec'; 5 | import { expect } from 'chai'; 6 | import { ERRORS } from '../helpers/errors'; 7 | import { findEvent, waitForTx } from '../helpers/utils'; 8 | 9 | import { 10 | BlueChipNFT__factory, X404__factory 11 | } from '../../typechain-types'; 12 | 13 | makeSuiteCleanRoom('create X404', function () { 14 | const ContractURI = "https://xrgb.xyz/contract" 15 | const TokenURI = "https://xrgb.xyz/metadata/" 16 | 17 | context('Generic', function () { 18 | let nft0Addr: string; 19 | let blueChipAddr: string; 20 | beforeEach(async function () { 21 | const nft0 = await new BlueChipNFT__factory(deployer).deploy(); 22 | nft0Addr = await nft0.getAddress(); 23 | const nft1 = await new BlueChipNFT__factory(deployer).deploy(); 24 | blueChipAddr = await nft1.getAddress(); 25 | }); 26 | 27 | context('Negatives', function () { 28 | it('User should fail to create if nft is not blurchip.', async function () { 29 | await expect(x404Hub.connect(owner).createX404(nft0Addr)).to.be.revertedWithCustomError(x404Hub, ERRORS.NotBlueChipNFT) 30 | }); 31 | it('User should fail to create if redeemMaxDeadline not initialized.', async function () { 32 | await expect(x404Hub.connect(owner).setBlueChipNftContract([blueChipAddr], true)).to.be.not.reverted 33 | await expect(x404Hub.connect(owner).setNewRedeemDeadline(0)).to.be.revertedWithCustomError(x404Hub, ERRORS.InvaildRedeemMaxDeadline) 34 | }); 35 | it('User should fail to create if emergce closed.', async function () { 36 | await expect(x404Hub.connect(owner).emergencyClose(true)).to.be.not.reverted 37 | await expect(x404Hub.connect(owner).createX404(blueChipAddr)).to.be.revertedWithCustomError(x404Hub, ERRORS.EmergencyClose) 38 | }); 39 | it('User should fail to create if created twice.', async function () { 40 | await expect(x404Hub.connect(owner).setBlueChipNftContract([blueChipAddr], true)).to.be.not.reverted 41 | await expect(x404Hub.connect(owner).createX404(blueChipAddr)).to.be.not.reverted 42 | await expect(x404Hub.connect(deployer).createX404(blueChipAddr)).to.be.reverted 43 | }); 44 | }) 45 | 46 | context('Scenarios', function () { 47 | it('User should correct varliable if created success.', async function () { 48 | await expect(x404Hub.connect(owner).setBlueChipNftContract([blueChipAddr], true)).to.be.not.reverted 49 | expect(await x404Hub.connect(owner)._blueChipNftContract(blueChipAddr)).to.equal(true) 50 | 51 | const receipt = await waitForTx(x404Hub.connect(deployer).createX404(blueChipAddr)) 52 | const event = findEvent(receipt, 'X404Created'); 53 | const x404Address = event!.args[0]; 54 | expect(await x404Hub.connect(deployer)._x404Contract(blueChipAddr)).to.equal(x404Address) 55 | }); 56 | 57 | it('Verify owner function after created success.', async function () { 58 | await expect(x404Hub.connect(owner).setBlueChipNftContract([blueChipAddr], true)).to.be.not.reverted 59 | expect(await x404Hub.connect(owner)._blueChipNftContract(blueChipAddr)).to.equal(true) 60 | 61 | const receipt = await waitForTx(x404Hub.connect(deployer).createX404(blueChipAddr)) 62 | const event = findEvent(receipt, 'X404Created'); 63 | const x404Address = event!.args[0]; 64 | expect(await x404Hub.connect(deployer)._x404Contract(blueChipAddr)).to.equal(x404Address) 65 | 66 | const x404 = X404__factory.connect(x404Address) 67 | expect(await x404Hub.connect(owner).setContractURI(blueChipAddr, ContractURI)).to.be.not.reverted 68 | expect(await x404.connect(owner).contractURI()).to.equal(ContractURI) 69 | 70 | expect(await x404Hub.connect(owner).setNewRedeemDeadline(7 * 24 * 3600)).to.be.not.reverted 71 | expect(await x404Hub.connect(owner)._redeemMaxDeadline()).to.equal(7 * 24 * 3600) 72 | }); 73 | }) 74 | }) 75 | }) -------------------------------------------------------------------------------- /contracts/X404Hub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 5 | import {X404HubStorage} from "./storage/X404HubStorage.sol"; 6 | import {DataTypes} from "./lib/DataTypes.sol"; 7 | import {Errors} from "./lib/Errors.sol"; 8 | import {Events} from "./lib/Events.sol"; 9 | import {X404} from "./X404.sol"; 10 | 11 | contract X404Hub is OwnableUpgradeable, X404HubStorage { 12 | modifier checkPermission(address nftContractAddress) { 13 | if (_bEmergencyClose) { 14 | revert Errors.EmergencyClose(); 15 | } 16 | if (!_bNoPermission) { 17 | if (!_blueChipNftContract[nftContractAddress]) { 18 | revert Errors.NotBlueChipNFT(); 19 | } 20 | } 21 | if (_redeemMaxDeadline == 0) { 22 | revert Errors.InvaildRedeemMaxDeadline(); 23 | } 24 | _; 25 | } 26 | 27 | /// @custom:oz-upgrades-unsafe-allow constructor 28 | constructor() { 29 | _disableInitializers(); 30 | } 31 | 32 | function initialize( 33 | address owner, 34 | uint256 maxRedeemDeadline, 35 | DataTypes.SwapRouter[] calldata swapRouterAddr 36 | ) public initializer { 37 | __Ownable_init(owner); 38 | if (maxRedeemDeadline == 0) { 39 | revert Errors.InvaildRedeemMaxDeadline(); 40 | } 41 | for (uint256 i = 0; i < swapRouterAddr.length; i++) { 42 | _swapRouterAddr.push(swapRouterAddr[i]); 43 | } 44 | _redeemMaxDeadline = maxRedeemDeadline; 45 | } 46 | 47 | function createX404( 48 | address nftContractAddress 49 | ) external checkPermission(nftContractAddress) returns (address x404) { 50 | _parameters = DataTypes.CreateX404Parameters({ 51 | nftContractAddr: nftContractAddress, 52 | creator: msg.sender, 53 | redeemMaxDeadline: _redeemMaxDeadline 54 | }); 55 | x404 = address( 56 | new X404{salt: keccak256(abi.encode(nftContractAddress))}() 57 | ); 58 | _x404Contract[nftContractAddress] = x404; 59 | delete _parameters; 60 | emit Events.X404Created(x404, nftContractAddress, msg.sender); 61 | } 62 | 63 | /* *****************Only Owner******************** 64 | */ 65 | function setContractURI( 66 | address nftContract, 67 | string calldata newContractUri 68 | ) public onlyOwner { 69 | if (_x404Contract[nftContract] == address(0)) { 70 | revert Errors.X404NotCreate(); 71 | } 72 | X404(_x404Contract[nftContract]).setContractURI(newContractUri); 73 | } 74 | 75 | function setRedeemFee( 76 | address nftContract, 77 | uint256 redeemFee 78 | ) public onlyOwner { 79 | if (_x404Contract[nftContract] == address(0)) { 80 | revert Errors.X404NotCreate(); 81 | } 82 | X404(_x404Contract[nftContract]).setRedeemFee(redeemFee); 83 | } 84 | 85 | function setTokenURI( 86 | address nftContract, 87 | string calldata newTokenURI 88 | ) public onlyOwner { 89 | if (_x404Contract[nftContract] == address(0)) { 90 | revert Errors.X404NotCreate(); 91 | } 92 | X404(_x404Contract[nftContract]).setTokenURI(newTokenURI); 93 | } 94 | 95 | function setNewRedeemDeadline(uint256 newDeadline) public onlyOwner { 96 | if (newDeadline == 0) { 97 | revert Errors.InvaildRedeemMaxDeadline(); 98 | } 99 | _redeemMaxDeadline = newDeadline; 100 | } 101 | 102 | function setSwapRouter( 103 | DataTypes.SwapRouter[] memory swapRouterAddr 104 | ) public onlyOwner { 105 | delete _swapRouterAddr; 106 | for (uint256 i = 0; i < swapRouterAddr.length; ) { 107 | _swapRouterAddr.push(swapRouterAddr[i]); 108 | unchecked { 109 | i++; 110 | } 111 | } 112 | } 113 | 114 | function emergencyClose(bool bClose) public onlyOwner { 115 | _bEmergencyClose = bClose; 116 | } 117 | 118 | function setCreateX404Permission(bool bPermission) public onlyOwner { 119 | _bNoPermission = bPermission; 120 | } 121 | 122 | function setBlueChipNftContract( 123 | address[] memory contractAddrs, 124 | bool state 125 | ) public onlyOwner { 126 | for (uint256 i = 0; i < contractAddrs.length; ) { 127 | if (contractAddrs[i] == address(0x0)) { 128 | revert Errors.CantBeZeroAddress(); 129 | } 130 | _blueChipNftContract[contractAddrs[i]] = state; 131 | unchecked { 132 | i++; 133 | } 134 | } 135 | } 136 | 137 | function getSwapRouter() 138 | public 139 | view 140 | returns (DataTypes.SwapRouter[] memory) 141 | { 142 | return _swapRouterAddr; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /contracts/lib/DoubleEndedQueue.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/DoubleEndedQueue.sol) 3 | // Modified by Pandora Labs to support native uint256 operations 4 | pragma solidity ^0.8.20; 5 | 6 | /** 7 | * @dev A sequence of items with the ability to efficiently push and pop items (i.e. insert and remove) on both ends of 8 | * the sequence (called front and back). Among other access patterns, it can be used to implement efficient LIFO and 9 | * FIFO queues. Storage use is optimized, and all operations are O(1) constant time. This includes {clear}, given that 10 | * the existing queue contents are left in storage. 11 | * 12 | * The struct is called `Uint256Deque`. This data structure can only be used in storage, and not in memory. 13 | * 14 | * ```solidity 15 | * DoubleEndedQueue.Uint256Deque queue; 16 | * ``` 17 | */ 18 | library DoubleEndedQueue { 19 | /** 20 | * @dev An operation (e.g. {front}) couldn't be completed due to the queue being empty. 21 | */ 22 | error QueueEmpty(); 23 | 24 | /** 25 | * @dev A push operation couldn't be completed due to the queue being full. 26 | */ 27 | error QueueFull(); 28 | 29 | /** 30 | * @dev An operation (e.g. {at}) couldn't be completed due to an index being out of bounds. 31 | */ 32 | error QueueOutOfBounds(); 33 | 34 | /** 35 | * @dev Indices are 128 bits so begin and end are packed in a single storage slot for efficient access. 36 | * 37 | * Struct members have an underscore prefix indicating that they are "private" and should not be read or written to 38 | * directly. Use the functions provided below instead. Modifying the struct manually may violate assumptions and 39 | * lead to unexpected behavior. 40 | * 41 | * The first item is at data[begin] and the last item is at data[end - 1]. This range can wrap around. 42 | */ 43 | struct Uint256Deque { 44 | uint128 _begin; 45 | uint128 _end; 46 | mapping(uint128 index => uint256) _data; 47 | } 48 | 49 | /** 50 | * @dev Inserts an item at the end of the queue. 51 | * 52 | * Reverts with {QueueFull} if the queue is full. 53 | */ 54 | function pushBack(Uint256Deque storage deque, uint256 value) internal { 55 | unchecked { 56 | uint128 backIndex = deque._end; 57 | if (backIndex + 1 == deque._begin) revert QueueFull(); 58 | deque._data[backIndex] = value; 59 | deque._end = backIndex + 1; 60 | } 61 | } 62 | 63 | /** 64 | * @dev Removes the item at the end of the queue and returns it. 65 | * 66 | * Reverts with {QueueEmpty} if the queue is empty. 67 | */ 68 | function popBack( 69 | Uint256Deque storage deque 70 | ) internal returns (uint256 value) { 71 | unchecked { 72 | uint128 backIndex = deque._end; 73 | if (backIndex == deque._begin) revert QueueEmpty(); 74 | --backIndex; 75 | value = deque._data[backIndex]; 76 | delete deque._data[backIndex]; 77 | deque._end = backIndex; 78 | } 79 | } 80 | 81 | /** 82 | * @dev Inserts an item at the beginning of the queue. 83 | * 84 | * Reverts with {QueueFull} if the queue is full. 85 | */ 86 | function pushFront(Uint256Deque storage deque, uint256 value) internal { 87 | unchecked { 88 | uint128 frontIndex = deque._begin - 1; 89 | if (frontIndex == deque._end) revert QueueFull(); 90 | deque._data[frontIndex] = value; 91 | deque._begin = frontIndex; 92 | } 93 | } 94 | 95 | /** 96 | * @dev Removes the item at the beginning of the queue and returns it. 97 | * 98 | * Reverts with `QueueEmpty` if the queue is empty. 99 | */ 100 | function popFront( 101 | Uint256Deque storage deque 102 | ) internal returns (uint256 value) { 103 | unchecked { 104 | uint128 frontIndex = deque._begin; 105 | if (frontIndex == deque._end) revert QueueEmpty(); 106 | value = deque._data[frontIndex]; 107 | delete deque._data[frontIndex]; 108 | deque._begin = frontIndex + 1; 109 | } 110 | } 111 | 112 | /** 113 | * @dev Returns the item at the beginning of the queue. 114 | * 115 | * Reverts with `QueueEmpty` if the queue is empty. 116 | */ 117 | function front( 118 | Uint256Deque storage deque 119 | ) internal view returns (uint256 value) { 120 | if (empty(deque)) revert QueueEmpty(); 121 | return deque._data[deque._begin]; 122 | } 123 | 124 | /** 125 | * @dev Returns the item at the end of the queue. 126 | * 127 | * Reverts with `QueueEmpty` if the queue is empty. 128 | */ 129 | function back( 130 | Uint256Deque storage deque 131 | ) internal view returns (uint256 value) { 132 | if (empty(deque)) revert QueueEmpty(); 133 | unchecked { 134 | return deque._data[deque._end - 1]; 135 | } 136 | } 137 | 138 | /** 139 | * @dev Return the item at a position in the queue given by `index`, with the first item at 0 and last item at 140 | * `length(deque) - 1`. 141 | * 142 | * Reverts with `QueueOutOfBounds` if the index is out of bounds. 143 | */ 144 | function at( 145 | Uint256Deque storage deque, 146 | uint256 index 147 | ) internal view returns (uint256 value) { 148 | if (index >= length(deque)) revert QueueOutOfBounds(); 149 | // By construction, length is a uint128, so the check above ensures that index can be safely downcast to uint128 150 | unchecked { 151 | return deque._data[deque._begin + uint128(index)]; 152 | } 153 | } 154 | 155 | /** 156 | * @dev Resets the queue back to being empty. 157 | * 158 | * NOTE: The current items are left behind in storage. This does not affect the functioning of the queue, but misses 159 | * out on potential gas refunds. 160 | */ 161 | function clear(Uint256Deque storage deque) internal { 162 | deque._begin = 0; 163 | deque._end = 0; 164 | } 165 | 166 | /** 167 | * @dev Returns the number of items in the queue. 168 | */ 169 | function length( 170 | Uint256Deque storage deque 171 | ) internal view returns (uint256) { 172 | unchecked { 173 | return uint256(deque._end - deque._begin); 174 | } 175 | } 176 | 177 | /** 178 | * @dev Returns true if the queue is empty. 179 | */ 180 | function empty(Uint256Deque storage deque) internal view returns (bool) { 181 | return deque._end == deque._begin; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /test/X404/depositNFT.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | makeSuiteCleanRoom,deployer, x404Hub, owner, deployerAddress, user, yestoday, tomorrow, tomorrow2, userAddress, abiCoder, userTwoAddress, ownerAddress 4 | } from '../__setup.spec'; 5 | import { expect } from 'chai'; 6 | import { ERRORS } from '../helpers/errors'; 7 | import { findEvent, waitForTx } from '../helpers/utils'; 8 | import { parseEther } from 'ethers'; 9 | 10 | import { 11 | BlueChipNFT, 12 | BlueChipNFT__factory, X404, X404__factory 13 | } from '../../typechain-types'; 14 | 15 | makeSuiteCleanRoom('depositNFT', function () { 16 | const ContractURI = "https://xrgb.xyz/contract" 17 | const TokenURI = "https://xrgb.xyz/metadata/" 18 | 19 | context('Generic', function () { 20 | let nft0Addr: string; 21 | let blueChipAddr: string; 22 | let x404: X404; 23 | let blueChipNft: BlueChipNFT; 24 | let normalNft: BlueChipNFT; 25 | let x404Addr: string; 26 | beforeEach(async function () { 27 | const nft0 = await new BlueChipNFT__factory(deployer).deploy(); 28 | nft0Addr = await nft0.getAddress(); 29 | const nft1 = await new BlueChipNFT__factory(deployer).deploy(); 30 | blueChipAddr = await nft1.getAddress(); 31 | 32 | await expect(x404Hub.connect(owner).setBlueChipNftContract([blueChipAddr], true)).to.be.not.reverted 33 | expect(await x404Hub.connect(owner)._blueChipNftContract(blueChipAddr)).to.equal(true) 34 | 35 | const receipt = await waitForTx(x404Hub.connect(deployer).createX404(blueChipAddr)) 36 | const event = findEvent(receipt, 'X404Created'); 37 | x404Addr = event!.args[0]; 38 | expect(await x404Hub.connect(deployer)._x404Contract(blueChipAddr)).to.equal(x404Addr) 39 | expect(await x404Hub.connect(owner).setContractURI(blueChipAddr, ContractURI)).to.be.not.reverted 40 | 41 | x404 = X404__factory.connect(x404Addr) 42 | expect(await x404.connect(owner).contractURI()).to.equal(ContractURI) 43 | 44 | expect(await x404Hub.connect(owner).setTokenURI(blueChipAddr, TokenURI)).to.be.not.reverted 45 | expect(await x404.connect(owner).baseURI()).to.equal(TokenURI) 46 | 47 | blueChipNft = BlueChipNFT__factory.connect(blueChipAddr) 48 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 49 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 50 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 51 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 52 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 53 | normalNft = BlueChipNFT__factory.connect(nft0Addr) 54 | expect(await normalNft.connect(user).mint()).to.be.not.reverted 55 | }); 56 | 57 | context('Negatives', function () { 58 | it('User should fail to deposit if deadline less than now.', async function () { 59 | await expect(x404.connect(user).depositNFT([0], yestoday)).to.be.revertedWithCustomError(x404, ERRORS.InvalidDeadLine) 60 | }); 61 | it('User should fail to deposit if deadline large than max deadlin.', async function () { 62 | await expect(x404.connect(user).depositNFT([0], tomorrow2)).to.be.revertedWithCustomError(x404, ERRORS.InvalidDeadLine) 63 | }); 64 | it('User should fail to deposit if not approve nft to contract.', async function () { 65 | await expect(x404.connect(user).depositNFT([0], tomorrow)).to.be.reverted 66 | }); 67 | it('User should fail to deposit if not correct nft contract use safeTransferFrom.', async function () { 68 | const abicode = abiCoder.encode(['uint256'], [tomorrow]); 69 | await expect(normalNft.connect(user)['safeTransferFrom(address,address,uint256,bytes)'](userAddress, x404Addr, 0, abicode)).to.be.revertedWithCustomError(x404, ERRORS.InvalidNFTAddress) 70 | }); 71 | it('User should fail to deposit if use error param when safeTransferFrom.', async function () { 72 | const abicode = abiCoder.encode(['address'], [userAddress]); 73 | await expect(blueChipNft.connect(user)['safeTransferFrom(address,address,uint256,bytes)'](userAddress, x404Addr, 0, abicode)).to.be.revertedWithCustomError(x404, ERRORS.InvalidDeadLine) 74 | }); 75 | }) 76 | 77 | context('Scenarios', function () { 78 | it('Get correct available if deposit nft success.', async function () { 79 | await blueChipNft.connect(user).setApprovalForAll(x404.getAddress(), true); 80 | await expect(x404.connect(user).depositNFT([0], tomorrow)).to.be.not.reverted 81 | expect(await blueChipNft.connect(user).ownerOf(0)).to.equal(await x404.getAddress()) 82 | 83 | const subInfo = await x404.connect(user).nftDepositInfo(0) 84 | expect(subInfo[0]).to.equal(userAddress) 85 | expect(subInfo[1]).to.equal(userAddress) 86 | expect(subInfo[2]).to.equal(tomorrow) 87 | expect(await x404.connect(user).checkTokenIdExsit(0)).to.equal(true) 88 | expect(await x404.connect(user).minted()).to.equal(1) 89 | 90 | await expect(x404.connect(user).depositNFT([2], tomorrow)).to.be.not.reverted 91 | expect(await x404.connect(user).checkTokenIdExsit(2)).to.equal(true) 92 | expect(await x404.connect(user).minted()).to.equal(2) 93 | }); 94 | it('Get correct available if user use safeTransferFrom.', async function () { 95 | const abicode = abiCoder.encode(['uint256'], [tomorrow]); 96 | await expect(blueChipNft.connect(user)['safeTransferFrom(address,address,uint256,bytes)'](userAddress, x404Addr, 0, abicode)).to.be.not.reverted 97 | expect(await blueChipNft.connect(user).ownerOf(0)).to.equal(await x404.getAddress()) 98 | 99 | const subInfo = await x404.connect(user).nftDepositInfo(0) 100 | expect(subInfo[0]).to.equal(userAddress) 101 | expect(subInfo[1]).to.equal(userAddress) 102 | expect(subInfo[2]).to.equal(tomorrow) 103 | expect(await x404.connect(user).checkTokenIdExsit(0)).to.equal(true) 104 | }); 105 | it('Get correct available if user transfer many times.', async function () { 106 | const x404Addr = await x404.getAddress() 107 | const abicode = abiCoder.encode(['uint256'], [tomorrow]); 108 | await expect(blueChipNft.connect(user)['safeTransferFrom(address,address,uint256,bytes)'](userAddress, x404Addr, 1, abicode)).to.be.not.reverted 109 | expect(await blueChipNft.connect(user).ownerOf(1)).to.equal(x404Addr) 110 | 111 | const subInfo = await x404.connect(user).nftDepositInfo(1) 112 | expect(subInfo[0]).to.equal(userAddress) 113 | expect(subInfo[1]).to.equal(userAddress) 114 | expect(subInfo[2]).to.equal(tomorrow) 115 | expect(await x404.connect(user).checkTokenIdExsit(1)).to.equal(true) 116 | expect(await x404.connect(user).minted()).to.equal(1) 117 | 118 | await expect(blueChipNft.connect(user).setApprovalForAll(x404.getAddress(), true)).to.be.not.reverted; 119 | await expect(x404.connect(user).depositNFT([0,2,3], tomorrow)).to.be.not.reverted 120 | expect(await blueChipNft.connect(user).balanceOf(x404Addr)).to.equal(4) 121 | expect(await x404.connect(user).erc721TotalSupply()).to.equal(4) 122 | 123 | await expect(x404.connect(user).transfer(userTwoAddress, parseEther("0.5"))).to.be.not.reverted 124 | await expect(x404.connect(user).transfer(ownerAddress, parseEther("0.6"))).to.be.not.reverted 125 | expect(await x404.connect(user).erc721TotalSupply()).to.equal(2) 126 | expect(await x404.connect(user).erc721BalanceOf(userAddress)).to.equal(2) 127 | const arr = await x404.connect(user).getERC721TokensInQueue(0,2) 128 | expect(arr[0]).to.equal(3) 129 | expect(arr[1]).to.equal(4) 130 | await expect(x404.connect(user).transfer(userTwoAddress, parseEther("0.5"))).to.be.not.reverted 131 | expect(await x404.connect(user).erc721TotalSupply()).to.equal(3) 132 | expect(await x404.connect(user).erc721BalanceOf(userAddress)).to.equal(2) 133 | expect(await x404.connect(user).erc721BalanceOf(userTwoAddress)).to.equal(1) 134 | expect(await x404.connect(user).ownerOf(4)).to.equal(userTwoAddress) 135 | }); 136 | }) 137 | }) 138 | }) -------------------------------------------------------------------------------- /test/X404/redeemNFT.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | makeSuiteCleanRoom,deployer, x404Hub, owner, deployerAddress, user, yestoday, tomorrow, tomorrow2, userAddress, abiCoder, userTwo, userTwoAddress, ownerAddress 4 | } from '../__setup.spec'; 5 | import { parseEther } from 'ethers'; 6 | import { expect } from 'chai'; 7 | import { ERRORS } from '../helpers/errors'; 8 | import { findEvent, waitForTx, getTimestamp, setNextBlockTimestamp } from '../helpers/utils'; 9 | 10 | import { 11 | BlueChipNFT, 12 | BlueChipNFT__factory, X404, X404__factory 13 | } from '../../typechain-types'; 14 | import { ethers } from 'hardhat'; 15 | 16 | makeSuiteCleanRoom('redeemNFT', function () { 17 | const ContractURI = "https://xrgb.xyz/contract" 18 | const TokenURI = "https://xrgb.xyz/metadata/" 19 | 20 | context('Generic', function () { 21 | let nft0Addr: string; 22 | let blueChipAddr: string; 23 | let x404: X404; 24 | let blueChipNft: BlueChipNFT; 25 | let normalNft: BlueChipNFT; 26 | let x404Addr: string; 27 | beforeEach(async function () { 28 | const nft0 = await new BlueChipNFT__factory(deployer).deploy(); 29 | nft0Addr = await nft0.getAddress(); 30 | const nft1 = await new BlueChipNFT__factory(deployer).deploy(); 31 | blueChipAddr = await nft1.getAddress(); 32 | 33 | await expect(x404Hub.connect(owner).setBlueChipNftContract([blueChipAddr], true)).to.be.not.reverted 34 | expect(await x404Hub.connect(owner)._blueChipNftContract(blueChipAddr)).to.equal(true) 35 | 36 | const receipt = await waitForTx(x404Hub.connect(deployer).createX404(blueChipAddr)) 37 | const event = findEvent(receipt, 'X404Created'); 38 | x404Addr = event!.args[0]; 39 | expect(await x404Hub.connect(deployer)._x404Contract(blueChipAddr)).to.equal(x404Addr) 40 | expect(await x404Hub.connect(owner).setContractURI(blueChipAddr, ContractURI)).to.be.not.reverted 41 | 42 | x404 = X404__factory.connect(x404Addr) 43 | expect(await x404.connect(owner).contractURI()).to.equal(ContractURI) 44 | 45 | blueChipNft = BlueChipNFT__factory.connect(blueChipAddr) 46 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 47 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 48 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 49 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 50 | expect(await blueChipNft.connect(user).mint()).to.be.not.reverted 51 | expect(await blueChipNft.connect(userTwo).mint()).to.be.not.reverted 52 | expect(await blueChipNft.connect(userTwo).mint()).to.be.not.reverted 53 | expect(await blueChipNft.connect(userTwo).mint()).to.be.not.reverted 54 | normalNft = BlueChipNFT__factory.connect(nft0Addr) 55 | expect(await normalNft.connect(user).mint()).to.be.not.reverted 56 | 57 | const abicode = abiCoder.encode(['uint256'], [tomorrow]); 58 | await expect(blueChipNft.connect(user)['safeTransferFrom(address,address,uint256,bytes)'](userAddress, x404Addr, 0, abicode)).to.be.not.reverted 59 | expect(await blueChipNft.connect(user).ownerOf(0)).to.equal(await x404.getAddress()) 60 | expect(await x404.connect(user).balanceOf(userAddress)).to.equal(parseEther("1")) 61 | expect(await x404.connect(user).erc721BalanceOf(userAddress)).to.equal(1) 62 | 63 | await blueChipNft.connect(user).setApprovalForAll(x404.getAddress(), true); 64 | await expect(x404.connect(user).depositNFT([3], tomorrow)).to.be.not.reverted 65 | expect(await blueChipNft.connect(user).ownerOf(3)).to.equal(await x404.getAddress()) 66 | expect(await x404.connect(user).balanceOf(userAddress)).to.equal(parseEther("2")) 67 | expect(await x404.connect(user).erc721BalanceOf(userAddress)).to.equal(2) 68 | expect(await x404.connect(user).ownerOf(1)).to.equal(userAddress) 69 | expect(await x404.connect(user).ownerOf(2)).to.equal(userAddress) 70 | 71 | const subInfo = await x404.connect(user).nftDepositInfo(0) 72 | expect(subInfo[0]).to.equal(userAddress) 73 | expect(subInfo[1]).to.equal(userAddress) 74 | expect(subInfo[2]).to.equal(tomorrow) 75 | expect(await x404.connect(user).checkTokenIdExsit(0)).to.equal(true) 76 | }); 77 | 78 | context('Negatives', function () { 79 | it('User should fail to deposit if noe own this nft.', async function () { 80 | const abicode = abiCoder.encode(['uint256'], [tomorrow]); 81 | await expect(blueChipNft.connect(user)['safeTransferFrom(address,address,uint256,bytes)'](userAddress, x404Addr, 0, abicode)).to.be.reverted 82 | }); 83 | it('User should fail to redeem if tokenid length = 0.', async function () { 84 | await expect(x404.connect(user).redeemNFT([])).to.be.revertedWithCustomError(x404, ERRORS.InvalidLength) 85 | }); 86 | it('User should fail to redeem if erc20 token not enough.', async function () { 87 | await expect(x404.connect(userTwo).redeemNFT([0])).to.be.reverted 88 | }); 89 | it('User should fail to redeem if erc20 token not enough.', async function () { 90 | const abicode = abiCoder.encode(['uint256'], [tomorrow]); 91 | await expect(blueChipNft.connect(userTwo)['safeTransferFrom(address,address,uint256,bytes)'](userTwoAddress, x404Addr, 5, abicode)).to.be.not.reverted 92 | expect(await x404.connect(user).balanceOf(userTwoAddress)).to.equal(parseEther("1")) 93 | expect(await x404.connect(user).erc721BalanceOf(userTwoAddress)).to.equal(1) 94 | 95 | await expect(x404.connect(userTwo).redeemNFT([0])).to.be.revertedWithCustomError(x404, ERRORS.NFTCannotRedeem) 96 | }); 97 | }) 98 | 99 | context('Scenarios', function () { 100 | it('Redeem success if you are the owner.', async function () { 101 | await expect(x404.connect(user).redeemNFT([0])).to.be.not.reverted 102 | expect(await x404.connect(user).balanceOf(userAddress)).to.equal(parseEther("1")) 103 | expect(await x404.connect(user).erc721BalanceOf(userAddress)).to.equal(1) 104 | expect(await blueChipNft.connect(user).ownerOf(0)).to.equal(userAddress) 105 | }); 106 | it('Redeem success if you are the owner.', async function () { 107 | const abicode = abiCoder.encode(['uint256'], [tomorrow]); 108 | await expect(blueChipNft.connect(userTwo)['safeTransferFrom(address,address,uint256,bytes)'](userTwoAddress, x404Addr, 5, abicode)).to.be.not.reverted 109 | await expect(blueChipNft.connect(userTwo)['safeTransferFrom(address,address,uint256,bytes)'](userTwoAddress, x404Addr, 6, abicode)).to.be.not.reverted 110 | expect(await x404.connect(user).balanceOf(userTwoAddress)).to.equal(parseEther("2")) 111 | expect(await x404.connect(user).erc721BalanceOf(userTwoAddress)).to.equal(2) 112 | expect(await x404.connect(user).ownerOf(3)).to.equal(userTwoAddress) 113 | expect(await x404.connect(user).ownerOf(4)).to.equal(userTwoAddress) 114 | expect(await x404.connect(user).checkTokenIdExsit(6)).to.equal(true) 115 | expect(await x404.connect(user).checkTokenIdExsit(5)).to.equal(true) 116 | expect(await x404.connect(user).checkTokenIdExsit(0)).to.equal(true) 117 | expect(await x404.connect(user).checkTokenIdExsit(3)).to.equal(true) 118 | expect(await x404.connect(user).erc721TotalSupply()).to.equal(4) 119 | 120 | const currentTimestamp = await getTimestamp(); 121 | await setNextBlockTimestamp(Number(currentTimestamp) + 48 * 60 * 60); 122 | 123 | expect(await x404Hub.connect(owner).setRedeemFee(blueChipAddr, parseEther("0.1"))).to.be.not.reverted 124 | const before = await ethers.provider.getBalance(ownerAddress); 125 | await expect(x404.connect(userTwo).redeemNFT([0,3], {value: parseEther("0.15")})).to.be.reverted 126 | await expect(x404.connect(userTwo).redeemNFT([3,5], {value: parseEther("0.3")})).to.be.not.reverted 127 | const after = await ethers.provider.getBalance(ownerAddress); 128 | expect(after - before).to.eq(parseEther("0.2")) 129 | 130 | expect(await x404.connect(user).balanceOf(userTwoAddress)).to.equal(0) 131 | expect(await x404.connect(user).erc721BalanceOf(userTwoAddress)).to.equal(0) 132 | await expect(x404.connect(user).ownerOf(4)).to.be.reverted 133 | await expect(x404.connect(user).ownerOf(3)).to.be.reverted 134 | expect(await blueChipNft.connect(user).ownerOf(3)).to.equal(userTwoAddress) 135 | expect(await blueChipNft.connect(user).ownerOf(5)).to.equal(userTwoAddress) 136 | 137 | expect(await x404.connect(user).erc721TotalSupply()).to.equal(2) 138 | const arr = await x404.connect(user).getERC721TokensInQueue(0,2) 139 | expect(arr[0]).to.equal(3) 140 | expect(arr[1]).to.equal(4) 141 | }); 142 | }) 143 | }) 144 | }) -------------------------------------------------------------------------------- /contracts/X404.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {ERC404} from "./ERC404.sol"; 5 | import {IX404Hub} from "./interfaces/IX404Hub.sol"; 6 | import {IPeripheryImmutableState} from "./interfaces/IPeripheryImmutableState.sol"; 7 | import {IUniswapV2Router} from "./interfaces/IUniswapV2Router.sol"; 8 | import {DataTypes} from "./lib/DataTypes.sol"; 9 | import {Errors} from "./lib/Errors.sol"; 10 | import {Events} from "./lib/Events.sol"; 11 | import {LibCalculatePair} from "./lib/LibCalculatePair.sol"; 12 | import {X404Storage} from "./storage/X404Storage.sol"; 13 | import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; 14 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 15 | import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; 16 | import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 17 | import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 18 | 19 | contract X404 is IERC721Receiver, ERC404, Ownable, X404Storage { 20 | using EnumerableSet for EnumerableSet.UintSet; 21 | 22 | address public immutable creator; 23 | address public immutable blueChipNftAddr; 24 | address public immutable x404Hub; 25 | 26 | modifier onlyX404Hub() { 27 | if (msg.sender != x404Hub) { 28 | revert Errors.OnlyCallByFactory(); 29 | } 30 | _; 31 | } 32 | 33 | constructor() Ownable(msg.sender) { 34 | decimals = 18; 35 | (blueChipNftAddr, creator, maxRedeemDeadline) = IX404Hub(msg.sender) 36 | ._parameters(); 37 | 38 | units = 10 ** 18; 39 | address newOwner = IX404Hub(msg.sender).owner(); 40 | string memory oriName = IERC721Metadata(blueChipNftAddr).name(); 41 | string memory oriSymbol = IERC721Metadata(blueChipNftAddr).symbol(); 42 | name = string.concat("X404-", oriName); 43 | symbol = string.concat("X404-", oriSymbol); 44 | DataTypes.SwapRouter[] memory swapRouterStruct = IX404Hub(msg.sender) 45 | .getSwapRouter(); 46 | _setRouterTransferExempt(swapRouterStruct); 47 | _setERC721TransferExempt(address(this), true); 48 | x404Hub = msg.sender; 49 | _transferOwnership(newOwner); 50 | } 51 | 52 | /// @notice redeem nfts from contract when user hold n * units erc20 token 53 | /// @param tokenIds The array tokenid of deposit nft. 54 | /// @param redeemDeadline The redeemDeadline. means before deadline, Only you can redeem your nft. after deadline, anyone who hold more than units erc20 token can redeem your nft. 55 | function depositNFT( 56 | uint256[] memory tokenIds, 57 | uint256 redeemDeadline 58 | ) external { 59 | if ( 60 | redeemDeadline < block.timestamp || 61 | redeemDeadline > block.timestamp + maxRedeemDeadline 62 | ) { 63 | revert Errors.InvalidDeadLine(); 64 | } 65 | uint256 len = tokenIds.length; 66 | if (len == 0) { 67 | revert Errors.InvalidLength(); 68 | } 69 | for (uint256 i = 0; i < len; ) { 70 | IERC721Metadata(blueChipNftAddr).transferFrom( 71 | msg.sender, 72 | address(this), 73 | tokenIds[i] 74 | ); 75 | if (tokenIdSet.add(tokenIds[i])) { 76 | NFTDepositInfo storage subInfo = nftDepositInfo[tokenIds[i]]; 77 | subInfo.caller = msg.sender; 78 | subInfo.oriOwner = msg.sender; 79 | subInfo.redeemDeadline = redeemDeadline; 80 | } else { 81 | revert InvalidTokenId(); 82 | } 83 | emit Events.X404DepositNFT( 84 | msg.sender, 85 | msg.sender, 86 | tokenIds[i], 87 | redeemDeadline 88 | ); 89 | unchecked { 90 | i++; 91 | } 92 | } 93 | _transferERC20WithERC721(address(0x0), msg.sender, len * units); 94 | } 95 | 96 | /// @notice redeem nfts from contract when user hold n * units erc20 token 97 | /// @param tokenIds The array tokenid of redeem nft. 98 | function redeemNFT(uint256[] memory tokenIds) external payable { 99 | uint256 len = tokenIds.length; 100 | if (len == 0) { 101 | revert Errors.InvalidLength(); 102 | } 103 | if (redeemFee > 0) { 104 | //revert if msg.value < redeemFee 105 | uint256 totalRedeemFee = len * redeemFee; 106 | if (msg.value < totalRedeemFee) { 107 | revert Errors.MsgValueNotEnough(); 108 | } 109 | //send redeemFee 110 | (bool sucess, ) = payable(owner()).call{value: totalRedeemFee}(""); 111 | if (!sucess) { 112 | revert Errors.SendETHFailed(); 113 | } 114 | //refund if msg.value > redeemFee 115 | if (msg.value > totalRedeemFee) { 116 | (bool sucess1, ) = payable(msg.sender).call{ 117 | value: msg.value - totalRedeemFee 118 | }(""); 119 | if (!sucess1) { 120 | revert Errors.SendETHFailed(); 121 | } 122 | } 123 | } else { 124 | //refund if not charge redeemFee 125 | if (msg.value > 0) { 126 | (bool sucess2, ) = payable(msg.sender).call{value: msg.value}( 127 | "" 128 | ); 129 | if (!sucess2) { 130 | revert Errors.SendETHFailed(); 131 | } 132 | } 133 | } 134 | 135 | _transferERC20WithERC721(msg.sender, address(0), units * len); 136 | 137 | for (uint256 i = 0; i < tokenIds.length; ) { 138 | address oriOwner = nftDepositInfo[tokenIds[i]].oriOwner; 139 | if ( 140 | oriOwner != msg.sender && 141 | nftDepositInfo[tokenIds[i]].redeemDeadline > block.timestamp 142 | ) { 143 | revert Errors.NFTCannotRedeem(); 144 | } 145 | if (!tokenIdSet.remove(tokenIds[i])) { 146 | revert Errors.RemoveFailed(); 147 | } 148 | IERC721Metadata(blueChipNftAddr).safeTransferFrom( 149 | address(this), 150 | msg.sender, 151 | tokenIds[i] 152 | ); 153 | emit Events.X404RedeemNFT(msg.sender, oriOwner, tokenIds[i]); 154 | delete nftDepositInfo[tokenIds[i]]; 155 | unchecked { 156 | i++; 157 | } 158 | } 159 | } 160 | 161 | /// @notice when user send nft to this contract by "safeTransferFrom" 162 | /// @param caller caller who call function "safeTransferFrom". 163 | /// @param from The Nft owner 164 | /// @param tokenId The nft tokenid 165 | /// @param data The redeem deadline 166 | function onERC721Received( 167 | address caller, 168 | address from, 169 | uint256 tokenId, 170 | bytes calldata data 171 | ) external returns (bytes4) { 172 | if (msg.sender != blueChipNftAddr) { 173 | revert Errors.InvalidNFTAddress(); 174 | } 175 | uint256 redeemDeadline = abi.decode(data, (uint256)); 176 | if ( 177 | redeemDeadline < block.timestamp || 178 | redeemDeadline > block.timestamp + maxRedeemDeadline 179 | ) { 180 | revert Errors.InvalidDeadLine(); 181 | } 182 | _transferERC20WithERC721(address(0), caller, units); 183 | if (tokenIdSet.add(tokenId)) { 184 | NFTDepositInfo storage subInfo = nftDepositInfo[tokenId]; 185 | subInfo.caller = caller; 186 | subInfo.oriOwner = from; 187 | subInfo.redeemDeadline = redeemDeadline; 188 | } else { 189 | revert InvalidTokenId(); 190 | } 191 | emit Events.X404DepositNFT(caller, from, tokenId, redeemDeadline); 192 | 193 | return IERC721Receiver.onERC721Received.selector; 194 | } 195 | 196 | function getTokenIdSet() external view returns (uint256[] memory) { 197 | return tokenIdSet.values(); 198 | } 199 | 200 | function checkTokenIdExsit(uint256 tokenId) external view returns (bool) { 201 | return tokenIdSet.contains(tokenId); 202 | } 203 | 204 | /**************Only Call By Factory Function **********/ 205 | 206 | function setContractURI( 207 | string calldata newContractUri 208 | ) external onlyX404Hub returns (bool) { 209 | if (bytes(newContractUri).length == 0) { 210 | revert Errors.InvalidLength(); 211 | } 212 | contractURI = newContractUri; 213 | emit Events.SetContractURI(contractURI); 214 | return true; 215 | } 216 | 217 | function setRedeemFee( 218 | uint256 newRedeemFee 219 | ) external onlyX404Hub returns (bool) { 220 | if (newRedeemFee > 0.2 ether) { 221 | revert Errors.RedeemFeeTooHigh(); 222 | } 223 | redeemFee = newRedeemFee; 224 | emit Events.SetRedeemFee(redeemFee); 225 | return true; 226 | } 227 | 228 | function setTokenURI(string calldata _tokenURI) external onlyX404Hub { 229 | if (bytes(_tokenURI).length == 0) { 230 | revert Errors.InvalidLength(); 231 | } 232 | baseURI = _tokenURI; 233 | emit Events.SetTokenURI(baseURI); 234 | } 235 | 236 | function tokenURI(uint256 id) public view override returns (string memory) { 237 | address erc721Owner = _getOwnerOf(id); 238 | if (erc721Owner == address(0x0)) { 239 | revert NotFound(); 240 | } 241 | return string.concat(baseURI, Strings.toString(id)); 242 | } 243 | 244 | /**************Internal Function **********/ 245 | function _setRouterTransferExempt( 246 | DataTypes.SwapRouter[] memory swapRouterStruct 247 | ) private { 248 | address thisAddress = address(this); 249 | for (uint i = 0; i < swapRouterStruct.length; ) { 250 | address routerAddr = swapRouterStruct[i].routerAddr; 251 | if (routerAddr == address(0)) { 252 | revert Errors.CantBeZeroAddress(); 253 | } 254 | _setERC721TransferExempt(routerAddr, true); 255 | 256 | if (swapRouterStruct[i].bV2orV3) { 257 | address weth_ = IUniswapV2Router(routerAddr).WETH(); 258 | address swapFactory = IUniswapV2Router(routerAddr).factory(); 259 | address pair = LibCalculatePair._getUniswapV2Pair( 260 | swapFactory, 261 | thisAddress, 262 | weth_ 263 | ); 264 | _setERC721TransferExempt(pair, true); 265 | } else { 266 | address weth_ = IPeripheryImmutableState(routerAddr).WETH9(); 267 | address swapFactory = IPeripheryImmutableState(routerAddr) 268 | .factory(); 269 | address v3NonfungiblePositionManager = swapRouterStruct[i] 270 | .uniswapV3NonfungiblePositionManager; 271 | if (v3NonfungiblePositionManager == address(0)) { 272 | revert Errors.CantBeZeroAddress(); 273 | } 274 | if ( 275 | IPeripheryImmutableState(v3NonfungiblePositionManager) 276 | .factory() != 277 | swapFactory || 278 | IPeripheryImmutableState(v3NonfungiblePositionManager) 279 | .WETH9() != 280 | weth_ 281 | ) { 282 | revert Errors.X404SwapV3FactoryMismatch(); 283 | } 284 | _setERC721TransferExempt(v3NonfungiblePositionManager, true); 285 | _setV3SwapTransferExempt(swapFactory, thisAddress, weth_); 286 | } 287 | unchecked { 288 | ++i; 289 | } 290 | } 291 | } 292 | 293 | function _setV3SwapTransferExempt( 294 | address swapFactory, 295 | address tokenA, 296 | address tokenB 297 | ) private { 298 | uint24[4] memory feeTiers = [ 299 | uint24(100), 300 | uint24(500), 301 | uint24(3_000), 302 | uint24(10_000) 303 | ]; 304 | 305 | for (uint256 i = 0; i < feeTiers.length; ) { 306 | address v3PairAddr = LibCalculatePair._getUniswapV3Pair( 307 | swapFactory, 308 | tokenA, 309 | tokenB, 310 | feeTiers[i] 311 | ); 312 | // Set the v3 pair as exempt. 313 | _setERC721TransferExempt(v3PairAddr, true); 314 | unchecked { 315 | ++i; 316 | } 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /scripts/deploy-utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers, Contract, ContractTransaction } from 'ethers' 2 | import { Provider } from '@ethersproject/abstract-provider' 3 | import { Signer } from '@ethersproject/abstract-signer' 4 | import { splitSignature } from '@ethersproject/bytes' 5 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 6 | import hre from 'hardhat'; 7 | 8 | async function delay(ms: number) { 9 | return new Promise((resolve) => setTimeout(resolve, ms)); 10 | } 11 | 12 | export async function deployContract(tx: any): Promise { 13 | const result = await tx; 14 | await result.deployTransaction.wait(); 15 | return result; 16 | } 17 | 18 | export async function deployWithVerify( 19 | tx: any, 20 | args: any, 21 | contractPath: string 22 | ): Promise { 23 | const deployedContract = await deployContract(tx); 24 | let count = 0; 25 | let maxTries = 8; 26 | const runtimeHRE = require('hardhat'); 27 | while (true) { 28 | await delay(10000); 29 | try { 30 | console.log('Verifying contract at', deployedContract.address); 31 | await runtimeHRE.run('verify:verify', { 32 | address: deployedContract.address, 33 | constructorArguments: args, 34 | contract: contractPath, 35 | }); 36 | break; 37 | } catch (error) { 38 | if (String(error).includes('Already Verified')) { 39 | console.log( 40 | `Already verified contract at ${contractPath} at address ${deployedContract.address}` 41 | ); 42 | break; 43 | } 44 | if (++count == maxTries) { 45 | console.log( 46 | `Failed to verify contract at ${contractPath} at address ${deployedContract.address}, error: ${error}` 47 | ); 48 | break; 49 | } 50 | console.log(`Retrying... Retry #${count}, last error: ${error}`); 51 | } 52 | } 53 | 54 | return deployedContract; 55 | } 56 | 57 | /** 58 | * @param {Any} hre Hardhat runtime environment 59 | * @param {String} name Contract name from the names object 60 | * @param {Any[]} args Constructor arguments 61 | * @param {String} contract Name of the solidity contract 62 | * @param {String} iface Alternative interface for calling the contract 63 | * @param {Function} postDeployAction Called after deployment 64 | */ 65 | 66 | export const deployAndVerifyAndThen = async ({ 67 | hre, 68 | name, 69 | args, 70 | contract, 71 | iface, 72 | postDeployAction, 73 | }: { 74 | hre: any 75 | name: string 76 | args: any[] 77 | contract?: string 78 | iface?: string 79 | postDeployAction?: (contract: Contract) => Promise 80 | }) => { 81 | const { deploy } = hre.deployments 82 | const { deployer } = await hre.getNamedAccounts() 83 | 84 | const result = await deploy(name, { 85 | contract, 86 | from: deployer, 87 | args, 88 | log: true, 89 | }) 90 | 91 | if (result.newlyDeployed) { 92 | if (!(await isHardhatNode(hre))) { 93 | // Verification sometimes fails, even when the contract is correctly deployed and eventually 94 | // verified. Possibly due to a race condition. We don't want to halt the whole deployment 95 | // process just because that happens. 96 | let count = 0; 97 | let maxTries = 8; 98 | while (true) { 99 | await delay(2000); 100 | try { 101 | console.log('Verifying contract at', result.address); 102 | await hre.run('verify:verify', { 103 | address: result.address, 104 | constructorArguments: args, 105 | }); 106 | break; 107 | } catch (error) { 108 | if (String(error).includes('Already Verified')) { 109 | console.log( 110 | `Already verified contract at address ${result.address}` 111 | ); 112 | break; 113 | } 114 | if (++count == maxTries) { 115 | console.log( 116 | `Failed to verify contract at address ${result.address}, error: ${error}` 117 | ); 118 | break; 119 | } 120 | console.log(`Retrying... Retry #${count}, last error: ${error}`); 121 | } 122 | } 123 | } 124 | if (postDeployAction) { 125 | const signer = hre.ethers.provider.getSigner(deployer) 126 | let abi = result.abi 127 | if (iface !== undefined) { 128 | const factory = await hre.ethers.getContractFactory(iface) 129 | abi = factory.interface 130 | } 131 | await postDeployAction( 132 | getAdvancedContract({ 133 | hre, 134 | contract: new Contract(result.address, abi, signer), 135 | }) 136 | ) 137 | } 138 | } 139 | } 140 | 141 | // Returns a version of the contract object which modifies all of the input contract's methods to: 142 | // 1. Waits for a confirmed receipt with more than deployConfig.numDeployConfirmations confirmations. 143 | // 2. Include simple resubmission logic, ONLY for Kovan, which appears to drop transactions. 144 | export const getAdvancedContract = (opts: { 145 | hre: any 146 | contract: Contract 147 | }): Contract => { 148 | // Temporarily override Object.defineProperty to bypass ether's object protection. 149 | const def = Object.defineProperty 150 | Object.defineProperty = (obj, propName, prop) => { 151 | prop.writable = true 152 | return def(obj, propName, prop) 153 | } 154 | 155 | const contract = new Contract( 156 | opts.contract.target, 157 | opts.contract.interface, 158 | opts.contract.runner 159 | ) 160 | 161 | // Now reset Object.defineProperty 162 | Object.defineProperty = def 163 | 164 | // Override each function call to also `.wait()` so as to simplify the deploy scripts' syntax. 165 | // for (const fnName of Object.keys(contract.functions)) { 166 | // const fn = contract[fnName].bind(contract) 167 | // ;(contract as any)[fnName] = async (...args: any) => { 168 | // // We want to use the gas price that has been configured at the beginning of the deployment. 169 | // // However, if the function being triggered is a "constant" (static) function, then we don't 170 | // // want to provide a gas price because we're prone to getting insufficient balance errors. 171 | // let gasPrice = undefined //opts.hre.deployConfig.gasPrice || undefined 172 | // // if (contract.interface.getFunction(fnName).constant) { 173 | // // gasPrice = 0 174 | // // } 175 | 176 | // const tx = await fn(...args, { 177 | // gasPrice, 178 | // }) 179 | 180 | // if (typeof tx !== 'object' || typeof tx.wait !== 'function') { 181 | // return tx 182 | // } 183 | 184 | // // Special logic for: 185 | // // (1) handling confirmations 186 | // // (2) handling an issue on Kovan specifically where transactions get dropped for no 187 | // // apparent reason. 188 | // const maxTimeout = 120 189 | // let timeout = 0 190 | // while (true) { 191 | // //await sleep(1000) 192 | // const receipt = await contract.provider.getTransactionReceipt(tx.hash) 193 | // if (receipt === null) { 194 | // timeout++ 195 | // if (timeout > maxTimeout && opts.hre.network.name === 'kovan') { 196 | // // Special resubmission logic ONLY required on Kovan. 197 | // console.log( 198 | // `WARNING: Exceeded max timeout on transaction. Attempting to submit transaction again...` 199 | // ) 200 | // return contract[fnName](...args) 201 | // } 202 | // } else if ( 203 | // receipt.confirmations >= 0 204 | // ) { 205 | // return tx 206 | // } 207 | // } 208 | // } 209 | // } 210 | 211 | return contract 212 | } 213 | 214 | export const getContractFromArtifact = async ( 215 | hre: any, 216 | name: string, 217 | options: { 218 | iface?: string 219 | signerOrProvider?: Signer | Provider | string 220 | } = {} 221 | ): Promise => { 222 | const artifact = await hre.deployments.get(name) 223 | await hre.ethers.provider.waitForTransaction(artifact.receipt.transactionHash) 224 | 225 | // Get the deployed contract's interface. 226 | let iface = new hre.ethers.Interface(artifact.abi) 227 | // Override with optional iface name if requested. 228 | if (options.iface) { 229 | const factory = await hre.ethers.getContractFactory(options.iface) 230 | iface = factory.interface 231 | } 232 | 233 | let signerOrProvider: Signer | Provider = hre.ethers.provider 234 | if (options.signerOrProvider) { 235 | if (typeof options.signerOrProvider === 'string') { 236 | signerOrProvider = hre.ethers.provider.getSigner(options.signerOrProvider) 237 | } else { 238 | signerOrProvider = options.signerOrProvider 239 | } 240 | } 241 | 242 | return getAdvancedContract({ 243 | hre, 244 | contract: new hre.ethers.Contract( 245 | artifact.address, 246 | iface, 247 | signerOrProvider 248 | ), 249 | }) 250 | } 251 | 252 | export const isHardhatNode = async (hre: HardhatRuntimeEnvironment) => { 253 | const chainId = hre.network.config.chainId; 254 | return chainId === 31337 255 | } 256 | 257 | export const getTomoImplAddr = async (hre: HardhatRuntimeEnvironment) => { 258 | const chainId = hre.network.config.chainId; 259 | if(chainId === 59140){ 260 | return process.env.TOMO_ADDRESS_LINEA_TESTNET || '' 261 | }else if(chainId === 59144){ 262 | return process.env.TOMO_ADDRESS_LINEA_MAINNET || '' 263 | } 264 | return '' 265 | } 266 | 267 | export async function waitForTx(tx: Promise) { 268 | (await tx); 269 | } 270 | 271 | export function getChainId(): number { 272 | return hre.network.config.chainId || 31337; 273 | } 274 | 275 | export async function buildTransferSeparator( 276 | tomo: string, 277 | name: string, 278 | subject: string, 279 | from: string, 280 | to: string, 281 | amount: number 282 | ): Promise<{ v: number; r: string; s: string }> { 283 | const msgParams = buildTransferKeyParams(tomo, name, subject, from, to, amount); 284 | return await getSig(msgParams); 285 | } 286 | 287 | const buildTransferKeyParams = ( 288 | tomo: string, 289 | name: string, 290 | subject: string, 291 | from: string, 292 | to: string, 293 | amount: number 294 | ) => ({ 295 | types: { 296 | TransferKey: [ 297 | { name: 'subject', type: 'address' }, 298 | { name: 'from', type: 'address' }, 299 | { name: 'to', type: 'address' }, 300 | { name: 'amount', type: 'uint256' }, 301 | ], 302 | }, 303 | domain: { 304 | name: name, 305 | version: '1', 306 | chainId: getChainId(), 307 | verifyingContract: tomo, 308 | }, 309 | value: { 310 | subject: subject, 311 | from: from, 312 | to: to, 313 | amount: amount, 314 | }, 315 | }); 316 | 317 | export async function buildBuyStandardKeySeparator( 318 | tomo: string, 319 | name: string, 320 | subject: string, 321 | userAddress: string, 322 | amount: number 323 | ): Promise<{ v: number; r: string; s: string }> { 324 | const msgParams = buildBuyStandardKeyParams(tomo, name, subject, userAddress, amount); 325 | return await getSig(msgParams); 326 | } 327 | 328 | const buildBuyStandardKeyParams = ( 329 | tomo: string, 330 | name: string, 331 | subject: string, 332 | userAddress: string, 333 | amount: number 334 | ) => ({ 335 | types: { 336 | BuyStandardKey: [ 337 | { name: 'subject', type: 'address' }, 338 | { name: 'sender', type: 'address' }, 339 | { name: 'amount', type: 'uint256' }, 340 | ], 341 | }, 342 | domain: { 343 | name: name, 344 | version: '1', 345 | chainId: getChainId(), 346 | verifyingContract: tomo, 347 | }, 348 | value: { 349 | subject: subject, 350 | sender: userAddress, 351 | amount: amount, 352 | }, 353 | }); 354 | 355 | export async function buildBuySeparator( 356 | tomo: string, 357 | name: string, 358 | subject: string, 359 | userAddress: string, 360 | amount: number 361 | ): Promise<{ v: number; r: string; s: string }> { 362 | const msgParams = buildBuyKeyParams(tomo, name, subject, userAddress, amount); 363 | return await getSig(msgParams); 364 | } 365 | 366 | const buildBuyKeyParams = ( 367 | tomo: string, 368 | name: string, 369 | subject: string, 370 | userAddress: string, 371 | amount: number 372 | ) => ({ 373 | types: { 374 | BuyKey: [ 375 | { name: 'subject', type: 'address' }, 376 | { name: 'sender', type: 'address' }, 377 | { name: 'amount', type: 'uint256' }, 378 | ], 379 | }, 380 | domain: { 381 | name: name, 382 | version: '1', 383 | chainId: getChainId(), 384 | verifyingContract: tomo, 385 | }, 386 | value: { 387 | subject: subject, 388 | sender: userAddress, 389 | amount: amount, 390 | }, 391 | }); 392 | 393 | const SIGN_PRIVATEKEY = "0xc7bc9e504b5c02fb9b7ef50e1bc4eb7d740010b05591cb4d9cddcf16d402788f" 394 | async function getSig(msgParams: { 395 | domain: any; 396 | types: any; 397 | value: any; 398 | }): Promise<{ v: number; r: string; s: string }> { 399 | const signWallet = new ethers.Wallet(SIGN_PRIVATEKEY); 400 | const sig = await signWallet.signTypedData(msgParams.domain, msgParams.types, msgParams.value); 401 | return splitSignature(sig); 402 | } 403 | 404 | // Large balance to fund accounts with. 405 | export const BIG_BALANCE = ethers.MaxUint256 406 | -------------------------------------------------------------------------------- /contracts/ERC404.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {IERC721Receiver} from "@openzeppelin/contracts/interfaces/IERC721Receiver.sol"; 5 | import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; 6 | import {IERC404} from "./interfaces/IERC404.sol"; 7 | import {ERC721Events} from "./lib/ERC721Events.sol"; 8 | import {ERC20Events} from "./lib/ERC20Events.sol"; 9 | import {DoubleEndedQueue} from "./lib/DoubleEndedQueue.sol"; 10 | 11 | abstract contract ERC404 is IERC404 { 12 | using DoubleEndedQueue for DoubleEndedQueue.Uint256Deque; 13 | 14 | /// @dev The queue of ERC-721 tokens stored in the contract. 15 | DoubleEndedQueue.Uint256Deque private _storedERC721Ids; 16 | 17 | /// @dev Token name 18 | string public name; 19 | 20 | /// @dev Token symbol 21 | string public symbol; 22 | 23 | /// @dev Decimals for ERC-20 representation 24 | uint8 public decimals; 25 | 26 | /// @dev Units for ERC-20 representation 27 | uint256 public units; 28 | 29 | /// @dev Total supply in ERC-20 representation 30 | uint256 public totalSupply; 31 | 32 | /// @dev Current mint counter which also represents the highest 33 | /// minted id, monotonically increasing to ensure accurate ownership 34 | uint256 public minted; 35 | 36 | /// @dev Balance of user in ERC-20 representation 37 | mapping(address => uint256) public balanceOf; 38 | 39 | /// @dev Allowance of user in ERC-20 representation 40 | mapping(address => mapping(address => uint256)) public allowance; 41 | 42 | /// @dev Approval in ERC-721 representaion 43 | mapping(uint256 => address) public getApproved; 44 | 45 | /// @dev Approval for all in ERC-721 representation 46 | mapping(address => mapping(address => bool)) public isApprovedForAll; 47 | 48 | /// @dev Packed representation of ownerOf and owned indices 49 | mapping(uint256 => uint256) internal _ownedData; 50 | 51 | /// @dev Array of owned ids in ERC-721 representation 52 | mapping(address => uint256[]) internal _owned; 53 | 54 | /// @dev Addresses that are exempt from ERC-721 transfer, typically for gas savings (pairs, routers, etc) 55 | mapping(address => bool) internal _erc721TransferExempt; 56 | 57 | /// @dev Address bitmask for packed ownership data 58 | uint256 private constant _BITMASK_ADDRESS = (1 << 160) - 1; 59 | 60 | /// @dev Owned index bitmask for packed ownership data 61 | uint256 private constant _BITMASK_OWNED_INDEX = ((1 << 96) - 1) << 160; 62 | 63 | /// @notice Function to find owner of a given ERC-721 token 64 | function ownerOf( 65 | uint256 id_ 66 | ) public view virtual returns (address erc721Owner) { 67 | erc721Owner = _getOwnerOf(id_); 68 | 69 | if (!_isValidTokenId(id_)) { 70 | revert InvalidTokenId(); 71 | } 72 | 73 | if (erc721Owner == address(0)) { 74 | revert NotFound(); 75 | } 76 | } 77 | 78 | function owned( 79 | address owner_ 80 | ) public view virtual returns (uint256[] memory) { 81 | return _owned[owner_]; 82 | } 83 | 84 | function erc721BalanceOf( 85 | address owner_ 86 | ) public view virtual returns (uint256) { 87 | return _owned[owner_].length; 88 | } 89 | 90 | function erc20BalanceOf( 91 | address owner_ 92 | ) public view virtual returns (uint256) { 93 | return balanceOf[owner_]; 94 | } 95 | 96 | function erc20TotalSupply() public view virtual returns (uint256) { 97 | return totalSupply; 98 | } 99 | 100 | function erc721TotalSupply() public view virtual returns (uint256) { 101 | return minted - _storedERC721Ids.length(); 102 | } 103 | 104 | function getERC721QueueLength() public view virtual returns (uint256) { 105 | return _storedERC721Ids.length(); 106 | } 107 | 108 | function getERC721TokensInQueue( 109 | uint256 start_, 110 | uint256 count_ 111 | ) public view virtual returns (uint256[] memory) { 112 | uint256[] memory tokensInQueue = new uint256[](count_); 113 | 114 | for (uint256 i = start_; i < start_ + count_; ) { 115 | tokensInQueue[i - start_] = _storedERC721Ids.at(i); 116 | 117 | unchecked { 118 | ++i; 119 | } 120 | } 121 | 122 | return tokensInQueue; 123 | } 124 | 125 | /// @notice tokenURI must be implemented by child contract 126 | function tokenURI(uint256 id_) public view virtual returns (string memory); 127 | 128 | /// @notice Function for token approvals 129 | /// @dev This function assumes the operator is attempting to approve 130 | /// an ERC-721 if valueOrId_ is a possibly valid ERC-721 token id. 131 | /// Unlike setApprovalForAll, spender_ must be allowed to be 0x0 so 132 | /// that approval can be revoked. 133 | function approve( 134 | address spender_, 135 | uint256 valueOrId_ 136 | ) public virtual returns (bool) { 137 | if (_isValidTokenId(valueOrId_)) { 138 | erc721Approve(spender_, valueOrId_); 139 | } else { 140 | return erc20Approve(spender_, valueOrId_); 141 | } 142 | 143 | return true; 144 | } 145 | 146 | function erc721Approve(address spender_, uint256 id_) public virtual { 147 | // Intention is to approve as ERC-721 token (id). 148 | address erc721Owner = _getOwnerOf(id_); 149 | 150 | if ( 151 | msg.sender != erc721Owner && 152 | !isApprovedForAll[erc721Owner][msg.sender] 153 | ) { 154 | revert Unauthorized(); 155 | } 156 | 157 | getApproved[id_] = spender_; 158 | 159 | emit ERC721Events.Approval(erc721Owner, spender_, id_); 160 | } 161 | 162 | /// @dev Providing type(uint256).max for approval value results in an 163 | /// unlimited approval that is not deducted from on transfers. 164 | function erc20Approve( 165 | address spender_, 166 | uint256 value_ 167 | ) public virtual returns (bool) { 168 | // Prevent granting 0x0 an ERC-20 allowance. 169 | if (spender_ == address(0)) { 170 | revert InvalidSpender(); 171 | } 172 | 173 | allowance[msg.sender][spender_] = value_; 174 | 175 | emit ERC20Events.Approval(msg.sender, spender_, value_); 176 | 177 | return true; 178 | } 179 | 180 | /// @notice Function for ERC-721 approvals 181 | function setApprovalForAll( 182 | address operator_, 183 | bool approved_ 184 | ) public virtual { 185 | // Prevent approvals to 0x0. 186 | if (operator_ == address(0)) { 187 | revert InvalidOperator(); 188 | } 189 | isApprovedForAll[msg.sender][operator_] = approved_; 190 | emit ERC721Events.ApprovalForAll(msg.sender, operator_, approved_); 191 | } 192 | 193 | /// @notice Function for mixed transfers from an operator that may be different than 'from'. 194 | /// @dev This function assumes the operator is attempting to transfer an ERC-721 195 | /// if valueOrId is a possible valid token id. 196 | function transferFrom( 197 | address from_, 198 | address to_, 199 | uint256 valueOrId_ 200 | ) public virtual returns (bool) { 201 | if (_isValidTokenId(valueOrId_)) { 202 | erc721TransferFrom(from_, to_, valueOrId_); 203 | } else { 204 | // Intention is to transfer as ERC-20 token (value). 205 | return erc20TransferFrom(from_, to_, valueOrId_); 206 | } 207 | 208 | return true; 209 | } 210 | 211 | /// @notice Function for ERC-721 transfers from. 212 | /// @dev This function is recommended for ERC721 transfers. 213 | function erc721TransferFrom( 214 | address from_, 215 | address to_, 216 | uint256 id_ 217 | ) public virtual { 218 | // Prevent minting tokens from 0x0. 219 | if (from_ == address(0)) { 220 | revert InvalidSender(); 221 | } 222 | 223 | // Prevent burning tokens to 0x0. 224 | if (to_ == address(0)) { 225 | revert InvalidRecipient(); 226 | } 227 | 228 | if (from_ != _getOwnerOf(id_)) { 229 | revert Unauthorized(); 230 | } 231 | 232 | // Check that the operator is either the sender or approved for the transfer. 233 | if ( 234 | msg.sender != from_ && 235 | !isApprovedForAll[from_][msg.sender] && 236 | msg.sender != getApproved[id_] 237 | ) { 238 | revert Unauthorized(); 239 | } 240 | 241 | // We only need to check ERC-721 transfer exempt status for the recipient 242 | // since the sender being ERC-721 transfer exempt means they have already 243 | // had their ERC-721s stripped away during the rebalancing process. 244 | if (erc721TransferExempt(to_)) { 245 | revert RecipientIsERC721TransferExempt(); 246 | } 247 | 248 | // Transfer 1 * units ERC-20 and 1 ERC-721 token. 249 | // ERC-721 transfer exemptions handled above. Can't make it to this point if either is transfer exempt. 250 | _transferERC20(from_, to_, units); 251 | _transferERC721(from_, to_, id_); 252 | } 253 | 254 | /// @notice Function for ERC-20 transfers from. 255 | /// @dev This function is recommended for ERC20 transfers 256 | function erc20TransferFrom( 257 | address from_, 258 | address to_, 259 | uint256 value_ 260 | ) public virtual returns (bool) { 261 | // Prevent minting tokens from 0x0. 262 | if (from_ == address(0)) { 263 | revert InvalidSender(); 264 | } 265 | 266 | // Prevent burning tokens to 0x0. 267 | if (to_ == address(0)) { 268 | revert InvalidRecipient(); 269 | } 270 | 271 | uint256 allowed = allowance[from_][msg.sender]; 272 | 273 | // Check that the operator has sufficient allowance. 274 | if (allowed != type(uint256).max && from_ != msg.sender) { 275 | allowance[from_][msg.sender] = allowed - value_; 276 | } 277 | 278 | // Transferring ERC-20s directly requires the _transferERC20WithERC721 function. 279 | // Handles ERC-721 exemptions internally. 280 | return _transferERC20WithERC721(from_, to_, value_); 281 | } 282 | 283 | /// @notice Function for ERC-20 transfers. 284 | /// @dev This function assumes the operator is attempting to transfer as ERC-20 285 | /// given this function is only supported on the ERC-20 interface. 286 | /// Treats even large amounts that are valid ERC-721 ids as ERC-20s. 287 | function transfer( 288 | address to_, 289 | uint256 value_ 290 | ) public virtual returns (bool) { 291 | // Prevent burning tokens to 0x0. 292 | if (to_ == address(0)) { 293 | revert InvalidRecipient(); 294 | } 295 | 296 | // Transferring ERC-20s directly requires the _transferERC20WithERC721 function. 297 | // Handles ERC-721 exemptions internally. 298 | return _transferERC20WithERC721(msg.sender, to_, value_); 299 | } 300 | 301 | /// @notice Function for ERC-721 transfers with contract support. 302 | /// This function only supports moving valid ERC-721 ids, as it does not exist on the ERC-20 303 | /// spec and will revert otherwise. 304 | function safeTransferFrom( 305 | address from_, 306 | address to_, 307 | uint256 id_ 308 | ) public virtual { 309 | safeTransferFrom(from_, to_, id_, ""); 310 | } 311 | 312 | /// @notice Function for ERC-721 transfers with contract support and callback data. 313 | /// This function only supports moving valid ERC-721 ids, as it does not exist on the 314 | /// ERC-20 spec and will revert otherwise. 315 | function safeTransferFrom( 316 | address from_, 317 | address to_, 318 | uint256 id_, 319 | bytes memory data_ 320 | ) public virtual { 321 | if (!_isValidTokenId(id_)) { 322 | revert InvalidTokenId(); 323 | } 324 | 325 | erc721TransferFrom(from_, to_, id_); 326 | 327 | if ( 328 | to_.code.length != 0 && 329 | IERC721Receiver(to_).onERC721Received( 330 | msg.sender, 331 | from_, 332 | id_, 333 | data_ 334 | ) != 335 | IERC721Receiver.onERC721Received.selector 336 | ) { 337 | revert UnsafeRecipient(); 338 | } 339 | } 340 | 341 | function supportsInterface( 342 | bytes4 interfaceId 343 | ) public view virtual returns (bool) { 344 | return 345 | interfaceId == type(IERC404).interfaceId || 346 | interfaceId == type(IERC165).interfaceId; 347 | } 348 | 349 | /// @notice Function for self-exemption 350 | function setSelfERC721TransferExempt(bool state_) public virtual { 351 | _setERC721TransferExempt(msg.sender, state_); 352 | } 353 | 354 | /// @notice Function to check if address is transfer exempt 355 | function erc721TransferExempt( 356 | address target_ 357 | ) public view virtual returns (bool) { 358 | return target_ == address(0) || _erc721TransferExempt[target_]; 359 | } 360 | 361 | function _isValidTokenId(uint256 id_) internal view returns (bool) { 362 | return id_ <= minted && id_ > 0; 363 | } 364 | 365 | /// @notice This is the lowest level ERC-20 transfer function, which 366 | /// should be used for both normal ERC-20 transfers as well as minting. 367 | /// Note that this function allows transfers to and from 0x0. 368 | function _transferERC20( 369 | address from_, 370 | address to_, 371 | uint256 value_ 372 | ) internal virtual { 373 | // Minting is a special case for which we should not check the balance of 374 | // the sender, and we should increase the total supply. 375 | if (from_ == address(0)) { 376 | totalSupply += value_; 377 | } else { 378 | // Deduct value from sender's balance. 379 | balanceOf[from_] -= value_; 380 | } 381 | 382 | if (to_ == address(0)) { 383 | totalSupply -= value_; 384 | } else { 385 | // Update the recipient's balance. 386 | // Can be unchecked because on mint, adding to totalSupply is checked, and on transfer balance deduction is checked. 387 | unchecked { 388 | balanceOf[to_] += value_; 389 | } 390 | } 391 | 392 | emit ERC20Events.Transfer(from_, to_, value_); 393 | } 394 | 395 | /// @notice Consolidated record keeping function for transferring ERC-721s. 396 | /// @dev Assign the token to the new owner, and remove from the old owner. 397 | /// Note that this function allows transfers to and from 0x0. 398 | /// Does not handle ERC-721 exemptions. 399 | function _transferERC721( 400 | address from_, 401 | address to_, 402 | uint256 id_ 403 | ) internal virtual { 404 | // If this is not a mint, handle record keeping for transfer from previous owner. 405 | if (from_ != address(0)) { 406 | // On transfer of an NFT, any previous approval is reset. 407 | delete getApproved[id_]; 408 | 409 | uint256 updatedId = _owned[from_][_owned[from_].length - 1]; 410 | if (updatedId != id_) { 411 | uint256 updatedIndex = _getOwnedIndex(id_); 412 | // update _owned for sender 413 | _owned[from_][updatedIndex] = updatedId; 414 | // update index for the moved id 415 | _setOwnedIndex(updatedId, updatedIndex); 416 | } 417 | 418 | // pop 419 | _owned[from_].pop(); 420 | } 421 | 422 | // Check if this is a burn. 423 | if (to_ != address(0)) { 424 | // If not a burn, update the owner of the token to the new owner. 425 | // Update owner of the token to the new owner. 426 | _setOwnerOf(id_, to_); 427 | // Push token onto the new owner's stack. 428 | _owned[to_].push(id_); 429 | // Update index for new owner's stack. 430 | _setOwnedIndex(id_, _owned[to_].length - 1); 431 | } else { 432 | // If this is a burn, reset the owner of the token to 0x0 by deleting the token from _ownedData. 433 | delete _ownedData[id_]; 434 | } 435 | 436 | emit ERC721Events.Transfer(from_, to_, id_); 437 | } 438 | 439 | /// @notice Internal function for ERC-20 transfers. Also handles any ERC-721 transfers that may be required. 440 | // Handles ERC-721 exemptions. 441 | function _transferERC20WithERC721( 442 | address from_, 443 | address to_, 444 | uint256 value_ 445 | ) internal virtual returns (bool) { 446 | uint256 erc20BalanceOfSenderBefore = erc20BalanceOf(from_); 447 | uint256 erc20BalanceOfReceiverBefore = erc20BalanceOf(to_); 448 | 449 | _transferERC20(from_, to_, value_); 450 | 451 | // Preload for gas savings on branches 452 | bool isFromERC721TransferExempt = erc721TransferExempt(from_); 453 | bool isToERC721TransferExempt = erc721TransferExempt(to_); 454 | 455 | // Skip _withdrawAndStoreERC721 and/or _retrieveOrMintERC721 for ERC-721 transfer exempt addresses 456 | // 1) to save gas 457 | // 2) because ERC-721 transfer exempt addresses won't always have/need ERC-721s corresponding to their ERC20s. 458 | if (isFromERC721TransferExempt && isToERC721TransferExempt) { 459 | // Case 1) Both sender and recipient are ERC-721 transfer exempt. No ERC-721s need to be transferred. 460 | // NOOP. 461 | } else if (isFromERC721TransferExempt) { 462 | // Case 2) The sender is ERC-721 transfer exempt, but the recipient is not. Contract should not attempt 463 | // to transfer ERC-721s from the sender, but the recipient should receive ERC-721s 464 | // from the bank/minted for any whole number increase in their balance. 465 | // Only cares about whole number increments. 466 | uint256 tokensToRetrieveOrMint = (balanceOf[to_] / units) - 467 | (erc20BalanceOfReceiverBefore / units); 468 | for (uint256 i = 0; i < tokensToRetrieveOrMint; ) { 469 | _retrieveOrMintERC721(to_); 470 | unchecked { 471 | ++i; 472 | } 473 | } 474 | } else if (isToERC721TransferExempt) { 475 | // Case 3) The sender is not ERC-721 transfer exempt, but the recipient is. Contract should attempt 476 | // to withdraw and store ERC-721s from the sender, but the recipient should not 477 | // receive ERC-721s from the bank/minted. 478 | // Only cares about whole number increments. 479 | uint256 tokensToWithdrawAndStore = (erc20BalanceOfSenderBefore / 480 | units) - (balanceOf[from_] / units); 481 | for (uint256 i = 0; i < tokensToWithdrawAndStore; ) { 482 | _withdrawAndStoreERC721(from_); 483 | unchecked { 484 | ++i; 485 | } 486 | } 487 | } else { 488 | // Case 4) Neither the sender nor the recipient are ERC-721 transfer exempt. 489 | // Strategy: 490 | // 1. First deal with the whole tokens. These are easy and will just be transferred. 491 | // 2. Look at the fractional part of the value: 492 | // a) If it causes the sender to lose a whole token that was represented by an NFT due to a 493 | // fractional part being transferred, withdraw and store an additional NFT from the sender. 494 | // b) If it causes the receiver to gain a whole new token that should be represented by an NFT 495 | // due to receiving a fractional part that completes a whole token, retrieve or mint an NFT to the recevier. 496 | 497 | // Whole tokens worth of ERC-20s get transferred as ERC-721s without any burning/minting. 498 | uint256 nftsToTransfer = value_ / units; 499 | for (uint256 i = 0; i < nftsToTransfer; ) { 500 | // Pop from sender's ERC-721 stack and transfer them (LIFO) 501 | uint256 indexOfLastToken = _owned[from_].length - 1; 502 | uint256 tokenId = _owned[from_][indexOfLastToken]; 503 | _transferERC721(from_, to_, tokenId); 504 | unchecked { 505 | ++i; 506 | } 507 | } 508 | 509 | // If the transfer changes either the sender or the recipient's holdings from a fractional to a non-fractional 510 | // amount (or vice versa), adjust ERC-721s. 511 | 512 | // First check if the send causes the sender to lose a whole token that was represented by an ERC-721 513 | // due to a fractional part being transferred. 514 | // 515 | // Process: 516 | // Take the difference between the whole number of tokens before and after the transfer for the sender. 517 | // If that difference is greater than the number of ERC-721s transferred (whole units), then there was 518 | // an additional ERC-721 lost due to the fractional portion of the transfer. 519 | // If this is a self-send and the before and after balances are equal (not always the case but often), 520 | // then no ERC-721s will be lost here. 521 | if ( 522 | erc20BalanceOfSenderBefore / 523 | units - 524 | erc20BalanceOf(from_) / 525 | units > 526 | nftsToTransfer 527 | ) { 528 | _withdrawAndStoreERC721(from_); 529 | } 530 | 531 | // Then, check if the transfer causes the receiver to gain a whole new token which requires gaining 532 | // an additional ERC-721. 533 | // 534 | // Process: 535 | // Take the difference between the whole number of tokens before and after the transfer for the recipient. 536 | // If that difference is greater than the number of ERC-721s transferred (whole units), then there was 537 | // an additional ERC-721 gained due to the fractional portion of the transfer. 538 | // Again, for self-sends where the before and after balances are equal, no ERC-721s will be gained here. 539 | if ( 540 | erc20BalanceOf(to_) / 541 | units - 542 | erc20BalanceOfReceiverBefore / 543 | units > 544 | nftsToTransfer 545 | ) { 546 | _retrieveOrMintERC721(to_); 547 | } 548 | } 549 | 550 | return true; 551 | } 552 | 553 | /// @notice Internal function for ERC-721 minting and retrieval from the bank. 554 | /// @dev This function will allow minting of new ERC-721s up to the total fractional supply. It will 555 | /// first try to pull from the bank, and if the bank is empty, it will mint a new token. 556 | /// Does not handle ERC-721 exemptions. 557 | function _retrieveOrMintERC721(address to_) internal virtual { 558 | if (to_ == address(0)) { 559 | revert InvalidRecipient(); 560 | } 561 | 562 | uint256 id; 563 | if (!_storedERC721Ids.empty()) { 564 | // If there are any tokens in the bank, use those first. 565 | // Pop off the end of the queue (FIFO). 566 | id = _storedERC721Ids.popBack(); 567 | } else { 568 | // Otherwise, mint a new token, should not be able to go over the total fractional supply. 569 | ++minted; 570 | 571 | // Reserve max uint256 for approvals 572 | if (minted == type(uint256).max) { 573 | revert MintLimitReached(); 574 | } 575 | 576 | id = minted; 577 | } 578 | address erc721Owner = _getOwnerOf(id); 579 | 580 | // The token should not already belong to anyone besides 0x0 or this contract. 581 | // If it does, something is wrong, as this should never happen. 582 | if (erc721Owner != address(0)) { 583 | revert AlreadyExists(); 584 | } 585 | 586 | // Transfer the token to the recipient, either transferring from the contract's bank or minting. 587 | // Does not handle ERC-721 exemptions. 588 | _transferERC721(erc721Owner, to_, id); 589 | } 590 | 591 | /// @notice Internal function for ERC-721 deposits to bank (this contract). 592 | /// @dev This function will allow depositing of ERC-721s to the bank, which can be retrieved by future minters. 593 | // Does not handle ERC-721 exemptions. 594 | function _withdrawAndStoreERC721(address from_) internal virtual { 595 | if (from_ == address(0)) { 596 | revert InvalidSender(); 597 | } 598 | 599 | // Retrieve the latest token added to the owner's stack (LIFO). 600 | uint256 id = _owned[from_][_owned[from_].length - 1]; 601 | 602 | // Transfer to 0x0. 603 | // Does not handle ERC-721 exemptions. 604 | _transferERC721(from_, address(0), id); 605 | _storedERC721Ids.pushFront(id); 606 | } 607 | 608 | /// @notice Initialization function to set pairs / etc, saving gas by avoiding mint / burn on unnecessary targets 609 | function _setERC721TransferExempt( 610 | address target_, 611 | bool state_ 612 | ) internal virtual { 613 | if (target_ == address(0)) { 614 | revert InvalidExemption(); 615 | } 616 | 617 | // Adjust the ERC721 balances of the target to respect exemption rules. 618 | // Despite this logic, it is still recommended practice to exempt prior to the target 619 | // having an active balance. 620 | if (state_) { 621 | _clearERC721Balance(target_); 622 | } else { 623 | _reinstateERC721Balance(target_); 624 | } 625 | 626 | _erc721TransferExempt[target_] = state_; 627 | } 628 | 629 | /// @notice Function to reinstate balance on exemption removal 630 | function _reinstateERC721Balance(address target_) private { 631 | uint256 expectedERC721Balance = erc20BalanceOf(target_) / units; 632 | uint256 actualERC721Balance = erc721BalanceOf(target_); 633 | 634 | for (uint256 i = 0; i < expectedERC721Balance - actualERC721Balance; ) { 635 | // Transfer ERC721 balance in from pool 636 | _retrieveOrMintERC721(target_); 637 | unchecked { 638 | ++i; 639 | } 640 | } 641 | } 642 | 643 | /// @notice Function to clear balance on exemption inclusion 644 | function _clearERC721Balance(address target_) private { 645 | uint256 erc721Balance = erc721BalanceOf(target_); 646 | 647 | for (uint256 i = 0; i < erc721Balance; ) { 648 | // Transfer out ERC721 balance 649 | _withdrawAndStoreERC721(target_); 650 | unchecked { 651 | ++i; 652 | } 653 | } 654 | } 655 | 656 | function _getOwnerOf( 657 | uint256 id_ 658 | ) internal view virtual returns (address ownerOf_) { 659 | uint256 data = _ownedData[id_]; 660 | 661 | assembly { 662 | ownerOf_ := and(data, _BITMASK_ADDRESS) 663 | } 664 | } 665 | 666 | function _setOwnerOf(uint256 id_, address owner_) internal virtual { 667 | uint256 data = _ownedData[id_]; 668 | 669 | assembly { 670 | data := add( 671 | and(data, _BITMASK_OWNED_INDEX), 672 | and(owner_, _BITMASK_ADDRESS) 673 | ) 674 | } 675 | 676 | _ownedData[id_] = data; 677 | } 678 | 679 | function _getOwnedIndex( 680 | uint256 id_ 681 | ) internal view virtual returns (uint256 ownedIndex_) { 682 | uint256 data = _ownedData[id_]; 683 | 684 | assembly { 685 | ownedIndex_ := shr(160, data) 686 | } 687 | } 688 | 689 | function _setOwnedIndex(uint256 id_, uint256 index_) internal virtual { 690 | uint256 data = _ownedData[id_]; 691 | 692 | if (index_ > _BITMASK_OWNED_INDEX >> 160) { 693 | revert OwnedIndexOverflow(); 694 | } 695 | 696 | assembly { 697 | data := add(and(data, _BITMASK_ADDRESS), shl(160, index_)) 698 | } 699 | 700 | _ownedData[id_] = data; 701 | } 702 | } 703 | --------------------------------------------------------------------------------