├── .github └── workflows │ ├── lint.yml │ ├── test-forge.yml │ └── test-hardhat.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── README.md ├── abis ├── Element.json ├── Foundation.json ├── LooksRareV2.json ├── NFT20.json ├── NFTXZap.json ├── Seaport.json ├── Sudoswap.json └── X2Y2.json ├── foundry.toml ├── hardhat.config.ts ├── package.json ├── remappings.txt ├── src ├── entities │ ├── Command.ts │ ├── NFTTrade.ts │ ├── index.ts │ └── protocols │ │ ├── cryptopunk.ts │ │ ├── element-market.ts │ │ ├── foundation.ts │ │ ├── index.ts │ │ ├── looksRareV2.ts │ │ ├── nft20.ts │ │ ├── nftx.ts │ │ ├── seaport.ts │ │ ├── sudoswap.ts │ │ ├── uniswap.ts │ │ ├── unwrapWETH.ts │ │ └── x2y2.ts ├── index.ts ├── swapRouter.ts └── utils │ ├── constants.ts │ ├── getNativeCurrencyValue.ts │ ├── inputTokens.ts │ ├── numbers.ts │ ├── routerCommands.ts │ └── routerTradeAdapter.ts ├── test ├── forge │ ├── MixedSwapCallParameters.t.sol │ ├── SwapERC20CallParameters.t.sol │ ├── SwapNFTCallParameters.t.sol │ ├── interop.json │ ├── utils │ │ ├── DeployRouter.sol │ │ ├── ICryptopunksMarket.sol │ │ └── Interop.sol │ └── writeInterop.ts ├── mixedTrades.test.ts ├── nftTrades.test.ts ├── orders │ ├── element.ts │ ├── looksRareV2.ts │ ├── seaportV1_4.ts │ ├── seaportV1_5.ts │ └── x2y2.ts ├── uniswapTrades.test.ts └── utils │ ├── addresses.ts │ ├── hexToDecimalString.ts │ ├── permit2.test.ts │ ├── permit2.ts │ └── uniswapData.ts ├── tsconfig.json └── yarn.lock /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | run-linters: 11 | name: Run linters 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out Git repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up node 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: 16 22 | registry-url: https://registry.npmjs.org 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Install Foundry 28 | uses: foundry-rs/foundry-toolchain@v1 29 | with: 30 | version: nightly 31 | 32 | - name: Lint 33 | run: yarn prettier 34 | -------------------------------------------------------------------------------- /.github/workflows/test-forge.yml: -------------------------------------------------------------------------------- 1 | name: Forge Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | name: Unit tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup node 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: 16 22 | registry-url: https://registry.npmjs.org 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Install Foundry 28 | uses: foundry-rs/foundry-toolchain@v1 29 | with: 30 | version: nightly 31 | 32 | - name: Run tests 33 | run: yarn test:forge 34 | env: 35 | FORK_URL: '${{ secrets.FORK_URL }}' 36 | -------------------------------------------------------------------------------- /.github/workflows/test-hardhat.yml: -------------------------------------------------------------------------------- 1 | name: Hardhat Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | name: Hardhat tests 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup node 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 16 23 | registry-url: https://registry.npmjs.org 24 | 25 | - name: Install dependencies 26 | run: yarn install --frozen-lockfile 27 | 28 | 29 | - name: Install Foundry 30 | uses: foundry-rs/foundry-toolchain@v1 31 | with: 32 | version: nightly 33 | 34 | - name: build 35 | run: yarn build 36 | 37 | # needed to generate abi 38 | - name: Build solidity 39 | run: forge build 40 | 41 | - name: Run tests 42 | run: yarn test:hardhat 43 | env: 44 | FORK_URL: '${{ secrets.FORK_URL }}' 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | docs 6 | cache 7 | out 8 | .env 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solmate"] 5 | path = lib/solmate 6 | url = https://github.com/transmissions11/solmate 7 | [submodule "lib/permit2"] 8 | path = lib/permit2 9 | url = https://github.com/Uniswap/permit2 10 | [submodule "lib/openzeppelin-contracts"] 11 | path = lib/openzeppelin-contracts 12 | url = https://github.com/openzeppelin/openzeppelin-contracts 13 | branch = v4.8.0 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out/ 2 | lib 3 | cache 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @uniswap/universal-router-sdk - Now at `Uniswap/sdks` 2 | 3 | All versions after 1.9.0 of this SDK can be found in the [SDK monorepo](https://github.com/Uniswap/sdks/tree/main/sdks/universal-router-sdk)! Please file all future issues, PR’s, and discussions there. 4 | 5 | ### Old Issues and PR’s 6 | 7 | If you have an issue or open PR that is still active on this SDK in this repository, please recreate it in the new repository. Some existing issues and PR’s may be automatically migrated by the Uniswap Labs team. 8 | -------------------------------------------------------------------------------- /abis/NFT20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "previousOwner", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "newOwner", 15 | "type": "address" 16 | } 17 | ], 18 | "name": "OwnershipTransferred", 19 | "type": "event" 20 | }, 21 | { 22 | "inputs": [], 23 | "name": "ETH", 24 | "outputs": [ 25 | { 26 | "internalType": "address", 27 | "name": "", 28 | "type": "address" 29 | } 30 | ], 31 | "stateMutability": "view", 32 | "type": "function" 33 | }, 34 | { 35 | "inputs": [], 36 | "name": "NFT20", 37 | "outputs": [ 38 | { 39 | "internalType": "contract INFT20Factory", 40 | "name": "", 41 | "type": "address" 42 | } 43 | ], 44 | "stateMutability": "view", 45 | "type": "function" 46 | }, 47 | { 48 | "inputs": [], 49 | "name": "UNIV2", 50 | "outputs": [ 51 | { 52 | "internalType": "address", 53 | "name": "", 54 | "type": "address" 55 | } 56 | ], 57 | "stateMutability": "view", 58 | "type": "function" 59 | }, 60 | { 61 | "inputs": [], 62 | "name": "UNIV3", 63 | "outputs": [ 64 | { 65 | "internalType": "address", 66 | "name": "", 67 | "type": "address" 68 | } 69 | ], 70 | "stateMutability": "view", 71 | "type": "function" 72 | }, 73 | { 74 | "inputs": [], 75 | "name": "WETH", 76 | "outputs": [ 77 | { 78 | "internalType": "address", 79 | "name": "", 80 | "type": "address" 81 | } 82 | ], 83 | "stateMutability": "view", 84 | "type": "function" 85 | }, 86 | { 87 | "inputs": [ 88 | { 89 | "internalType": "address", 90 | "name": "_nft", 91 | "type": "address" 92 | }, 93 | { 94 | "internalType": "uint256[]", 95 | "name": "_toIds", 96 | "type": "uint256[]" 97 | }, 98 | { 99 | "internalType": "uint256[]", 100 | "name": "_toAmounts", 101 | "type": "uint256[]" 102 | }, 103 | { 104 | "internalType": "address", 105 | "name": "_receipient", 106 | "type": "address" 107 | }, 108 | { 109 | "internalType": "uint24", 110 | "name": "_fee", 111 | "type": "uint24" 112 | }, 113 | { 114 | "internalType": "bool", 115 | "name": "isV3", 116 | "type": "bool" 117 | } 118 | ], 119 | "name": "ethForNft", 120 | "outputs": [], 121 | "stateMutability": "payable", 122 | "type": "function" 123 | }, 124 | { 125 | "inputs": [ 126 | { 127 | "internalType": "address", 128 | "name": "_nft", 129 | "type": "address" 130 | }, 131 | { 132 | "internalType": "uint256[]", 133 | "name": "_ids", 134 | "type": "uint256[]" 135 | }, 136 | { 137 | "internalType": "uint256[]", 138 | "name": "_amounts", 139 | "type": "uint256[]" 140 | }, 141 | { 142 | "internalType": "bool", 143 | "name": "isErc721", 144 | "type": "bool" 145 | }, 146 | { 147 | "internalType": "uint24", 148 | "name": "_fee", 149 | "type": "uint24" 150 | }, 151 | { 152 | "internalType": "bool", 153 | "name": "isV3", 154 | "type": "bool" 155 | } 156 | ], 157 | "name": "nftForEth", 158 | "outputs": [], 159 | "stateMutability": "nonpayable", 160 | "type": "function" 161 | }, 162 | { 163 | "inputs": [], 164 | "name": "owner", 165 | "outputs": [ 166 | { 167 | "internalType": "address", 168 | "name": "", 169 | "type": "address" 170 | } 171 | ], 172 | "stateMutability": "view", 173 | "type": "function" 174 | }, 175 | { 176 | "inputs": [ 177 | { 178 | "internalType": "address", 179 | "name": "tokenAddress", 180 | "type": "address" 181 | }, 182 | { 183 | "internalType": "uint256", 184 | "name": "tokenAmount", 185 | "type": "uint256" 186 | }, 187 | { 188 | "internalType": "address", 189 | "name": "sendTo", 190 | "type": "address" 191 | } 192 | ], 193 | "name": "recoverERC20", 194 | "outputs": [], 195 | "stateMutability": "nonpayable", 196 | "type": "function" 197 | }, 198 | { 199 | "inputs": [], 200 | "name": "renounceOwnership", 201 | "outputs": [], 202 | "stateMutability": "nonpayable", 203 | "type": "function" 204 | }, 205 | { 206 | "inputs": [ 207 | { 208 | "internalType": "address", 209 | "name": "_registry", 210 | "type": "address" 211 | } 212 | ], 213 | "name": "setNFT20", 214 | "outputs": [], 215 | "stateMutability": "nonpayable", 216 | "type": "function" 217 | }, 218 | { 219 | "inputs": [ 220 | { 221 | "internalType": "address", 222 | "name": "newOwner", 223 | "type": "address" 224 | } 225 | ], 226 | "name": "transferOwnership", 227 | "outputs": [], 228 | "stateMutability": "nonpayable", 229 | "type": "function" 230 | }, 231 | { 232 | "inputs": [], 233 | "name": "withdrawEth", 234 | "outputs": [], 235 | "stateMutability": "payable", 236 | "type": "function" 237 | }, 238 | { 239 | "stateMutability": "payable", 240 | "type": "receive" 241 | } 242 | ] 243 | -------------------------------------------------------------------------------- /abis/NFTXZap.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { "internalType": "address", "name": "_nftxFactory", "type": "address" }, 5 | { "internalType": "address", "name": "_WETH", "type": "address" }, 6 | { "internalType": "address payable", "name": "_swapTarget", "type": "address" }, 7 | { "internalType": "uint256", "name": "_dustThreshold", "type": "uint256" } 8 | ], 9 | "stateMutability": "nonpayable", 10 | "type": "constructor" 11 | }, 12 | { 13 | "anonymous": false, 14 | "inputs": [ 15 | { "indexed": false, "internalType": "uint256", "name": "count", "type": "uint256" }, 16 | { "indexed": false, "internalType": "uint256", "name": "ethSpent", "type": "uint256" }, 17 | { "indexed": false, "internalType": "address", "name": "to", "type": "address" } 18 | ], 19 | "name": "Buy", 20 | "type": "event" 21 | }, 22 | { 23 | "anonymous": false, 24 | "inputs": [ 25 | { "indexed": false, "internalType": "uint256", "name": "ethAmount", "type": "uint256" }, 26 | { "indexed": false, "internalType": "uint256", "name": "vTokenAmount", "type": "uint256" }, 27 | { "indexed": false, "internalType": "address", "name": "to", "type": "address" } 28 | ], 29 | "name": "DustReturned", 30 | "type": "event" 31 | }, 32 | { 33 | "anonymous": false, 34 | "inputs": [ 35 | { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, 36 | { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } 37 | ], 38 | "name": "OwnershipTransferred", 39 | "type": "event" 40 | }, 41 | { 42 | "anonymous": false, 43 | "inputs": [ 44 | { "indexed": false, "internalType": "uint256", "name": "count", "type": "uint256" }, 45 | { "indexed": false, "internalType": "uint256", "name": "ethReceived", "type": "uint256" }, 46 | { "indexed": false, "internalType": "address", "name": "to", "type": "address" } 47 | ], 48 | "name": "Sell", 49 | "type": "event" 50 | }, 51 | { 52 | "anonymous": false, 53 | "inputs": [ 54 | { "indexed": false, "internalType": "uint256", "name": "count", "type": "uint256" }, 55 | { "indexed": false, "internalType": "uint256", "name": "ethSpent", "type": "uint256" }, 56 | { "indexed": false, "internalType": "address", "name": "to", "type": "address" } 57 | ], 58 | "name": "Swap", 59 | "type": "event" 60 | }, 61 | { 62 | "inputs": [], 63 | "name": "WETH", 64 | "outputs": [{ "internalType": "contract IWETH", "name": "", "type": "address" }], 65 | "stateMutability": "view", 66 | "type": "function" 67 | }, 68 | { 69 | "inputs": [ 70 | { "internalType": "uint256", "name": "vaultId", "type": "uint256" }, 71 | { "internalType": "uint256", "name": "amount", "type": "uint256" }, 72 | { "internalType": "uint256[]", "name": "specificIds", "type": "uint256[]" }, 73 | { "internalType": "bytes", "name": "swapCallData", "type": "bytes" }, 74 | { "internalType": "address payable", "name": "to", "type": "address" } 75 | ], 76 | "name": "buyAndRedeem", 77 | "outputs": [], 78 | "stateMutability": "payable", 79 | "type": "function" 80 | }, 81 | { 82 | "inputs": [ 83 | { "internalType": "uint256", "name": "vaultId", "type": "uint256" }, 84 | { "internalType": "uint256[]", "name": "idsIn", "type": "uint256[]" }, 85 | { "internalType": "uint256[]", "name": "amounts", "type": "uint256[]" }, 86 | { "internalType": "uint256[]", "name": "specificIds", "type": "uint256[]" }, 87 | { "internalType": "bytes", "name": "swapCallData", "type": "bytes" }, 88 | { "internalType": "address payable", "name": "to", "type": "address" } 89 | ], 90 | "name": "buyAndSwap1155", 91 | "outputs": [], 92 | "stateMutability": "payable", 93 | "type": "function" 94 | }, 95 | { 96 | "inputs": [ 97 | { "internalType": "uint256", "name": "vaultId", "type": "uint256" }, 98 | { "internalType": "uint256[]", "name": "idsIn", "type": "uint256[]" }, 99 | { "internalType": "uint256[]", "name": "specificIds", "type": "uint256[]" }, 100 | { "internalType": "bytes", "name": "swapCallData", "type": "bytes" }, 101 | { "internalType": "address payable", "name": "to", "type": "address" } 102 | ], 103 | "name": "buyAndSwap721", 104 | "outputs": [], 105 | "stateMutability": "payable", 106 | "type": "function" 107 | }, 108 | { 109 | "inputs": [], 110 | "name": "dustThreshold", 111 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 112 | "stateMutability": "view", 113 | "type": "function" 114 | }, 115 | { 116 | "inputs": [], 117 | "name": "feeDistributor", 118 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 119 | "stateMutability": "view", 120 | "type": "function" 121 | }, 122 | { 123 | "inputs": [ 124 | { "internalType": "uint256", "name": "vaultId", "type": "uint256" }, 125 | { "internalType": "uint256[]", "name": "ids", "type": "uint256[]" }, 126 | { "internalType": "uint256[]", "name": "amounts", "type": "uint256[]" }, 127 | { "internalType": "bytes", "name": "swapCallData", "type": "bytes" }, 128 | { "internalType": "address payable", "name": "to", "type": "address" } 129 | ], 130 | "name": "mintAndSell1155", 131 | "outputs": [], 132 | "stateMutability": "nonpayable", 133 | "type": "function" 134 | }, 135 | { 136 | "inputs": [ 137 | { "internalType": "uint256", "name": "vaultId", "type": "uint256" }, 138 | { "internalType": "uint256[]", "name": "ids", "type": "uint256[]" }, 139 | { "internalType": "bytes", "name": "swapCallData", "type": "bytes" }, 140 | { "internalType": "address payable", "name": "to", "type": "address" } 141 | ], 142 | "name": "mintAndSell721", 143 | "outputs": [], 144 | "stateMutability": "nonpayable", 145 | "type": "function" 146 | }, 147 | { 148 | "inputs": [], 149 | "name": "nftxFactory", 150 | "outputs": [{ "internalType": "contract INFTXVaultFactory", "name": "", "type": "address" }], 151 | "stateMutability": "view", 152 | "type": "function" 153 | }, 154 | { 155 | "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 156 | "name": "nftxVaultAddresses", 157 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 158 | "stateMutability": "view", 159 | "type": "function" 160 | }, 161 | { 162 | "inputs": [ 163 | { "internalType": "address", "name": "", "type": "address" }, 164 | { "internalType": "address", "name": "", "type": "address" }, 165 | { "internalType": "uint256[]", "name": "", "type": "uint256[]" }, 166 | { "internalType": "uint256[]", "name": "", "type": "uint256[]" }, 167 | { "internalType": "bytes", "name": "", "type": "bytes" } 168 | ], 169 | "name": "onERC1155BatchReceived", 170 | "outputs": [{ "internalType": "bytes4", "name": "", "type": "bytes4" }], 171 | "stateMutability": "nonpayable", 172 | "type": "function" 173 | }, 174 | { 175 | "inputs": [ 176 | { "internalType": "address", "name": "", "type": "address" }, 177 | { "internalType": "address", "name": "", "type": "address" }, 178 | { "internalType": "uint256", "name": "", "type": "uint256" }, 179 | { "internalType": "uint256", "name": "", "type": "uint256" }, 180 | { "internalType": "bytes", "name": "", "type": "bytes" } 181 | ], 182 | "name": "onERC1155Received", 183 | "outputs": [{ "internalType": "bytes4", "name": "", "type": "bytes4" }], 184 | "stateMutability": "nonpayable", 185 | "type": "function" 186 | }, 187 | { 188 | "inputs": [ 189 | { "internalType": "address", "name": "", "type": "address" }, 190 | { "internalType": "address", "name": "", "type": "address" }, 191 | { "internalType": "uint256", "name": "", "type": "uint256" }, 192 | { "internalType": "bytes", "name": "", "type": "bytes" } 193 | ], 194 | "name": "onERC721Received", 195 | "outputs": [{ "internalType": "bytes4", "name": "", "type": "bytes4" }], 196 | "stateMutability": "nonpayable", 197 | "type": "function" 198 | }, 199 | { 200 | "inputs": [], 201 | "name": "owner", 202 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 203 | "stateMutability": "view", 204 | "type": "function" 205 | }, 206 | { 207 | "inputs": [{ "internalType": "bool", "name": "_paused", "type": "bool" }], 208 | "name": "pause", 209 | "outputs": [], 210 | "stateMutability": "nonpayable", 211 | "type": "function" 212 | }, 213 | { 214 | "inputs": [], 215 | "name": "paused", 216 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 217 | "stateMutability": "view", 218 | "type": "function" 219 | }, 220 | { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, 221 | { 222 | "inputs": [{ "internalType": "address", "name": "token", "type": "address" }], 223 | "name": "rescue", 224 | "outputs": [], 225 | "stateMutability": "nonpayable", 226 | "type": "function" 227 | }, 228 | { 229 | "inputs": [{ "internalType": "uint256", "name": "_dustThreshold", "type": "uint256" }], 230 | "name": "setDustThreshold", 231 | "outputs": [], 232 | "stateMutability": "nonpayable", 233 | "type": "function" 234 | }, 235 | { 236 | "inputs": [{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }], 237 | "name": "supportsInterface", 238 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 239 | "stateMutability": "view", 240 | "type": "function" 241 | }, 242 | { 243 | "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], 244 | "name": "transferOwnership", 245 | "outputs": [], 246 | "stateMutability": "nonpayable", 247 | "type": "function" 248 | }, 249 | { "stateMutability": "payable", "type": "receive" } 250 | ] 251 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc = "0.8.17" 3 | fs_permissions = [{ access = "read", path = "./permit2/out/Permit2.sol/Permit2.json"}, { access = "read", path = "./test/forge/interop.json"}] 4 | src = "./test/forge" 5 | via_ir = true 6 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | export default { 5 | networks: { 6 | hardhat: { 7 | allowUnlimitedContractSize: false, 8 | chainId: 1, 9 | forking: { 10 | url: `${process.env.FORK_URL}`, 11 | blockNumber: 15360000, 12 | }, 13 | }, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uniswap/universal-router-sdk", 3 | "version": "1.9.0", 4 | "description": "sdk for integrating with the Universal Router contracts", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "engines": { 11 | "node": ">=14" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "scripts": { 17 | "test:all": "yarn test:hardhat && yarn test:forge", 18 | "test:hardhat": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' hardhat test", 19 | "test:forge": "forge test", 20 | "install:ur": "cd node_modules/@uniswap/universal-router && forge install", 21 | "build": "tsdx build", 22 | "prettier:fix": "prettier --write '**/*.ts' && prettier --write '**/*.json'", 23 | "forge:fix": "forge fmt", 24 | "lint:fix": "yarn prettier:fix && yarn forge:fix", 25 | "prettier": "prettier --check '**/*.ts' && prettier --check '**/*.json'", 26 | "docs": "typedoc" 27 | }, 28 | "author": "@Uniswap", 29 | "prettier": { 30 | "printWidth": 120, 31 | "semi": false, 32 | "singleQuote": true, 33 | "trailingComma": "es5" 34 | }, 35 | "devDependencies": { 36 | "@types/chai": "^4.2.18", 37 | "@types/mocha": "^8.2.2", 38 | "@types/node": "^15.12.2", 39 | "@types/node-fetch": "^2.6.2", 40 | "chai": "^4.3.4", 41 | "dotenv": "^16.0.3", 42 | "eslint-plugin-prettier": "^3.4.0", 43 | "hardhat": "^2.6.8", 44 | "prettier": "^2.3.1", 45 | "ts-node": "^10.0.0", 46 | "tsdx": "^0.14.1", 47 | "tslib": "^2.3.0", 48 | "typedoc": "^0.21.2", 49 | "typescript": "^4.3.3" 50 | }, 51 | "dependencies": { 52 | "@uniswap/permit2-sdk": "^1.2.0", 53 | "@uniswap/router-sdk": "^1.9.0", 54 | "@uniswap/sdk-core": "^4.2.0", 55 | "@uniswap/universal-router": "1.6.0", 56 | "@uniswap/v2-sdk": "^4.3.0", 57 | "@uniswap/v3-sdk": "^3.11.0", 58 | "bignumber.js": "^9.0.2", 59 | "ethers": "^5.3.1" 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "git+https://github.com/Uniswap/universal-router-sdk.git" 64 | }, 65 | "bugs": { 66 | "url": "https://github.com/Uniswap/universal-router-sdk/issues" 67 | }, 68 | "homepage": "https://github.com/Uniswap/universal-router-sdk#readme" 69 | } 70 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @ensdomains/=node_modules/@uniswap/universal-router/node_modules/@ensdomains/ 2 | @openzeppelin/=lib/openzeppelin-contracts/ 3 | @uniswap/=node_modules/@uniswap/ 4 | base64-sol/=node_modules/base64-sol/ 5 | ds-test/=lib/forge-std/lib/ds-test/src/ 6 | forge-std/=lib/forge-std/src/ 7 | hardhat/=node_modules/@uniswap/universal-router/node_modules/hardhat/ 8 | openzeppelin-contracts=lib/openzeppelin-contracts/ 9 | permit2/=lib/permit2/ 10 | universal-router/=node_modules/@uniswap/universal-router/contracts 11 | solmate/=lib/solmate/ 12 | v2-core/=node_modules/@uniswap/universal-router/lib/v2-core/ 13 | v3-core/=node_modules/@uniswap/universal-router/lib/v3-core/ 14 | -------------------------------------------------------------------------------- /src/entities/Command.ts: -------------------------------------------------------------------------------- 1 | import { RoutePlanner } from '../utils/routerCommands' 2 | 3 | export type TradeConfig = { 4 | allowRevert: boolean 5 | } 6 | 7 | export enum RouterTradeType { 8 | UniswapTrade = 'UniswapTrade', 9 | NFTTrade = 'NFTTrade', 10 | UnwrapWETH = 'UnwrapWETH', 11 | } 12 | 13 | // interface for entities that can be encoded as a Universal Router command 14 | export interface Command { 15 | tradeType: RouterTradeType 16 | encode(planner: RoutePlanner, config: TradeConfig): void 17 | } 18 | -------------------------------------------------------------------------------- /src/entities/NFTTrade.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import { BigNumber, BigNumberish } from 'ethers' 3 | import { SeaportData } from './protocols/seaport' 4 | import { FoundationData } from './protocols/foundation' 5 | import { NFTXData } from './protocols/nftx' 6 | import { NFT20Data } from './protocols/nft20' 7 | import { RoutePlanner } from '../utils/routerCommands' 8 | import { Command, RouterTradeType, TradeConfig } from './Command' 9 | import { SudoswapData } from './protocols/sudoswap' 10 | import { CryptopunkData } from './protocols/cryptopunk' 11 | import { X2Y2Data } from './protocols/x2y2' 12 | import { ElementData } from './protocols/element-market' 13 | import { LooksRareV2Data } from './protocols/looksRareV2' 14 | 15 | export type SupportedProtocolsData = 16 | | SeaportData 17 | | FoundationData 18 | | NFTXData 19 | | LooksRareV2Data 20 | | X2Y2Data 21 | | CryptopunkData 22 | | NFT20Data 23 | | SudoswapData 24 | | ElementData 25 | 26 | export abstract class NFTTrade implements Command { 27 | readonly tradeType: RouterTradeType = RouterTradeType.NFTTrade 28 | readonly orders: T[] 29 | readonly market: Market 30 | 31 | constructor(market: Market, orders: T[]) { 32 | invariant(orders.length > 0, 'no buy Items') 33 | this.market = market 34 | this.orders = orders 35 | } 36 | 37 | abstract encode(planner: RoutePlanner, config: TradeConfig): void 38 | 39 | abstract getBuyItems(): BuyItem[] 40 | 41 | // optional parameter for the markets that accept ERC20s not just ETH 42 | abstract getTotalPrice(token?: string): BigNumber 43 | } 44 | 45 | export type BuyItem = { 46 | tokenAddress: string 47 | tokenId: BigNumberish 48 | tokenType: TokenType 49 | amount?: BigNumberish // for 1155 50 | } 51 | 52 | export enum Market { 53 | Foundation = 'foundation', 54 | LooksRareV2 = 'looksrareV2', 55 | NFT20 = 'nft20', 56 | NFTX = 'nftx', 57 | Seaport = 'seaport', 58 | Sudoswap = 'Sudoswap', 59 | Cryptopunks = 'cryptopunks', 60 | X2Y2 = 'x2y2', 61 | Element = 'element', 62 | } 63 | 64 | export enum TokenType { 65 | ERC721 = 'ERC721', 66 | ERC1155 = 'ERC1155', 67 | Cryptopunk = 'Cryptopunk', 68 | } 69 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './protocols' 2 | export * from './NFTTrade' 3 | export * from './Command' 4 | -------------------------------------------------------------------------------- /src/entities/protocols/cryptopunk.ts: -------------------------------------------------------------------------------- 1 | import { TradeConfig } from '../Command' 2 | import { NFTTrade, Market, TokenType, BuyItem } from '../NFTTrade' 3 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 4 | import { BigNumber, BigNumberish } from 'ethers' 5 | 6 | export type CryptopunkData = { 7 | tokenId: BigNumberish 8 | recipient: string 9 | value: BigNumberish 10 | } 11 | 12 | export class CryptopunkTrade extends NFTTrade { 13 | public static CRYPTOPUNK_ADDRESS: string = '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb' 14 | 15 | constructor(orders: CryptopunkData[]) { 16 | super(Market.Cryptopunks, orders) 17 | } 18 | 19 | encode(planner: RoutePlanner, config: TradeConfig): void { 20 | for (const item of this.orders) { 21 | planner.addCommand(CommandType.CRYPTOPUNKS, [item.tokenId, item.recipient, item.value], config.allowRevert) 22 | } 23 | } 24 | 25 | getBuyItems(): BuyItem[] { 26 | let buyItems: BuyItem[] = [] 27 | for (const item of this.orders) { 28 | buyItems.push({ 29 | tokenAddress: CryptopunkTrade.CRYPTOPUNK_ADDRESS, 30 | tokenId: item.tokenId, 31 | tokenType: TokenType.Cryptopunk, 32 | }) 33 | } 34 | return buyItems 35 | } 36 | 37 | getTotalPrice(): BigNumber { 38 | let total = BigNumber.from(0) 39 | for (const item of this.orders) { 40 | total = total.add(item.value) 41 | } 42 | return total 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/entities/protocols/element-market.ts: -------------------------------------------------------------------------------- 1 | import abi from '../../../abis/Element.json' 2 | import { Interface } from '@ethersproject/abi' 3 | import { BuyItem, Market, NFTTrade, TokenType } from '../NFTTrade' 4 | import { TradeConfig } from '../Command' 5 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 6 | import { BigNumber } from 'ethers' 7 | import { ZERO_ADDRESS } from '../../utils/constants' 8 | import { ZERO } from '@uniswap/router-sdk' 9 | 10 | export interface Fee { 11 | recipient: string 12 | amount: string 13 | feeData: string 14 | } 15 | 16 | // For now we are not adding ERC1155 support, but we might want it in future 17 | // So structuring the ElementData like this to give us flexibility to support it 18 | type ElementPartialData = { 19 | maker: string 20 | taker: string 21 | expiry: string 22 | nonce: string 23 | erc20Token: string 24 | erc20TokenAmount: string 25 | fees: Fee[] 26 | } 27 | 28 | export type ERC721SellOrder = ElementPartialData & { 29 | nft: string 30 | nftId: string 31 | } 32 | 33 | export type OrderSignature = { 34 | signatureType: number // 0 for 721 and 1 for presigned 35 | v: number 36 | r: string 37 | s: string 38 | } 39 | 40 | export type ElementData = { 41 | order: ERC721SellOrder 42 | signature: OrderSignature 43 | recipient: string 44 | } 45 | 46 | export class ElementTrade extends NFTTrade { 47 | private static ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'.toLowerCase() 48 | public static INTERFACE: Interface = new Interface(abi) 49 | 50 | constructor(orders: ElementData[]) { 51 | super(Market.Element, orders) 52 | } 53 | 54 | encode(planner: RoutePlanner, config: TradeConfig): void { 55 | for (const item of this.orders) { 56 | if (item.order.erc20Token.toLowerCase() != ElementTrade.ETH_ADDRESS) throw new Error('Only ETH supported') 57 | if (item.order.taker != ZERO_ADDRESS && item.recipient.toLowerCase() != item.order.taker.toLowerCase()) 58 | throw new Error('Order has fixed taker') 59 | 60 | const value = this.getOrderPriceIncludingFees(item.order) 61 | 62 | const calldata = ElementTrade.INTERFACE.encodeFunctionData('buyERC721Ex', [ 63 | item.order, 64 | item.signature, 65 | item.order.taker == ZERO_ADDRESS ? item.recipient : item.order.taker, 66 | '0x', // extraData 67 | ]) 68 | 69 | planner.addCommand(CommandType.ELEMENT_MARKET, [value.toString(), calldata], config.allowRevert) 70 | } 71 | } 72 | 73 | getBuyItems(): BuyItem[] { 74 | let buyItems: BuyItem[] = [] 75 | for (const item of this.orders) { 76 | buyItems.push({ 77 | tokenAddress: item.order.nft, 78 | tokenId: item.order.nftId, 79 | tokenType: TokenType.ERC721, 80 | }) 81 | } 82 | return buyItems 83 | } 84 | 85 | getTotalPrice(): BigNumber { 86 | let total = BigNumber.from(0) 87 | for (const item of this.orders) { 88 | total = total.add(this.getOrderPriceIncludingFees(item.order)) 89 | } 90 | return total 91 | } 92 | 93 | /// @dev If there are fees, we have to send an ETH value of the erc20TokenAmount + sum of fees 94 | /// However, for the calldata we have to send the original erc20TokenAmount, so we separate the logic here 95 | /// so we never directly edit the original order object 96 | getOrderPriceIncludingFees(order: ERC721SellOrder): BigNumber { 97 | const nftPrice = BigNumber.from(order.erc20TokenAmount) 98 | if (order.fees) { 99 | return order.fees.reduce((acc, fee) => { 100 | return acc.add(BigNumber.from(fee.amount)) 101 | }, nftPrice) 102 | } 103 | return nftPrice 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/entities/protocols/foundation.ts: -------------------------------------------------------------------------------- 1 | import abi from '../../../abis/Foundation.json' 2 | import { Interface } from '@ethersproject/abi' 3 | import { BuyItem, Market, NFTTrade, TokenType } from '../NFTTrade' 4 | import { TradeConfig } from '../Command' 5 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 6 | import { BigNumber, BigNumberish } from 'ethers' 7 | 8 | export type FoundationData = { 9 | recipient: string 10 | tokenAddress: string 11 | tokenId: BigNumberish 12 | price: BigNumberish 13 | referrer: string // address 14 | } 15 | 16 | export class FoundationTrade extends NFTTrade { 17 | public static INTERFACE: Interface = new Interface(abi) 18 | 19 | constructor(orders: FoundationData[]) { 20 | super(Market.Foundation, orders) 21 | } 22 | 23 | encode(planner: RoutePlanner, config: TradeConfig): void { 24 | for (const item of this.orders) { 25 | const calldata = FoundationTrade.INTERFACE.encodeFunctionData('buyV2', [ 26 | item.tokenAddress, 27 | item.tokenId, 28 | item.price, 29 | item.referrer, 30 | ]) 31 | planner.addCommand( 32 | CommandType.FOUNDATION, 33 | [item.price, calldata, item.recipient, item.tokenAddress, item.tokenId], 34 | config.allowRevert 35 | ) 36 | } 37 | } 38 | 39 | getBuyItems(): BuyItem[] { 40 | let buyItems: BuyItem[] = [] 41 | for (const item of this.orders) { 42 | buyItems.push({ 43 | tokenAddress: item.tokenAddress, 44 | tokenId: item.tokenId, 45 | tokenType: TokenType.ERC721, 46 | }) 47 | } 48 | return buyItems 49 | } 50 | 51 | getTotalPrice(): BigNumber { 52 | let total = BigNumber.from(0) 53 | for (const item of this.orders) { 54 | total = total.add(item.price) 55 | } 56 | return total 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/entities/protocols/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cryptopunk' 2 | export * from './foundation' 3 | export * from './looksRareV2' 4 | export * from './nft20' 5 | export * from './nftx' 6 | export * from './seaport' 7 | export * from './uniswap' 8 | export * from './sudoswap' 9 | export * from './x2y2' 10 | export * from './unwrapWETH' 11 | -------------------------------------------------------------------------------- /src/entities/protocols/looksRareV2.ts: -------------------------------------------------------------------------------- 1 | import abi from '../../../abis/LooksRareV2.json' 2 | import { Interface } from '@ethersproject/abi' 3 | import { BuyItem, Market, NFTTrade, TokenType } from '../NFTTrade' 4 | import { TradeConfig } from '../Command' 5 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 6 | import { BigNumber } from 'ethers' 7 | import { ZERO_ADDRESS } from '../../utils/constants' 8 | 9 | export type MakerOrder = { 10 | quoteType: number 11 | globalNonce: string 12 | subsetNonce: string 13 | orderNonce: string 14 | strategyId: number 15 | collectionType: number 16 | collection: string 17 | currency: string 18 | signer: string 19 | startTime: number 20 | endTime: number 21 | price: string 22 | itemIds: string[] 23 | amounts: string[] 24 | additionalParameters: string 25 | } 26 | 27 | export type TakerOrder = { 28 | recipient: string 29 | additionalParameters: string 30 | } 31 | 32 | export type MerkleProof = { 33 | value: string 34 | position: number 35 | } 36 | 37 | export type MerkleTree = { 38 | root: string 39 | proof: MerkleProof[] 40 | } 41 | 42 | export type LRV2APIOrder = MakerOrder & { 43 | id: string 44 | hash: string 45 | signature: string 46 | createdAt: string 47 | merkleRoot?: string 48 | merkleProof?: MerkleProof[] 49 | status: string 50 | } 51 | 52 | export type LooksRareV2Data = { 53 | apiOrder: LRV2APIOrder 54 | taker: string 55 | } 56 | 57 | export class LooksRareV2Trade extends NFTTrade { 58 | public static INTERFACE: Interface = new Interface(abi) 59 | private static ERC721_ORDER = 0 60 | 61 | constructor(orders: LooksRareV2Data[]) { 62 | super(Market.LooksRareV2, orders) 63 | } 64 | 65 | encode(planner: RoutePlanner, config: TradeConfig): void { 66 | const { takerBids, makerOrders, makerSignatures, totalValue, merkleTrees } = this.refactorAPIData(this.orders) 67 | 68 | let calldata 69 | if (this.orders.length == 1) { 70 | calldata = LooksRareV2Trade.INTERFACE.encodeFunctionData('executeTakerBid', [ 71 | takerBids[0], 72 | makerOrders[0], 73 | makerSignatures[0], 74 | merkleTrees[0], 75 | ZERO_ADDRESS, // affiliate 76 | ]) 77 | } else { 78 | calldata = LooksRareV2Trade.INTERFACE.encodeFunctionData('executeMultipleTakerBids', [ 79 | takerBids, 80 | makerOrders, 81 | makerSignatures, 82 | merkleTrees, 83 | ZERO_ADDRESS, // affiliate 84 | false, // isAtomic (we deal with this in allowRevert) 85 | ]) 86 | } 87 | 88 | planner.addCommand(CommandType.LOOKS_RARE_V2, [totalValue, calldata], config.allowRevert) 89 | } 90 | 91 | getBuyItems(): BuyItem[] { 92 | let buyItems: BuyItem[] = [] 93 | for (const item of this.orders) { 94 | const tokenAddress = item.apiOrder.collection 95 | const tokenType = 96 | item.apiOrder.collectionType == LooksRareV2Trade.ERC721_ORDER ? TokenType.ERC721 : TokenType.ERC1155 97 | for (const tokenId of item.apiOrder.itemIds) 98 | buyItems.push({ 99 | tokenAddress, 100 | tokenId, 101 | tokenType, 102 | }) 103 | } 104 | return buyItems 105 | } 106 | 107 | getTotalPrice(): BigNumber { 108 | let total = BigNumber.from(0) 109 | for (const item of this.orders) { 110 | total = total.add(item.apiOrder.price) 111 | } 112 | return total 113 | } 114 | 115 | private refactorAPIData(orders: LooksRareV2Data[]): { 116 | takerBids: TakerOrder[] 117 | makerOrders: MakerOrder[] 118 | makerSignatures: string[] 119 | totalValue: BigNumber 120 | merkleTrees: MerkleTree[] 121 | } { 122 | let takerBids: TakerOrder[] = [] 123 | let makerOrders: MakerOrder[] = [] 124 | let makerSignatures: string[] = [] 125 | let totalValue: BigNumber = BigNumber.from(0) 126 | let merkleTrees: MerkleTree[] = [] 127 | 128 | orders.forEach((order) => { 129 | makerOrders.push({ ...order.apiOrder }) 130 | 131 | makerSignatures.push(order.apiOrder.signature) 132 | 133 | takerBids.push({ 134 | recipient: order.taker, 135 | additionalParameters: '0x', 136 | }) 137 | 138 | totalValue = totalValue.add(BigNumber.from(order.apiOrder.price)) 139 | 140 | merkleTrees.push({ 141 | root: order.apiOrder.merkleRoot ?? '0x0000000000000000000000000000000000000000000000000000000000000000', 142 | proof: order.apiOrder.merkleProof ?? [], 143 | }) 144 | }) 145 | 146 | return { takerBids, makerOrders, makerSignatures, totalValue, merkleTrees } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/entities/protocols/nft20.ts: -------------------------------------------------------------------------------- 1 | import abi from '../../../abis/NFT20.json' 2 | import { Interface } from '@ethersproject/abi' 3 | import { TradeConfig } from '../Command' 4 | import { NFTTrade, Market, TokenType, BuyItem } from '../NFTTrade' 5 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 6 | import { BigNumber, BigNumberish } from 'ethers' 7 | 8 | export type NFT20Data = { 9 | tokenAddress: string 10 | tokenIds: BigNumberish[] 11 | tokenAmounts: BigNumberish[] 12 | recipient: string 13 | fee: BigNumberish 14 | isV3: boolean 15 | value: BigNumberish 16 | } 17 | 18 | export class NFT20Trade extends NFTTrade { 19 | public static INTERFACE: Interface = new Interface(abi) 20 | 21 | constructor(orders: NFT20Data[]) { 22 | super(Market.NFT20, orders) 23 | } 24 | 25 | encode(planner: RoutePlanner, config: TradeConfig): void { 26 | for (const order of this.orders) { 27 | const calldata = NFT20Trade.INTERFACE.encodeFunctionData('ethForNft', [ 28 | order.tokenAddress, 29 | order.tokenIds, 30 | order.tokenAmounts, 31 | order.recipient, 32 | order.fee, 33 | order.isV3, 34 | ]) 35 | planner.addCommand(CommandType.NFT20, [order.value, calldata], config.allowRevert) 36 | } 37 | } 38 | 39 | getBuyItems(): BuyItem[] { 40 | let buyItems: BuyItem[] = [] 41 | for (const pool of this.orders) { 42 | for (const tokenId of pool.tokenIds) { 43 | buyItems.push({ 44 | tokenAddress: pool.tokenAddress, 45 | tokenId: tokenId, 46 | tokenType: TokenType.ERC721, 47 | }) 48 | } 49 | } 50 | 51 | return buyItems 52 | } 53 | 54 | getTotalPrice(): BigNumber { 55 | let total = BigNumber.from(0) 56 | for (const item of this.orders) { 57 | total = total.add(item.value) 58 | } 59 | return total 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/entities/protocols/nftx.ts: -------------------------------------------------------------------------------- 1 | import abi from '../../../abis/NFTXZap.json' 2 | import { Interface } from '@ethersproject/abi' 3 | import { BuyItem, Market, NFTTrade, TokenType } from '../NFTTrade' 4 | import { TradeConfig } from '../Command' 5 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 6 | import { BigNumber, BigNumberish } from 'ethers' 7 | 8 | export type NFTXData = { 9 | recipient: string 10 | vaultId: BigNumberish 11 | tokenAddress: string 12 | tokenIds: BigNumberish[] 13 | value: BigNumber 14 | swapCalldata: string 15 | } 16 | 17 | export class NFTXTrade extends NFTTrade { 18 | public static INTERFACE: Interface = new Interface(abi) 19 | 20 | constructor(orders: NFTXData[]) { 21 | super(Market.NFTX, orders) 22 | } 23 | 24 | encode(planner: RoutePlanner, config: TradeConfig): void { 25 | for (const order of this.orders) { 26 | const calldata = NFTXTrade.INTERFACE.encodeFunctionData('buyAndRedeem', [ 27 | order.vaultId, 28 | order.tokenIds.length, 29 | order.tokenIds, 30 | order.swapCalldata, 31 | order.recipient, 32 | ]) 33 | 34 | planner.addCommand(CommandType.NFTX, [order.value, calldata], config.allowRevert) 35 | } 36 | } 37 | 38 | getBuyItems(): BuyItem[] { 39 | let buyItems: BuyItem[] = [] 40 | for (const order of this.orders) { 41 | for (const tokenId of order.tokenIds) { 42 | buyItems.push({ 43 | tokenAddress: order.tokenAddress, 44 | tokenId: tokenId, 45 | tokenType: TokenType.ERC721, 46 | }) 47 | } 48 | } 49 | return buyItems 50 | } 51 | 52 | getTotalPrice(): BigNumber { 53 | let total = BigNumber.from(0) 54 | for (const item of this.orders) { 55 | total = total.add(item.value) 56 | } 57 | return total 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/entities/protocols/seaport.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from 'ethers' 2 | import { Interface } from '@ethersproject/abi' 3 | import abi from '../../../abis/Seaport.json' 4 | import { BuyItem, Market, NFTTrade, TokenType } from '../NFTTrade' 5 | import { TradeConfig } from '../Command' 6 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 7 | import { encodeInputTokenOptions, Permit2Permit } from '../../utils/inputTokens' 8 | import { ETH_ADDRESS } from '../../utils/constants' 9 | 10 | export type SeaportData = { 11 | items: Order[] 12 | recipient: string // address 13 | protocolAddress: string 14 | inputTokenProcessing?: InputTokenProcessing[] 15 | } 16 | 17 | export type InputTokenProcessing = { 18 | token: string 19 | permit2Permit?: Permit2Permit 20 | protocolApproval: boolean 21 | permit2TransferFrom: boolean 22 | } 23 | 24 | export type FulfillmentComponent = { 25 | orderIndex: BigNumberish 26 | itemIndex: BigNumberish 27 | } 28 | 29 | export type OfferItem = { 30 | itemType: BigNumberish // enum 31 | token: string // address 32 | identifierOrCriteria: BigNumberish 33 | startAmount: BigNumberish 34 | endAmount: BigNumberish 35 | } 36 | 37 | export type ConsiderationItem = OfferItem & { 38 | recipient: string 39 | } 40 | 41 | export type Order = { 42 | parameters: OrderParameters 43 | signature: string 44 | } 45 | 46 | type OrderParameters = { 47 | offerer: string // address, 48 | offer: OfferItem[] 49 | consideration: ConsiderationItem[] 50 | orderType: BigNumberish // enum 51 | startTime: BigNumberish 52 | endTime: BigNumberish 53 | zoneHash: string // bytes32 54 | zone: string // address 55 | salt: BigNumberish 56 | conduitKey: string // bytes32, 57 | totalOriginalConsiderationItems: BigNumberish 58 | } 59 | 60 | export type AdvancedOrder = Order & { 61 | numerator: BigNumber // uint120 62 | denominator: BigNumber // uint120 63 | extraData: string // bytes 64 | } 65 | 66 | export class SeaportTrade extends NFTTrade { 67 | public static INTERFACE: Interface = new Interface(abi) 68 | public static OPENSEA_CONDUIT_KEY: string = '0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000' 69 | 70 | constructor(orders: SeaportData[]) { 71 | super(Market.Seaport, orders) 72 | } 73 | 74 | encode(planner: RoutePlanner, config: TradeConfig): void { 75 | for (const order of this.orders) { 76 | let advancedOrders: AdvancedOrder[] = [] 77 | let orderFulfillments: FulfillmentComponent[][] = order.items.map((_, index) => [ 78 | { orderIndex: index, itemIndex: 0 }, 79 | ]) 80 | let considerationFulFillments: FulfillmentComponent[][] = this.getConsiderationFulfillments(order.items) 81 | 82 | for (const item of order.items) { 83 | const { advancedOrder } = this.getAdvancedOrderParams(item) 84 | advancedOrders.push(advancedOrder) 85 | } 86 | 87 | let calldata: string 88 | if (advancedOrders.length == 1) { 89 | calldata = SeaportTrade.INTERFACE.encodeFunctionData('fulfillAdvancedOrder', [ 90 | advancedOrders[0], 91 | [], 92 | SeaportTrade.OPENSEA_CONDUIT_KEY, 93 | order.recipient, 94 | ]) 95 | } else { 96 | calldata = SeaportTrade.INTERFACE.encodeFunctionData('fulfillAvailableAdvancedOrders', [ 97 | advancedOrders, 98 | [], 99 | orderFulfillments, 100 | considerationFulFillments, 101 | SeaportTrade.OPENSEA_CONDUIT_KEY, 102 | order.recipient, 103 | 100, // TODO: look into making this a better number 104 | ]) 105 | } 106 | 107 | if (!!order.inputTokenProcessing) { 108 | for (const inputToken of order.inputTokenProcessing) 109 | encodeInputTokenOptions(planner, { 110 | approval: inputToken.protocolApproval 111 | ? { token: inputToken.token, protocol: order.protocolAddress } 112 | : undefined, 113 | permit2Permit: inputToken.permit2Permit, 114 | permit2TransferFrom: inputToken.permit2TransferFrom 115 | ? { token: inputToken.token, amount: this.getTotalOrderPrice(order, inputToken.token).toString() } 116 | : undefined, 117 | }) 118 | } 119 | 120 | planner.addCommand( 121 | this.commandMap(order.protocolAddress), 122 | [this.getTotalOrderPrice(order, ETH_ADDRESS).toString(), calldata], 123 | config.allowRevert 124 | ) 125 | } 126 | } 127 | 128 | getBuyItems(): BuyItem[] { 129 | let buyItems: BuyItem[] = [] 130 | for (const order of this.orders) { 131 | for (const item of order.items) { 132 | for (const offer of item.parameters.offer) { 133 | buyItems.push({ 134 | tokenAddress: offer.token, 135 | tokenId: offer.identifierOrCriteria, 136 | tokenType: TokenType.ERC721, 137 | }) 138 | } 139 | } 140 | } 141 | return buyItems 142 | } 143 | 144 | getInputTokens(): Set { 145 | let inputTokens = new Set() 146 | for (const order of this.orders) { 147 | for (const item of order.items) { 148 | for (const consideration of item.parameters.consideration) { 149 | const token = consideration.token.toLowerCase() 150 | inputTokens.add(token) 151 | } 152 | } 153 | } 154 | return inputTokens 155 | } 156 | 157 | getTotalOrderPrice(order: SeaportData, token: string = ETH_ADDRESS): BigNumber { 158 | let totalOrderPrice = BigNumber.from(0) 159 | for (const item of order.items) { 160 | totalOrderPrice = totalOrderPrice.add(this.calculateValue(item.parameters.consideration, token)) 161 | } 162 | return totalOrderPrice 163 | } 164 | 165 | getTotalPrice(token: string = ETH_ADDRESS): BigNumber { 166 | let totalPrice = BigNumber.from(0) 167 | for (const order of this.orders) { 168 | for (const item of order.items) { 169 | totalPrice = totalPrice.add(this.calculateValue(item.parameters.consideration, token)) 170 | } 171 | } 172 | return totalPrice 173 | } 174 | 175 | private commandMap(protocolAddress: string): CommandType { 176 | switch (protocolAddress.toLowerCase()) { 177 | case '0x00000000000000adc04c56bf30ac9d3c0aaf14dc': // Seaport v1.5 178 | return CommandType.SEAPORT_V1_5 179 | case '0x00000000000001ad428e4906ae43d8f9852d0dd6': // Seaport v1.4 180 | return CommandType.SEAPORT_V1_4 181 | default: 182 | throw new Error('unsupported Seaport address') 183 | } 184 | } 185 | 186 | private getConsiderationFulfillments(protocolDatas: Order[]): FulfillmentComponent[][] { 187 | let considerationFulfillments: FulfillmentComponent[][] = [] 188 | const considerationRecipients: string[] = [] 189 | 190 | for (const i in protocolDatas) { 191 | const protocolData = protocolDatas[i] 192 | 193 | for (const j in protocolData.parameters.consideration) { 194 | const item = protocolData.parameters.consideration[j] 195 | 196 | if (considerationRecipients.findIndex((x) => x === item.recipient) === -1) { 197 | considerationRecipients.push(item.recipient) 198 | } 199 | 200 | const recipientIndex = considerationRecipients.findIndex((x) => x === item.recipient) 201 | 202 | if (!considerationFulfillments[recipientIndex]) { 203 | considerationFulfillments.push([ 204 | { 205 | orderIndex: i, 206 | itemIndex: j, 207 | }, 208 | ]) 209 | } else { 210 | considerationFulfillments[recipientIndex].push({ 211 | orderIndex: i, 212 | itemIndex: j, 213 | }) 214 | } 215 | } 216 | } 217 | return considerationFulfillments 218 | } 219 | 220 | private getAdvancedOrderParams(data: Order): { advancedOrder: AdvancedOrder } { 221 | const advancedOrder = { 222 | parameters: data.parameters, 223 | numerator: BigNumber.from('1'), 224 | denominator: BigNumber.from('1'), 225 | signature: data.signature, 226 | extraData: '0x00', 227 | } 228 | return { advancedOrder } 229 | } 230 | 231 | private calculateValue(considerations: ConsiderationItem[], token: string): BigNumber { 232 | return considerations.reduce( 233 | (amt: BigNumber, consideration: ConsiderationItem) => 234 | consideration.token.toLowerCase() == token.toLowerCase() ? amt.add(consideration.startAmount) : amt, 235 | BigNumber.from(0) 236 | ) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/entities/protocols/sudoswap.ts: -------------------------------------------------------------------------------- 1 | import abi from '../../../abis/Sudoswap.json' 2 | import { Interface } from '@ethersproject/abi' 3 | import { BuyItem, Market, NFTTrade, TokenType } from '../NFTTrade' 4 | import { TradeConfig } from '../Command' 5 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 6 | import { BigNumber, BigNumberish } from 'ethers' 7 | 8 | type PairSwap = { 9 | swapInfo: { 10 | pair: string // address 11 | nftIds: BigNumberish[] 12 | } 13 | tokenAddress: string // address 14 | maxCost: BigNumberish 15 | } 16 | 17 | export type SudoswapData = { 18 | swaps: PairSwap[] 19 | nftRecipient: string 20 | ethRecipient: string 21 | deadline: BigNumberish 22 | } 23 | 24 | export class SudoswapTrade extends NFTTrade { 25 | public static INTERFACE: Interface = new Interface(abi) 26 | 27 | constructor(orders: SudoswapData[]) { 28 | super(Market.Sudoswap, orders) 29 | } 30 | 31 | encode(planner: RoutePlanner, config: TradeConfig): void { 32 | for (const order of this.orders) { 33 | const calldata = SudoswapTrade.INTERFACE.encodeFunctionData('robustSwapETHForSpecificNFTs', [ 34 | order.swaps.map((swap) => { 35 | return { swapInfo: swap.swapInfo, maxCost: swap.maxCost } 36 | }), 37 | order.ethRecipient, 38 | order.nftRecipient, 39 | order.deadline, 40 | ]) 41 | const value = order.swaps.reduce((prevVal, swap) => { 42 | return prevVal.add(swap.maxCost) 43 | }, BigNumber.from(0)) 44 | planner.addCommand(CommandType.SUDOSWAP, [value, calldata], config.allowRevert) 45 | } 46 | } 47 | 48 | getBuyItems(): BuyItem[] { 49 | let buyItems: BuyItem[] = [] 50 | for (const order of this.orders) { 51 | for (const swap of order.swaps) { 52 | for (const tokenId of swap.swapInfo.nftIds) { 53 | buyItems.push({ 54 | tokenAddress: swap.tokenAddress, 55 | tokenId, 56 | tokenType: TokenType.ERC721, 57 | }) 58 | } 59 | } 60 | } 61 | return buyItems 62 | } 63 | 64 | getTotalPrice(): BigNumber { 65 | let total = BigNumber.from(0) 66 | for (const order of this.orders) { 67 | for (const swap of order.swaps) { 68 | total = total.add(swap.maxCost) 69 | } 70 | } 71 | return total 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/entities/protocols/uniswap.ts: -------------------------------------------------------------------------------- 1 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 2 | import { Trade as V2Trade, Pair } from '@uniswap/v2-sdk' 3 | import { Trade as V3Trade, Pool, encodeRouteToPath } from '@uniswap/v3-sdk' 4 | import { 5 | Trade as RouterTrade, 6 | MixedRouteTrade, 7 | Protocol, 8 | IRoute, 9 | RouteV2, 10 | RouteV3, 11 | MixedRouteSDK, 12 | MixedRoute, 13 | SwapOptions as RouterSwapOptions, 14 | getOutputOfPools, 15 | encodeMixedRouteToPath, 16 | partitionMixedRouteByProtocol, 17 | } from '@uniswap/router-sdk' 18 | import { Permit2Permit } from '../../utils/inputTokens' 19 | import { Currency, TradeType, CurrencyAmount, Percent } from '@uniswap/sdk-core' 20 | import { Command, RouterTradeType, TradeConfig } from '../Command' 21 | import { SENDER_AS_RECIPIENT, ROUTER_AS_RECIPIENT, CONTRACT_BALANCE, ETH_ADDRESS } from '../../utils/constants' 22 | import { encodeFeeBips } from '../../utils/numbers' 23 | import { BigNumber, BigNumberish } from 'ethers' 24 | 25 | export type FlatFeeOptions = { 26 | amount: BigNumberish 27 | recipient: string 28 | } 29 | 30 | // the existing router permit object doesn't include enough data for permit2 31 | // so we extend swap options with the permit2 permit 32 | // when safe mode is enabled, the SDK will add an extra ETH sweep for security 33 | // when useRouterBalance is enabled the SDK will use the balance in the router for the swap 34 | export type SwapOptions = Omit & { 35 | useRouterBalance?: boolean 36 | inputTokenPermit?: Permit2Permit 37 | flatFee?: FlatFeeOptions 38 | safeMode?: boolean 39 | } 40 | 41 | const REFUND_ETH_PRICE_IMPACT_THRESHOLD = new Percent(50, 100) 42 | 43 | interface Swap { 44 | route: IRoute 45 | inputAmount: CurrencyAmount 46 | outputAmount: CurrencyAmount 47 | } 48 | 49 | // Wrapper for uniswap router-sdk trade entity to encode swaps for Universal Router 50 | // also translates trade objects from previous (v2, v3) SDKs 51 | export class UniswapTrade implements Command { 52 | readonly tradeType: RouterTradeType = RouterTradeType.UniswapTrade 53 | readonly payerIsUser: boolean 54 | 55 | constructor(public trade: RouterTrade, public options: SwapOptions) { 56 | if (!!options.fee && !!options.flatFee) throw new Error('Only one fee option permitted') 57 | 58 | if (this.inputRequiresWrap) this.payerIsUser = false 59 | else if (this.options.useRouterBalance) this.payerIsUser = false 60 | else this.payerIsUser = true 61 | } 62 | 63 | get inputRequiresWrap(): boolean { 64 | return this.trade.inputAmount.currency.isNative 65 | } 66 | 67 | encode(planner: RoutePlanner, _config: TradeConfig): void { 68 | // If the input currency is the native currency, we need to wrap it with the router as the recipient 69 | if (this.inputRequiresWrap) { 70 | // TODO: optimize if only one v2 pool we can directly send this to the pool 71 | planner.addCommand(CommandType.WRAP_ETH, [ 72 | ROUTER_AS_RECIPIENT, 73 | this.trade.maximumAmountIn(this.options.slippageTolerance).quotient.toString(), 74 | ]) 75 | } 76 | // The overall recipient at the end of the trade, SENDER_AS_RECIPIENT uses the msg.sender 77 | this.options.recipient = this.options.recipient ?? SENDER_AS_RECIPIENT 78 | 79 | // flag for whether we want to perform slippage check on aggregate output of multiple routes 80 | // 1. when there are >2 exact input trades. this is only a heuristic, 81 | // as it's still more gas-expensive even in this case, but has benefits 82 | // in that the reversion probability is lower 83 | const performAggregatedSlippageCheck = 84 | this.trade.tradeType === TradeType.EXACT_INPUT && this.trade.routes.length > 2 85 | const outputIsNative = this.trade.outputAmount.currency.isNative 86 | const routerMustCustody = performAggregatedSlippageCheck || outputIsNative || hasFeeOption(this.options) 87 | 88 | for (const swap of this.trade.swaps) { 89 | switch (swap.route.protocol) { 90 | case Protocol.V2: 91 | addV2Swap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody) 92 | break 93 | case Protocol.V3: 94 | addV3Swap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody) 95 | break 96 | case Protocol.MIXED: 97 | addMixedSwap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody) 98 | break 99 | default: 100 | throw new Error('UNSUPPORTED_TRADE_PROTOCOL') 101 | } 102 | } 103 | 104 | let minimumAmountOut: BigNumber = BigNumber.from( 105 | this.trade.minimumAmountOut(this.options.slippageTolerance).quotient.toString() 106 | ) 107 | 108 | // The router custodies for 3 reasons: to unwrap, to take a fee, and/or to do a slippage check 109 | if (routerMustCustody) { 110 | // If there is a fee, that percentage is sent to the fee recipient 111 | // In the case where ETH is the output currency, the fee is taken in WETH (for gas reasons) 112 | if (!!this.options.fee) { 113 | const feeBips = encodeFeeBips(this.options.fee.fee) 114 | planner.addCommand(CommandType.PAY_PORTION, [ 115 | this.trade.outputAmount.currency.wrapped.address, 116 | this.options.fee.recipient, 117 | feeBips, 118 | ]) 119 | 120 | // If the trade is exact output, and a fee was taken, we must adjust the amount out to be the amount after the fee 121 | // Otherwise we continue as expected with the trade's normal expected output 122 | if (this.trade.tradeType === TradeType.EXACT_OUTPUT) { 123 | minimumAmountOut = minimumAmountOut.sub(minimumAmountOut.mul(feeBips).div(10000)) 124 | } 125 | } 126 | 127 | // If there is a flat fee, that absolute amount is sent to the fee recipient 128 | // In the case where ETH is the output currency, the fee is taken in WETH (for gas reasons) 129 | if (!!this.options.flatFee) { 130 | const feeAmount = this.options.flatFee.amount 131 | if (minimumAmountOut.lt(feeAmount)) throw new Error('Flat fee amount greater than minimumAmountOut') 132 | 133 | planner.addCommand(CommandType.TRANSFER, [ 134 | this.trade.outputAmount.currency.wrapped.address, 135 | this.options.flatFee.recipient, 136 | feeAmount, 137 | ]) 138 | 139 | // If the trade is exact output, and a fee was taken, we must adjust the amount out to be the amount after the fee 140 | // Otherwise we continue as expected with the trade's normal expected output 141 | if (this.trade.tradeType === TradeType.EXACT_OUTPUT) { 142 | minimumAmountOut = minimumAmountOut.sub(feeAmount) 143 | } 144 | } 145 | 146 | // The remaining tokens that need to be sent to the user after the fee is taken will be caught 147 | // by this if-else clause. 148 | if (outputIsNative) { 149 | planner.addCommand(CommandType.UNWRAP_WETH, [this.options.recipient, minimumAmountOut]) 150 | } else { 151 | planner.addCommand(CommandType.SWEEP, [ 152 | this.trade.outputAmount.currency.wrapped.address, 153 | this.options.recipient, 154 | minimumAmountOut, 155 | ]) 156 | } 157 | } 158 | 159 | if (this.inputRequiresWrap && (this.trade.tradeType === TradeType.EXACT_OUTPUT || riskOfPartialFill(this.trade))) { 160 | // for exactOutput swaps that take native currency as input 161 | // we need to send back the change to the user 162 | planner.addCommand(CommandType.UNWRAP_WETH, [this.options.recipient, 0]) 163 | } 164 | 165 | if (this.options.safeMode) planner.addCommand(CommandType.SWEEP, [ETH_ADDRESS, this.options.recipient, 0]) 166 | } 167 | } 168 | 169 | // encode a uniswap v2 swap 170 | function addV2Swap( 171 | planner: RoutePlanner, 172 | { route, inputAmount, outputAmount }: Swap, 173 | tradeType: TradeType, 174 | options: SwapOptions, 175 | payerIsUser: boolean, 176 | routerMustCustody: boolean 177 | ): void { 178 | const trade = new V2Trade( 179 | route as RouteV2, 180 | tradeType == TradeType.EXACT_INPUT ? inputAmount : outputAmount, 181 | tradeType 182 | ) 183 | 184 | if (tradeType == TradeType.EXACT_INPUT) { 185 | planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ 186 | // if native, we have to unwrap so keep in the router for now 187 | routerMustCustody ? ROUTER_AS_RECIPIENT : options.recipient, 188 | trade.maximumAmountIn(options.slippageTolerance).quotient.toString(), 189 | trade.minimumAmountOut(options.slippageTolerance).quotient.toString(), 190 | route.path.map((pool) => pool.address), 191 | payerIsUser, 192 | ]) 193 | } else if (tradeType == TradeType.EXACT_OUTPUT) { 194 | planner.addCommand(CommandType.V2_SWAP_EXACT_OUT, [ 195 | routerMustCustody ? ROUTER_AS_RECIPIENT : options.recipient, 196 | trade.minimumAmountOut(options.slippageTolerance).quotient.toString(), 197 | trade.maximumAmountIn(options.slippageTolerance).quotient.toString(), 198 | route.path.map((pool) => pool.address), 199 | payerIsUser, 200 | ]) 201 | } 202 | } 203 | 204 | // encode a uniswap v3 swap 205 | function addV3Swap( 206 | planner: RoutePlanner, 207 | { route, inputAmount, outputAmount }: Swap, 208 | tradeType: TradeType, 209 | options: SwapOptions, 210 | payerIsUser: boolean, 211 | routerMustCustody: boolean 212 | ): void { 213 | const trade = V3Trade.createUncheckedTrade({ 214 | route: route as RouteV3, 215 | inputAmount, 216 | outputAmount, 217 | tradeType, 218 | }) 219 | 220 | const path = encodeRouteToPath(route as RouteV3, trade.tradeType === TradeType.EXACT_OUTPUT) 221 | if (tradeType == TradeType.EXACT_INPUT) { 222 | planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ 223 | routerMustCustody ? ROUTER_AS_RECIPIENT : options.recipient, 224 | trade.maximumAmountIn(options.slippageTolerance).quotient.toString(), 225 | trade.minimumAmountOut(options.slippageTolerance).quotient.toString(), 226 | path, 227 | payerIsUser, 228 | ]) 229 | } else if (tradeType == TradeType.EXACT_OUTPUT) { 230 | planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [ 231 | routerMustCustody ? ROUTER_AS_RECIPIENT : options.recipient, 232 | trade.minimumAmountOut(options.slippageTolerance).quotient.toString(), 233 | trade.maximumAmountIn(options.slippageTolerance).quotient.toString(), 234 | path, 235 | payerIsUser, 236 | ]) 237 | } 238 | } 239 | 240 | // encode a mixed route swap, i.e. including both v2 and v3 pools 241 | function addMixedSwap( 242 | planner: RoutePlanner, 243 | swap: Swap, 244 | tradeType: TradeType, 245 | options: SwapOptions, 246 | payerIsUser: boolean, 247 | routerMustCustody: boolean 248 | ): void { 249 | const { route, inputAmount, outputAmount } = swap 250 | const tradeRecipient = routerMustCustody ? ROUTER_AS_RECIPIENT : options.recipient 251 | 252 | // single hop, so it can be reduced to plain v2 or v3 swap logic 253 | if (route.pools.length === 1) { 254 | if (route.pools[0] instanceof Pool) { 255 | return addV3Swap(planner, swap, tradeType, options, payerIsUser, routerMustCustody) 256 | } else if (route.pools[0] instanceof Pair) { 257 | return addV2Swap(planner, swap, tradeType, options, payerIsUser, routerMustCustody) 258 | } else { 259 | throw new Error('Invalid route type') 260 | } 261 | } 262 | 263 | const trade = MixedRouteTrade.createUncheckedTrade({ 264 | route: route as MixedRoute, 265 | inputAmount, 266 | outputAmount, 267 | tradeType, 268 | }) 269 | 270 | const amountIn = trade.maximumAmountIn(options.slippageTolerance, inputAmount).quotient.toString() 271 | const amountOut = trade.minimumAmountOut(options.slippageTolerance, outputAmount).quotient.toString() 272 | 273 | // logic from 274 | // https://github.com/Uniswap/router-sdk/blob/d8eed164e6c79519983844ca8b6a3fc24ebcb8f8/src/swapRouter.ts#L276 275 | const sections = partitionMixedRouteByProtocol(route as MixedRoute) 276 | const isLastSectionInRoute = (i: number) => { 277 | return i === sections.length - 1 278 | } 279 | 280 | let outputToken 281 | let inputToken = route.input.wrapped 282 | 283 | for (let i = 0; i < sections.length; i++) { 284 | const section = sections[i] 285 | /// Now, we get output of this section 286 | outputToken = getOutputOfPools(section, inputToken) 287 | 288 | const newRouteOriginal = new MixedRouteSDK( 289 | [...section], 290 | section[0].token0.equals(inputToken) ? section[0].token0 : section[0].token1, 291 | outputToken 292 | ) 293 | const newRoute = new MixedRoute(newRouteOriginal) 294 | 295 | /// Previous output is now input 296 | inputToken = outputToken 297 | 298 | const mixedRouteIsAllV3 = (route: MixedRouteSDK) => { 299 | return route.pools.every((pool) => pool instanceof Pool) 300 | } 301 | 302 | if (mixedRouteIsAllV3(newRoute)) { 303 | const path: string = encodeMixedRouteToPath(newRoute) 304 | 305 | planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ 306 | // if not last section: send tokens directly to the first v2 pair of the next section 307 | // note: because of the partitioning function we can be sure that the next section is v2 308 | isLastSectionInRoute(i) ? tradeRecipient : (sections[i + 1][0] as Pair).liquidityToken.address, 309 | i == 0 ? amountIn : CONTRACT_BALANCE, // amountIn 310 | !isLastSectionInRoute(i) ? 0 : amountOut, // amountOut 311 | path, // path 312 | payerIsUser && i === 0, // payerIsUser 313 | ]) 314 | } else { 315 | planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ 316 | isLastSectionInRoute(i) ? tradeRecipient : ROUTER_AS_RECIPIENT, // recipient 317 | i === 0 ? amountIn : CONTRACT_BALANCE, // amountIn 318 | !isLastSectionInRoute(i) ? 0 : amountOut, // amountOutMin 319 | newRoute.path.map((pool) => pool.address), // path 320 | payerIsUser && i === 0, 321 | ]) 322 | } 323 | } 324 | } 325 | 326 | // if price impact is very high, there's a chance of hitting max/min prices resulting in a partial fill of the swap 327 | function riskOfPartialFill(trade: RouterTrade): boolean { 328 | return trade.priceImpact.greaterThan(REFUND_ETH_PRICE_IMPACT_THRESHOLD) 329 | } 330 | 331 | function hasFeeOption(swapOptions: SwapOptions): boolean { 332 | return !!swapOptions.fee || !!swapOptions.flatFee 333 | } 334 | -------------------------------------------------------------------------------- /src/entities/protocols/unwrapWETH.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import { BigNumberish } from 'ethers' 3 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 4 | import { encodeInputTokenOptions, Permit2Permit } from '../../utils/inputTokens' 5 | import { Command, RouterTradeType, TradeConfig } from '../Command' 6 | import { ROUTER_AS_RECIPIENT, WETH_ADDRESS } from '../../utils/constants' 7 | 8 | export class UnwrapWETH implements Command { 9 | readonly tradeType: RouterTradeType = RouterTradeType.UnwrapWETH 10 | readonly permit2Data: Permit2Permit 11 | readonly wethAddress: string 12 | readonly amount: BigNumberish 13 | 14 | constructor(amount: BigNumberish, chainId: number, permit2?: Permit2Permit) { 15 | this.wethAddress = WETH_ADDRESS(chainId) 16 | this.amount = amount 17 | 18 | if (!!permit2) { 19 | invariant( 20 | permit2.details.token.toLowerCase() === this.wethAddress.toLowerCase(), 21 | `must be permitting WETH address: ${this.wethAddress}` 22 | ) 23 | invariant(permit2.details.amount >= amount, `Did not permit enough WETH for unwrapWETH transaction`) 24 | this.permit2Data = permit2 25 | } 26 | } 27 | 28 | encode(planner: RoutePlanner, _: TradeConfig): void { 29 | encodeInputTokenOptions(planner, { 30 | permit2Permit: this.permit2Data, 31 | permit2TransferFrom: { 32 | token: this.wethAddress, 33 | amount: this.amount.toString(), 34 | }, 35 | }) 36 | planner.addCommand(CommandType.UNWRAP_WETH, [ROUTER_AS_RECIPIENT, this.amount]) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/entities/protocols/x2y2.ts: -------------------------------------------------------------------------------- 1 | import abi from '../../../abis/X2Y2.json' 2 | import { Interface } from '@ethersproject/abi' 3 | import { BuyItem, Market, NFTTrade, TokenType } from '../NFTTrade' 4 | import { TradeConfig } from '../Command' 5 | import { RoutePlanner, CommandType } from '../../utils/routerCommands' 6 | import { BigNumber, BigNumberish } from 'ethers' 7 | 8 | type X2Y2PartialData = { 9 | signedInput: string 10 | recipient: string 11 | tokenAddress: string 12 | tokenId: BigNumberish 13 | price: BigNumberish 14 | } 15 | 16 | export type X2Y2_721_Data = X2Y2PartialData & { 17 | tokenType: TokenType.ERC721 18 | } 19 | 20 | export type X2Y2_1155_Data = X2Y2PartialData & { 21 | tokenType: TokenType.ERC1155 22 | tokenAmount: BigNumberish 23 | } 24 | 25 | export type X2Y2Data = X2Y2_721_Data | X2Y2_1155_Data 26 | 27 | export class X2Y2Trade extends NFTTrade { 28 | public static INTERFACE: Interface = new Interface(abi) 29 | 30 | constructor(orders: X2Y2Data[]) { 31 | super(Market.X2Y2, orders) 32 | } 33 | 34 | encode(planner: RoutePlanner, config: TradeConfig): void { 35 | for (const item of this.orders) { 36 | const functionSelector = X2Y2Trade.INTERFACE.getSighash(X2Y2Trade.INTERFACE.getFunction('run')) 37 | const calldata = functionSelector + item.signedInput.slice(2) 38 | 39 | if (item.tokenType == TokenType.ERC721) { 40 | planner.addCommand( 41 | CommandType.X2Y2_721, 42 | [item.price, calldata, item.recipient, item.tokenAddress, item.tokenId], 43 | config.allowRevert 44 | ) 45 | } else if (item.tokenType == TokenType.ERC1155) { 46 | planner.addCommand( 47 | CommandType.X2Y2_1155, 48 | [item.price, calldata, item.recipient, item.tokenAddress, item.tokenId, item.tokenAmount], 49 | config.allowRevert 50 | ) 51 | } 52 | } 53 | } 54 | 55 | getBuyItems(): BuyItem[] { 56 | let buyItems: BuyItem[] = [] 57 | for (const item of this.orders) { 58 | buyItems.push({ 59 | tokenAddress: item.tokenAddress, 60 | tokenId: item.tokenId, 61 | tokenType: item.tokenType, 62 | }) 63 | } 64 | return buyItems 65 | } 66 | 67 | getTotalPrice(): BigNumber { 68 | let total = BigNumber.from(0) 69 | for (const item of this.orders) { 70 | total = total.add(item.price) 71 | } 72 | return total 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { SwapRouter } from './swapRouter' 2 | export * from './entities' 3 | export * from './utils/routerTradeAdapter' 4 | export { RoutePlanner, CommandType } from './utils/routerCommands' 5 | export { 6 | UNIVERSAL_ROUTER_ADDRESS, 7 | UNIVERSAL_ROUTER_CREATION_BLOCK, 8 | PERMIT2_ADDRESS, 9 | ROUTER_AS_RECIPIENT, 10 | WETH_ADDRESS, 11 | } from './utils/constants' 12 | -------------------------------------------------------------------------------- /src/swapRouter.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import { abi } from '@uniswap/universal-router/artifacts/contracts/UniversalRouter.sol/UniversalRouter.json' 3 | import { Interface } from '@ethersproject/abi' 4 | import { BigNumber, BigNumberish } from 'ethers' 5 | import { MethodParameters } from '@uniswap/v3-sdk' 6 | import { Trade as RouterTrade } from '@uniswap/router-sdk' 7 | import { Currency, TradeType } from '@uniswap/sdk-core' 8 | import { Command, RouterTradeType } from './entities/Command' 9 | import { Market, NFTTrade, SupportedProtocolsData } from './entities/NFTTrade' 10 | import { UniswapTrade, SwapOptions } from './entities/protocols/uniswap' 11 | import { UnwrapWETH } from './entities/protocols/unwrapWETH' 12 | import { CommandType, RoutePlanner } from './utils/routerCommands' 13 | import { encodePermit } from './utils/inputTokens' 14 | import { ROUTER_AS_RECIPIENT, SENDER_AS_RECIPIENT, ETH_ADDRESS } from './utils/constants' 15 | import { SeaportTrade } from './entities' 16 | 17 | export type SwapRouterConfig = { 18 | sender?: string // address 19 | deadline?: BigNumberish 20 | } 21 | 22 | type SupportedNFTTrade = NFTTrade 23 | 24 | export abstract class SwapRouter { 25 | public static INTERFACE: Interface = new Interface(abi) 26 | 27 | public static swapCallParameters(trades: Command[] | Command, config: SwapRouterConfig = {}): MethodParameters { 28 | if (!Array.isArray(trades)) trades = [trades] 29 | 30 | const nftTrades = trades.filter((trade, _, []) => trade.hasOwnProperty('market')) as SupportedNFTTrade[] 31 | const allowRevert = nftTrades.length == 1 && nftTrades[0].orders.length == 1 ? false : true 32 | const planner = new RoutePlanner() 33 | 34 | // track value flow to require the right amount of native value 35 | let currentNativeValueInRouter = BigNumber.from(0) 36 | let transactionValue = BigNumber.from(0) 37 | 38 | // tracks the input tokens (and ETH) used to buy NFTs to allow us to sweep 39 | let nftInputTokens = new Set() 40 | 41 | for (const trade of trades) { 42 | /** 43 | * is NFTTrade 44 | */ 45 | if (trade.tradeType == RouterTradeType.NFTTrade) { 46 | const nftTrade = trade as SupportedNFTTrade 47 | nftTrade.encode(planner, { allowRevert }) 48 | const tradePrice = nftTrade.getTotalPrice() 49 | 50 | if (nftTrade.market == Market.Seaport) { 51 | const seaportTrade = nftTrade as SeaportTrade 52 | const seaportInputTokens = seaportTrade.getInputTokens() 53 | seaportInputTokens.forEach((inputToken) => { 54 | nftInputTokens.add(inputToken) 55 | }) 56 | } else { 57 | nftInputTokens.add(ETH_ADDRESS) 58 | } 59 | 60 | // send enough native value to contract for NFT purchase 61 | if (currentNativeValueInRouter.lt(tradePrice)) { 62 | transactionValue = transactionValue.add(tradePrice.sub(currentNativeValueInRouter)) 63 | currentNativeValueInRouter = BigNumber.from(0) 64 | } else { 65 | currentNativeValueInRouter = currentNativeValueInRouter.sub(tradePrice) 66 | } 67 | /** 68 | * is UniswapTrade 69 | */ 70 | } else if (trade.tradeType == RouterTradeType.UniswapTrade) { 71 | const uniswapTrade = trade as UniswapTrade 72 | const inputIsNative = uniswapTrade.trade.inputAmount.currency.isNative 73 | const outputIsNative = uniswapTrade.trade.outputAmount.currency.isNative 74 | const swapOptions = uniswapTrade.options 75 | 76 | invariant(!(inputIsNative && !!swapOptions.inputTokenPermit), 'NATIVE_INPUT_PERMIT') 77 | 78 | if (!!swapOptions.inputTokenPermit) { 79 | encodePermit(planner, swapOptions.inputTokenPermit) 80 | } 81 | 82 | if (inputIsNative) { 83 | transactionValue = transactionValue.add( 84 | BigNumber.from(uniswapTrade.trade.maximumAmountIn(swapOptions.slippageTolerance).quotient.toString()) 85 | ) 86 | } 87 | // track amount of native currency in the router 88 | if (outputIsNative && swapOptions.recipient == ROUTER_AS_RECIPIENT) { 89 | currentNativeValueInRouter = currentNativeValueInRouter.add( 90 | BigNumber.from(uniswapTrade.trade.minimumAmountOut(swapOptions.slippageTolerance).quotient.toString()) 91 | ) 92 | } 93 | uniswapTrade.encode(planner, { allowRevert: false }) 94 | /** 95 | * is UnwrapWETH 96 | */ 97 | } else if (trade.tradeType == RouterTradeType.UnwrapWETH) { 98 | const UnwrapWETH = trade as UnwrapWETH 99 | trade.encode(planner, { allowRevert: false }) 100 | currentNativeValueInRouter = currentNativeValueInRouter.add(UnwrapWETH.amount) 101 | /** 102 | * else 103 | */ 104 | } else { 105 | throw 'trade must be of instance: UniswapTrade or NFTTrade' 106 | } 107 | } 108 | 109 | // TODO: matches current logic for now, but should eventually only sweep for multiple NFT trades 110 | // or NFT trades with potential slippage (i.e. sudo). 111 | // Note: NFTXV2 sends excess ETH to the caller (router), not the specified recipient 112 | nftInputTokens.forEach((inputToken) => { 113 | planner.addCommand(CommandType.SWEEP, [inputToken, SENDER_AS_RECIPIENT, 0]) 114 | }) 115 | return SwapRouter.encodePlan(planner, transactionValue, config) 116 | } 117 | 118 | /** 119 | * @deprecated in favor of swapCallParameters. Update before next major version 2.0.0 120 | * This version does not work correctly for Seaport ERC20->NFT purchases 121 | * Produces the on-chain method name to call and the hex encoded parameters to pass as arguments for a given swap. 122 | * @param trades to produce call parameters for 123 | */ 124 | public static swapNFTCallParameters(trades: SupportedNFTTrade[], config: SwapRouterConfig = {}): MethodParameters { 125 | let planner = new RoutePlanner() 126 | let totalPrice = BigNumber.from(0) 127 | 128 | const allowRevert = trades.length == 1 && trades[0].orders.length == 1 ? false : true 129 | 130 | for (const trade of trades) { 131 | trade.encode(planner, { allowRevert }) 132 | totalPrice = totalPrice.add(trade.getTotalPrice()) 133 | } 134 | 135 | planner.addCommand(CommandType.SWEEP, [ETH_ADDRESS, SENDER_AS_RECIPIENT, 0]) 136 | return SwapRouter.encodePlan(planner, totalPrice, config) 137 | } 138 | 139 | /** 140 | * @deprecated in favor of swapCallParameters. Update before next major version 2.0.0 141 | * Produces the on-chain method name to call and the hex encoded parameters to pass as arguments for a given trade. 142 | * @param trades to produce call parameters for 143 | * @param options options for the call parameters 144 | */ 145 | public static swapERC20CallParameters( 146 | trades: RouterTrade, 147 | options: SwapOptions 148 | ): MethodParameters { 149 | // TODO: use permit if signature included in swapOptions 150 | const planner = new RoutePlanner() 151 | 152 | const trade: UniswapTrade = new UniswapTrade(trades, options) 153 | 154 | const inputCurrency = trade.trade.inputAmount.currency 155 | invariant(!(inputCurrency.isNative && !!options.inputTokenPermit), 'NATIVE_INPUT_PERMIT') 156 | 157 | if (options.inputTokenPermit) { 158 | encodePermit(planner, options.inputTokenPermit) 159 | } 160 | 161 | const nativeCurrencyValue = inputCurrency.isNative 162 | ? BigNumber.from(trade.trade.maximumAmountIn(options.slippageTolerance).quotient.toString()) 163 | : BigNumber.from(0) 164 | 165 | trade.encode(planner, { allowRevert: false }) 166 | return SwapRouter.encodePlan(planner, nativeCurrencyValue, { 167 | deadline: options.deadlineOrPreviousBlockhash ? BigNumber.from(options.deadlineOrPreviousBlockhash) : undefined, 168 | }) 169 | } 170 | 171 | /** 172 | * Encodes a planned route into a method name and parameters for the Router contract. 173 | * @param planner the planned route 174 | * @param nativeCurrencyValue the native currency value of the planned route 175 | * @param config the router config 176 | */ 177 | private static encodePlan( 178 | planner: RoutePlanner, 179 | nativeCurrencyValue: BigNumber, 180 | config: SwapRouterConfig = {} 181 | ): MethodParameters { 182 | const { commands, inputs } = planner 183 | const functionSignature = !!config.deadline ? 'execute(bytes,bytes[],uint256)' : 'execute(bytes,bytes[])' 184 | const parameters = !!config.deadline ? [commands, inputs, config.deadline] : [commands, inputs] 185 | const calldata = SwapRouter.INTERFACE.encodeFunctionData(functionSignature, parameters) 186 | return { calldata, value: nativeCurrencyValue.toHexString() } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | 3 | type ChainConfig = { 4 | router: string 5 | creationBlock: number 6 | weth: string 7 | } 8 | 9 | const WETH_NOT_SUPPORTED_ON_CHAIN = '0x0000000000000000000000000000000000000000' 10 | 11 | const CHAIN_CONFIGS: { [key: number]: ChainConfig } = { 12 | // mainnet 13 | [1]: { 14 | router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 15 | weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 16 | creationBlock: 17143817, 17 | }, 18 | // goerli 19 | [5]: { 20 | router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 21 | weth: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', 22 | creationBlock: 8940568, 23 | }, 24 | // sepolia 25 | [11155111]: { 26 | router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 27 | weth: '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14', 28 | creationBlock: 3543575, 29 | }, 30 | // polygon 31 | [137]: { 32 | router: '0xec7BE89e9d109e7e3Fec59c222CF297125FEFda2', 33 | weth: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', 34 | creationBlock: 52210153, 35 | }, 36 | //polygon mumbai 37 | [80001]: { 38 | router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 39 | weth: '0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889', 40 | creationBlock: 35176052, 41 | }, 42 | //optimism 43 | [10]: { 44 | router: '0xCb1355ff08Ab38bBCE60111F1bb2B784bE25D7e8', 45 | weth: '0x4200000000000000000000000000000000000006', 46 | creationBlock: 114702266, 47 | }, 48 | // optimism goerli 49 | [420]: { 50 | router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 51 | weth: '0x4200000000000000000000000000000000000006', 52 | creationBlock: 8887728, 53 | }, 54 | // arbitrum 55 | [42161]: { 56 | router: '0x5E325eDA8064b456f4781070C0738d849c824258', 57 | weth: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', 58 | creationBlock: 169472836, 59 | }, 60 | // arbitrum goerli 61 | [421613]: { 62 | router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 63 | weth: '0xe39Ab88f8A4777030A534146A9Ca3B52bd5D43A3', 64 | creationBlock: 18815277, 65 | }, 66 | // celo 67 | [42220]: { 68 | router: '0x643770e279d5d0733f21d6dc03a8efbabf3255b4', 69 | weth: WETH_NOT_SUPPORTED_ON_CHAIN, 70 | creationBlock: 21407637, 71 | }, 72 | // celo alfajores 73 | [44787]: { 74 | router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 75 | weth: WETH_NOT_SUPPORTED_ON_CHAIN, 76 | creationBlock: 17566658, 77 | }, 78 | // binance smart chain 79 | [56]: { 80 | router: '0x4Dae2f939ACf50408e13d58534Ff8c2776d45265', 81 | weth: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', 82 | creationBlock: 35160263, 83 | }, 84 | // avalanche 85 | [43114]: { 86 | router: '0x4Dae2f939ACf50408e13d58534Ff8c2776d45265', 87 | weth: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', 88 | creationBlock: 40237257, 89 | }, 90 | // base goerli 91 | [84531]: { 92 | router: '0xd0872d928672ae2ff74bdb2f5130ac12229cafaf', 93 | weth: '0x4200000000000000000000000000000000000006', 94 | creationBlock: 6915289, 95 | }, 96 | // base mainnet 97 | [8453]: { 98 | router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 99 | weth: '0x4200000000000000000000000000000000000006', 100 | creationBlock: 9107268, 101 | }, 102 | [81457]: { 103 | router: '0x643770E279d5D0733F21d6DC03A8efbABf3255B4', 104 | weth: '0x4300000000000000000000000000000000000004', 105 | creationBlock: 1116444, 106 | }, 107 | } 108 | 109 | export const UNIVERSAL_ROUTER_ADDRESS = (chainId: number): string => { 110 | if (!(chainId in CHAIN_CONFIGS)) throw new Error(`Universal Router not deployed on chain ${chainId}`) 111 | return CHAIN_CONFIGS[chainId].router 112 | } 113 | 114 | export const UNIVERSAL_ROUTER_CREATION_BLOCK = (chainId: number): number => { 115 | if (!(chainId in CHAIN_CONFIGS)) throw new Error(`Universal Router not deployed on chain ${chainId}`) 116 | return CHAIN_CONFIGS[chainId].creationBlock 117 | } 118 | 119 | export const WETH_ADDRESS = (chainId: number): string => { 120 | if (!(chainId in CHAIN_CONFIGS)) throw new Error(`Universal Router not deployed on chain ${chainId}`) 121 | 122 | if (CHAIN_CONFIGS[chainId].weth == WETH_NOT_SUPPORTED_ON_CHAIN) throw new Error(`Chain ${chainId} does not have WETH`) 123 | 124 | return CHAIN_CONFIGS[chainId].weth 125 | } 126 | 127 | export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' 128 | 129 | export const CONTRACT_BALANCE = BigNumber.from(2).pow(255) 130 | export const ETH_ADDRESS = '0x0000000000000000000000000000000000000000' 131 | export const E_ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' 132 | export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' 133 | export const MAX_UINT256 = BigNumber.from(2).pow(256).sub(1) 134 | export const MAX_UINT160 = BigNumber.from(2).pow(160).sub(1) 135 | 136 | export const SENDER_AS_RECIPIENT = '0x0000000000000000000000000000000000000001' 137 | export const ROUTER_AS_RECIPIENT = '0x0000000000000000000000000000000000000002' 138 | 139 | export const OPENSEA_CONDUIT_SPENDER_ID = 0 140 | export const SUDOSWAP_SPENDER_ID = 1 141 | -------------------------------------------------------------------------------- /src/utils/getNativeCurrencyValue.ts: -------------------------------------------------------------------------------- 1 | import { Currency, CurrencyAmount, Ether } from '@uniswap/sdk-core' 2 | 3 | export function getNativeCurrencyValue(currencyValues: CurrencyAmount[]): CurrencyAmount { 4 | for (const value of currencyValues) { 5 | if (value.currency.isNative) { 6 | const nativeCurrency = value.currency 7 | const zero = CurrencyAmount.fromRawAmount(nativeCurrency, 0) 8 | 9 | return currencyValues.reduce(function (prevValue: CurrencyAmount, currValue: CurrencyAmount) { 10 | const value = currValue.currency.isNative ? currValue : zero 11 | return prevValue.add(value) 12 | }, zero) 13 | } 14 | } 15 | return CurrencyAmount.fromRawAmount(Ether.onChain(1), 0) 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/inputTokens.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant' 2 | import { ethers } from 'ethers' 3 | import { PermitSingle } from '@uniswap/permit2-sdk' 4 | import { CommandType, RoutePlanner } from './routerCommands' 5 | import { OPENSEA_CONDUIT_SPENDER_ID, ROUTER_AS_RECIPIENT, SUDOSWAP_SPENDER_ID } from './constants' 6 | 7 | export interface Permit2Permit extends PermitSingle { 8 | signature: string 9 | } 10 | 11 | export type ApproveProtocol = { 12 | token: string 13 | protocol: string 14 | } 15 | 16 | export type Permit2TransferFrom = { 17 | token: string 18 | amount: string 19 | recipient?: string 20 | } 21 | 22 | export type InputTokenOptions = { 23 | approval?: ApproveProtocol 24 | permit2Permit?: Permit2Permit 25 | permit2TransferFrom?: Permit2TransferFrom 26 | } 27 | 28 | const SIGNATURE_LENGTH = 65 29 | const EIP_2098_SIGNATURE_LENGTH = 64 30 | 31 | export function encodePermit(planner: RoutePlanner, permit2: Permit2Permit): void { 32 | let signature = permit2.signature 33 | 34 | const length = ethers.utils.arrayify(permit2.signature).length 35 | // signature data provided for EIP-1271 may have length different from ECDSA signature 36 | if (length === SIGNATURE_LENGTH || length === EIP_2098_SIGNATURE_LENGTH) { 37 | // sanitizes signature to cover edge cases of malformed EIP-2098 sigs and v used as recovery id 38 | signature = ethers.utils.joinSignature(ethers.utils.splitSignature(permit2.signature)) 39 | } 40 | 41 | planner.addCommand(CommandType.PERMIT2_PERMIT, [permit2, signature]) 42 | } 43 | 44 | // Handles the encoding of commands needed to gather input tokens for a trade 45 | // Approval: The router approving another address to take tokens. 46 | // note: Only seaport and sudoswap support this action. Approvals are left open. 47 | // Permit: A Permit2 signature-based Permit to allow the router to access a user's tokens 48 | // Transfer: A Permit2 TransferFrom of tokens from a user to either the router or another address 49 | export function encodeInputTokenOptions(planner: RoutePlanner, options: InputTokenOptions) { 50 | // first ensure that all tokens provided for encoding are the same 51 | if (!!options.approval && !!options.permit2Permit) 52 | invariant(options.approval.token === options.permit2Permit.details.token, `inconsistent token`) 53 | if (!!options.approval && !!options.permit2TransferFrom) 54 | invariant(options.approval.token === options.permit2TransferFrom.token, `inconsistent token`) 55 | if (!!options.permit2TransferFrom && !!options.permit2Permit) 56 | invariant(options.permit2TransferFrom.token === options.permit2Permit.details.token, `inconsistent token`) 57 | 58 | // if an options.approval is required, add it 59 | if (!!options.approval) { 60 | planner.addCommand(CommandType.APPROVE_ERC20, [ 61 | options.approval.token, 62 | mapApprovalProtocol(options.approval.protocol), 63 | ]) 64 | } 65 | 66 | // if this order has a options.permit2Permit, encode it 67 | if (!!options.permit2Permit) { 68 | encodePermit(planner, options.permit2Permit) 69 | } 70 | 71 | if (!!options.permit2TransferFrom) { 72 | planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [ 73 | options.permit2TransferFrom.token, 74 | options.permit2TransferFrom.recipient ? options.permit2TransferFrom.recipient : ROUTER_AS_RECIPIENT, 75 | options.permit2TransferFrom.amount, 76 | ]) 77 | } 78 | } 79 | 80 | function mapApprovalProtocol(protocolAddress: string): number { 81 | switch (protocolAddress.toLowerCase()) { 82 | case '0x00000000000000adc04c56bf30ac9d3c0aaf14dc': // Seaport v1.5 83 | return OPENSEA_CONDUIT_SPENDER_ID 84 | case '0x00000000000001ad428e4906ae43d8f9852d0dd6': // Seaport v1.4 85 | return OPENSEA_CONDUIT_SPENDER_ID 86 | case '0x2b2e8cda09bba9660dca5cb6233787738ad68329': // Sudoswap 87 | return SUDOSWAP_SPENDER_ID 88 | default: 89 | throw new Error('unsupported protocol address') 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | import JSBI from 'jsbi' 3 | import bn from 'bignumber.js' 4 | import { Percent } from '@uniswap/sdk-core' 5 | import { toHex } from '@uniswap/v3-sdk' 6 | 7 | export function expandTo18DecimalsBN(n: number): BigNumber { 8 | // use bn intermediately to allow decimals in intermediate calculations 9 | return BigNumber.from(new bn(n).times(new bn(10).pow(18)).toFixed()) 10 | } 11 | 12 | export function expandTo18Decimals(n: number): JSBI { 13 | return JSBI.BigInt(BigNumber.from(n).mul(BigNumber.from(10).pow(18)).toString()) 14 | } 15 | 16 | export function encodeFeeBips(fee: Percent): string { 17 | return toHex(fee.multiply(10_000).quotient) 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/routerCommands.ts: -------------------------------------------------------------------------------- 1 | import { defaultAbiCoder } from 'ethers/lib/utils' 2 | 3 | /** 4 | * CommandTypes 5 | * @description Flags that modify a command's execution 6 | * @enum {number} 7 | */ 8 | export enum CommandType { 9 | V3_SWAP_EXACT_IN = 0x00, 10 | V3_SWAP_EXACT_OUT = 0x01, 11 | PERMIT2_TRANSFER_FROM = 0x02, 12 | PERMIT2_PERMIT_BATCH = 0x03, 13 | SWEEP = 0x04, 14 | TRANSFER = 0x05, 15 | PAY_PORTION = 0x06, 16 | 17 | V2_SWAP_EXACT_IN = 0x08, 18 | V2_SWAP_EXACT_OUT = 0x09, 19 | PERMIT2_PERMIT = 0x0a, 20 | WRAP_ETH = 0x0b, 21 | UNWRAP_WETH = 0x0c, 22 | PERMIT2_TRANSFER_FROM_BATCH = 0x0d, 23 | BALANCE_CHECK_ERC20 = 0x0e, 24 | 25 | // NFT-related command types 26 | SEAPORT_V1_5 = 0x10, 27 | LOOKS_RARE_V2 = 0x11, 28 | NFTX = 0x12, 29 | CRYPTOPUNKS = 0x13, 30 | // 0x14 31 | OWNER_CHECK_721 = 0x15, 32 | OWNER_CHECK_1155 = 0x16, 33 | SWEEP_ERC721 = 0x17, 34 | 35 | X2Y2_721 = 0x18, 36 | SUDOSWAP = 0x19, 37 | NFT20 = 0x1a, 38 | X2Y2_1155 = 0x1b, 39 | FOUNDATION = 0x1c, 40 | SWEEP_ERC1155 = 0x1d, 41 | ELEMENT_MARKET = 0x1e, 42 | 43 | SEAPORT_V1_4 = 0x20, 44 | EXECUTE_SUB_PLAN = 0x21, 45 | APPROVE_ERC20 = 0x22, 46 | } 47 | 48 | const ALLOW_REVERT_FLAG = 0x80 49 | 50 | const REVERTIBLE_COMMANDS = new Set([ 51 | CommandType.SEAPORT_V1_5, 52 | CommandType.SEAPORT_V1_4, 53 | CommandType.NFTX, 54 | CommandType.LOOKS_RARE_V2, 55 | CommandType.X2Y2_721, 56 | CommandType.X2Y2_1155, 57 | CommandType.FOUNDATION, 58 | CommandType.SUDOSWAP, 59 | CommandType.NFT20, 60 | CommandType.EXECUTE_SUB_PLAN, 61 | CommandType.CRYPTOPUNKS, 62 | CommandType.ELEMENT_MARKET, 63 | ]) 64 | 65 | const PERMIT_STRUCT = 66 | '((address token,uint160 amount,uint48 expiration,uint48 nonce) details,address spender,uint256 sigDeadline)' 67 | 68 | const PERMIT_BATCH_STRUCT = 69 | '((address token,uint160 amount,uint48 expiration,uint48 nonce)[] details,address spender,uint256 sigDeadline)' 70 | 71 | const PERMIT2_TRANSFER_FROM_STRUCT = '(address from,address to,uint160 amount,address token)' 72 | const PERMIT2_TRANSFER_FROM_BATCH_STRUCT = PERMIT2_TRANSFER_FROM_STRUCT + '[]' 73 | 74 | const ABI_DEFINITION: { [key in CommandType]: string[] } = { 75 | // Batch Reverts 76 | [CommandType.EXECUTE_SUB_PLAN]: ['bytes', 'bytes[]'], 77 | 78 | // Permit2 Actions 79 | [CommandType.PERMIT2_PERMIT]: [PERMIT_STRUCT, 'bytes'], 80 | [CommandType.PERMIT2_PERMIT_BATCH]: [PERMIT_BATCH_STRUCT, 'bytes'], 81 | [CommandType.PERMIT2_TRANSFER_FROM]: ['address', 'address', 'uint160'], 82 | [CommandType.PERMIT2_TRANSFER_FROM_BATCH]: [PERMIT2_TRANSFER_FROM_BATCH_STRUCT], 83 | 84 | // Uniswap Actions 85 | [CommandType.V3_SWAP_EXACT_IN]: ['address', 'uint256', 'uint256', 'bytes', 'bool'], 86 | [CommandType.V3_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'bytes', 'bool'], 87 | [CommandType.V2_SWAP_EXACT_IN]: ['address', 'uint256', 'uint256', 'address[]', 'bool'], 88 | [CommandType.V2_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'address[]', 'bool'], 89 | 90 | // Token Actions and Checks 91 | [CommandType.WRAP_ETH]: ['address', 'uint256'], 92 | [CommandType.UNWRAP_WETH]: ['address', 'uint256'], 93 | [CommandType.SWEEP]: ['address', 'address', 'uint256'], 94 | [CommandType.SWEEP_ERC721]: ['address', 'address', 'uint256'], 95 | [CommandType.SWEEP_ERC1155]: ['address', 'address', 'uint256', 'uint256'], 96 | [CommandType.TRANSFER]: ['address', 'address', 'uint256'], 97 | [CommandType.PAY_PORTION]: ['address', 'address', 'uint256'], 98 | [CommandType.BALANCE_CHECK_ERC20]: ['address', 'address', 'uint256'], 99 | [CommandType.OWNER_CHECK_721]: ['address', 'address', 'uint256'], 100 | [CommandType.OWNER_CHECK_1155]: ['address', 'address', 'uint256', 'uint256'], 101 | [CommandType.APPROVE_ERC20]: ['address', 'uint256'], 102 | 103 | // NFT Markets 104 | [CommandType.SEAPORT_V1_5]: ['uint256', 'bytes'], 105 | [CommandType.SEAPORT_V1_4]: ['uint256', 'bytes'], 106 | [CommandType.NFTX]: ['uint256', 'bytes'], 107 | [CommandType.LOOKS_RARE_V2]: ['uint256', 'bytes'], 108 | [CommandType.X2Y2_721]: ['uint256', 'bytes', 'address', 'address', 'uint256'], 109 | [CommandType.X2Y2_1155]: ['uint256', 'bytes', 'address', 'address', 'uint256', 'uint256'], 110 | [CommandType.FOUNDATION]: ['uint256', 'bytes', 'address', 'address', 'uint256'], 111 | [CommandType.SUDOSWAP]: ['uint256', 'bytes'], 112 | [CommandType.NFT20]: ['uint256', 'bytes'], 113 | [CommandType.CRYPTOPUNKS]: ['uint256', 'address', 'uint256'], 114 | [CommandType.ELEMENT_MARKET]: ['uint256', 'bytes'], 115 | } 116 | 117 | export class RoutePlanner { 118 | commands: string 119 | inputs: string[] 120 | 121 | constructor() { 122 | this.commands = '0x' 123 | this.inputs = [] 124 | } 125 | 126 | addSubPlan(subplan: RoutePlanner): void { 127 | this.addCommand(CommandType.EXECUTE_SUB_PLAN, [subplan.commands, subplan.inputs], true) 128 | } 129 | 130 | addCommand(type: CommandType, parameters: any[], allowRevert = false): void { 131 | let command = createCommand(type, parameters) 132 | this.inputs.push(command.encodedInput) 133 | if (allowRevert) { 134 | if (!REVERTIBLE_COMMANDS.has(command.type)) { 135 | throw new Error(`command type: ${command.type} cannot be allowed to revert`) 136 | } 137 | command.type = command.type | ALLOW_REVERT_FLAG 138 | } 139 | 140 | this.commands = this.commands.concat(command.type.toString(16).padStart(2, '0')) 141 | } 142 | } 143 | 144 | export type RouterCommand = { 145 | type: CommandType 146 | encodedInput: string 147 | } 148 | 149 | export function createCommand(type: CommandType, parameters: any[]): RouterCommand { 150 | const encodedInput = defaultAbiCoder.encode(ABI_DEFINITION[type], parameters) 151 | return { type, encodedInput } 152 | } 153 | -------------------------------------------------------------------------------- /src/utils/routerTradeAdapter.ts: -------------------------------------------------------------------------------- 1 | import { MixedRouteSDK, Trade as RouterTrade } from '@uniswap/router-sdk' 2 | import { Currency, CurrencyAmount, Ether, Token, TradeType } from '@uniswap/sdk-core' 3 | import { Pair, Route as V2Route } from '@uniswap/v2-sdk' 4 | import { Pool, Route as V3Route, FeeAmount } from '@uniswap/v3-sdk' 5 | import { BigNumber } from 'ethers' 6 | import { ETH_ADDRESS, E_ETH_ADDRESS } from './constants' 7 | 8 | export type TokenInRoute = { 9 | address: string 10 | chainId: number 11 | symbol: string 12 | decimals: string 13 | name?: string 14 | buyFeeBps?: string 15 | sellFeeBps?: string 16 | } 17 | 18 | export enum PoolType { 19 | V2Pool = 'v2-pool', 20 | V3Pool = 'v3-pool', 21 | } 22 | 23 | export type V2Reserve = { 24 | token: TokenInRoute 25 | quotient: string 26 | } 27 | 28 | export type V2PoolInRoute = { 29 | type: PoolType.V2Pool 30 | address?: string 31 | tokenIn: TokenInRoute 32 | tokenOut: TokenInRoute 33 | reserve0: V2Reserve 34 | reserve1: V2Reserve 35 | amountIn?: string 36 | amountOut?: string 37 | } 38 | 39 | export type V3PoolInRoute = { 40 | type: PoolType.V3Pool 41 | address?: string 42 | tokenIn: TokenInRoute 43 | tokenOut: TokenInRoute 44 | sqrtRatioX96: string 45 | liquidity: string 46 | tickCurrent: string 47 | fee: string 48 | amountIn?: string 49 | amountOut?: string 50 | } 51 | 52 | export type PartialClassicQuote = { 53 | // We need tokenIn/Out to support native currency 54 | tokenIn: string 55 | tokenOut: string 56 | tradeType: TradeType 57 | route: Array<(V3PoolInRoute | V2PoolInRoute)[]> 58 | } 59 | 60 | interface RouteResult { 61 | routev3: V3Route | null 62 | routev2: V2Route | null 63 | mixedRoute: MixedRouteSDK | null 64 | inputAmount: CurrencyAmount 65 | outputAmount: CurrencyAmount 66 | } 67 | 68 | export const isNativeCurrency = (address: string) => 69 | address.toLowerCase() === ETH_ADDRESS.toLowerCase() || address.toLowerCase() === E_ETH_ADDRESS.toLowerCase() 70 | 71 | // Helper class to convert routing-specific quote entities to RouterTrade entities 72 | // the returned RouterTrade can then be used to build the UniswapTrade entity in this package 73 | export class RouterTradeAdapter { 74 | // Generate a RouterTrade using fields from a classic quote response 75 | static fromClassicQuote(quote: PartialClassicQuote) { 76 | const { route, tokenIn, tokenOut } = quote 77 | 78 | if (!route) throw new Error('Expected route to be present') 79 | if (!route.length) throw new Error('Expected there to be at least one route') 80 | if (route.some((r) => !r.length)) throw new Error('Expected all routes to have at least one pool') 81 | const firstRoute = route[0] 82 | 83 | const tokenInData = firstRoute[0].tokenIn 84 | const tokenOutData = firstRoute[firstRoute.length - 1].tokenOut 85 | 86 | if (!tokenInData || !tokenOutData) throw new Error('Expected both tokenIn and tokenOut to be present') 87 | if (tokenInData.chainId !== tokenOutData.chainId) 88 | throw new Error('Expected tokenIn and tokenOut to be have same chainId') 89 | 90 | const parsedCurrencyIn = RouterTradeAdapter.toCurrency(isNativeCurrency(tokenIn), tokenInData) 91 | const parsedCurrencyOut = RouterTradeAdapter.toCurrency(isNativeCurrency(tokenOut), tokenOutData) 92 | 93 | const typedRoutes: RouteResult[] = route.map((subRoute) => { 94 | const rawAmountIn = subRoute[0].amountIn 95 | const rawAmountOut = subRoute[subRoute.length - 1].amountOut 96 | 97 | if (!rawAmountIn || !rawAmountOut) { 98 | throw new Error('Expected both raw amountIn and raw amountOut to be present') 99 | } 100 | 101 | const inputAmount = CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn) 102 | const outputAmount = CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut) 103 | 104 | const isOnlyV2 = RouterTradeAdapter.isVersionedRoute(PoolType.V2Pool, subRoute) 105 | const isOnlyV3 = RouterTradeAdapter.isVersionedRoute(PoolType.V3Pool, subRoute) 106 | 107 | return { 108 | routev3: isOnlyV3 109 | ? new V3Route( 110 | (subRoute as V3PoolInRoute[]).map(RouterTradeAdapter.toPool), 111 | parsedCurrencyIn, 112 | parsedCurrencyOut 113 | ) 114 | : null, 115 | routev2: isOnlyV2 116 | ? new V2Route( 117 | (subRoute as V2PoolInRoute[]).map(RouterTradeAdapter.toPair), 118 | parsedCurrencyIn, 119 | parsedCurrencyOut 120 | ) 121 | : null, 122 | mixedRoute: 123 | !isOnlyV3 && !isOnlyV2 124 | ? new MixedRouteSDK(subRoute.map(RouterTradeAdapter.toPoolOrPair), parsedCurrencyIn, parsedCurrencyOut) 125 | : null, 126 | inputAmount, 127 | outputAmount, 128 | } 129 | }) 130 | 131 | return new RouterTrade({ 132 | v2Routes: typedRoutes 133 | .filter((route) => route.routev2) 134 | .map((route) => ({ 135 | routev2: route.routev2 as V2Route, 136 | inputAmount: route.inputAmount, 137 | outputAmount: route.outputAmount, 138 | })), 139 | v3Routes: typedRoutes 140 | .filter((route) => route.routev3) 141 | .map((route) => ({ 142 | routev3: route.routev3 as V3Route, 143 | inputAmount: route.inputAmount, 144 | outputAmount: route.outputAmount, 145 | })), 146 | mixedRoutes: typedRoutes 147 | .filter((route) => route.mixedRoute) 148 | .map((route) => ({ 149 | mixedRoute: route.mixedRoute as MixedRouteSDK, 150 | inputAmount: route.inputAmount, 151 | outputAmount: route.outputAmount, 152 | })), 153 | tradeType: quote.tradeType, 154 | }) 155 | } 156 | 157 | private static toCurrency(isNative: boolean, token: TokenInRoute): Currency { 158 | if (isNative) { 159 | return Ether.onChain(token.chainId) 160 | } 161 | return this.toToken(token) 162 | } 163 | 164 | private static toPoolOrPair = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => { 165 | return pool.type === PoolType.V3Pool ? RouterTradeAdapter.toPool(pool) : RouterTradeAdapter.toPair(pool) 166 | } 167 | 168 | private static toToken(token: TokenInRoute): Token { 169 | const { chainId, address, decimals, symbol, buyFeeBps, sellFeeBps } = token 170 | return new Token( 171 | chainId, 172 | address, 173 | parseInt(decimals.toString()), 174 | symbol, 175 | /* name */ undefined, 176 | false, 177 | buyFeeBps ? BigNumber.from(buyFeeBps) : undefined, 178 | sellFeeBps ? BigNumber.from(sellFeeBps) : undefined 179 | ) 180 | } 181 | 182 | private static toPool({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool { 183 | return new Pool( 184 | RouterTradeAdapter.toToken(tokenIn), 185 | RouterTradeAdapter.toToken(tokenOut), 186 | parseInt(fee) as FeeAmount, 187 | sqrtRatioX96, 188 | liquidity, 189 | parseInt(tickCurrent) 190 | ) 191 | } 192 | 193 | private static toPair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair => { 194 | return new Pair( 195 | CurrencyAmount.fromRawAmount(RouterTradeAdapter.toToken(reserve0.token), reserve0.quotient), 196 | CurrencyAmount.fromRawAmount(RouterTradeAdapter.toToken(reserve1.token), reserve1.quotient) 197 | ) 198 | } 199 | 200 | private static isVersionedRoute( 201 | type: PoolType, 202 | route: (V3PoolInRoute | V2PoolInRoute)[] 203 | ): route is T[] { 204 | return route.every((pool) => pool.type === type) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /test/forge/MixedSwapCallParameters.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {Test, stdJson} from "forge-std/Test.sol"; 5 | import {ERC20} from "solmate/src/tokens/ERC20.sol"; 6 | import {ERC721} from "solmate/src/tokens/ERC721.sol"; 7 | import {UniversalRouter} from "universal-router/UniversalRouter.sol"; 8 | import {Permit2} from "permit2/src/Permit2.sol"; 9 | import {DeployRouter} from "./utils/DeployRouter.sol"; 10 | import {MethodParameters, Interop} from "./utils/Interop.sol"; 11 | 12 | contract MixedSwapCallParameters is Test, Interop, DeployRouter { 13 | using stdJson for string; 14 | 15 | ERC20 private constant WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 16 | ERC20 private constant USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 17 | ERC20 private constant DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 18 | // starting eth balance 19 | uint256 constant BALANCE = 50_000 ether; 20 | 21 | ERC721 constant LOOKS_RARE_NFT = ERC721(0xAA107cCFe230a29C345Fd97bc6eb9Bd2fccD0750); 22 | ERC721 constant SEAPORT_NFT = ERC721(0xcee3C4F9f52cE89e310F19b363a9d4F796B56A68); 23 | 24 | function setUp() public { 25 | fromPrivateKey = 0x1234; 26 | from = vm.addr(fromPrivateKey); 27 | string memory root = vm.projectRoot(); 28 | json = vm.readFile(string.concat(root, "/test/forge/interop.json")); 29 | } 30 | 31 | function testMixedERC20ForLooksRareNFT() public { 32 | MethodParameters memory params = readFixture(json, "._ERC20_FOR_1_LOOKSRARE_NFT"); 33 | 34 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 35 | vm.startPrank(from); 36 | 37 | deployRouterAndPermit2(); 38 | vm.deal(from, BALANCE); 39 | 40 | deal(address(USDC), from, BALANCE); 41 | USDC.approve(address(permit2), BALANCE); 42 | permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); 43 | assertEq(USDC.balanceOf(from), BALANCE); 44 | 45 | uint256 balanceOfBefore = USDC.balanceOf(from); 46 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 47 | (bool success,) = address(router).call{value: params.value}(params.data); 48 | require(success, "call failed"); 49 | assertLt(USDC.balanceOf(from), balanceOfBefore); 50 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 1); 51 | } 52 | 53 | function testMixedWETHForLooksRareNFTWithPermit() public { 54 | MethodParameters memory params = readFixture(json, "._PERMIT_AND_WETH_FOR_1_LOOKSRARE_NFT"); 55 | 56 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 57 | vm.startPrank(from); 58 | 59 | deployRouterAndPermit2(); 60 | 61 | vm.deal(from, BALANCE); 62 | 63 | deal(address(WETH), from, BALANCE); 64 | WETH.approve(address(permit2), BALANCE); 65 | assertEq(WETH.balanceOf(from), BALANCE); 66 | 67 | uint256 balanceOfBefore = WETH.balanceOf(from); 68 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 69 | (bool success,) = address(router).call{value: params.value}(params.data); 70 | require(success, "call failed"); 71 | assertLt(WETH.balanceOf(from), balanceOfBefore); 72 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 1); 73 | } 74 | 75 | function testMixedWETHForLooksRareNFT() public { 76 | MethodParameters memory params = readFixture(json, "._WETH_FOR_1_LOOKSRARE_NFT"); 77 | 78 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 79 | vm.startPrank(from); 80 | 81 | deployRouterAndPermit2(); 82 | 83 | vm.deal(from, BALANCE); 84 | 85 | deal(address(WETH), from, BALANCE); 86 | WETH.approve(address(permit2), BALANCE); 87 | permit2.approve(address(WETH), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); 88 | assertEq(WETH.balanceOf(from), BALANCE); 89 | 90 | uint256 balanceOfBefore = WETH.balanceOf(from); 91 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 92 | (bool success,) = address(router).call{value: params.value}(params.data); 93 | require(success, "call failed"); 94 | assertLt(WETH.balanceOf(from), balanceOfBefore); 95 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 1); 96 | } 97 | 98 | function testMixedERC20AndETHForLooksRareNFT() public { 99 | MethodParameters memory params = readFixture(json, "._ERC20_AND_ETH_FOR_1_LOOKSRARE_NFT"); 100 | 101 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 102 | vm.startPrank(from); 103 | 104 | deployRouterAndPermit2(); 105 | vm.deal(from, BALANCE); 106 | 107 | deal(address(USDC), from, BALANCE); 108 | USDC.approve(address(permit2), BALANCE); 109 | permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); 110 | assertEq(USDC.balanceOf(from), BALANCE); 111 | 112 | uint256 balanceOfBefore = USDC.balanceOf(from); 113 | uint256 ethBalanceOfBefore = address(from).balance; 114 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 115 | (bool success,) = address(router).call{value: params.value}(params.data); 116 | require(success, "call failed"); 117 | assertEq(balanceOfBefore - USDC.balanceOf(from), 184272629); 118 | assertEq(ethBalanceOfBefore - address(from).balance, 101892924857227687); 119 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 1); 120 | assertEq(address(router).balance, 0); 121 | } 122 | 123 | function testMixedERC20ForLooksRareAndSeaportNFTs() public { 124 | MethodParameters memory params = readFixture(json, "._ERC20_FOR_1_LOOKSRARE_NFT_1_SEAPORT_NFT"); 125 | 126 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 127 | vm.startPrank(from); 128 | 129 | deployRouterAndPermit2(); 130 | vm.deal(from, BALANCE); 131 | 132 | deal(address(USDC), from, BALANCE); 133 | USDC.approve(address(permit2), BALANCE); 134 | permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); 135 | assertEq(USDC.balanceOf(from), BALANCE); 136 | 137 | uint256 balanceOfBefore = USDC.balanceOf(from); 138 | uint256 ethBalanceOfBefore = address(from).balance; 139 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 140 | assertEq(SEAPORT_NFT.balanceOf(RECIPIENT), 0); 141 | (bool success,) = address(router).call{value: params.value}(params.data); 142 | require(success, "call failed"); 143 | assertEq(balanceOfBefore - USDC.balanceOf(from), 412470713); 144 | assertEq(ethBalanceOfBefore - address(from).balance, 0); 145 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 1); 146 | assertEq(SEAPORT_NFT.balanceOf(RECIPIENT), 1); 147 | assertEq(address(router).balance, 0); 148 | } 149 | 150 | function testMixedERC20AndETHForLooksRareAndSeaportNFTs() public { 151 | MethodParameters memory params = readFixture(json, "._ERC20_AND_ETH_FOR_1_LOOKSRARE_NFT_1_SEAPORT_NFT"); 152 | 153 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 154 | vm.startPrank(from); 155 | 156 | deployRouterAndPermit2(); 157 | vm.deal(from, BALANCE); 158 | 159 | deal(address(USDC), from, BALANCE); 160 | USDC.approve(address(permit2), BALANCE); 161 | permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); 162 | assertEq(USDC.balanceOf(from), BALANCE); 163 | 164 | uint256 balanceOfBefore = USDC.balanceOf(from); 165 | uint256 ethBalanceOfBefore = address(from).balance; 166 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 167 | assertEq(SEAPORT_NFT.balanceOf(RECIPIENT), 0); 168 | (bool success,) = address(router).call{value: params.value}(params.data); 169 | require(success, "call failed"); 170 | assertEq(balanceOfBefore - USDC.balanceOf(from), 375656349); 171 | assertEq(ethBalanceOfBefore - address(from).balance, 19599999999999998); 172 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 1); 173 | assertEq(SEAPORT_NFT.balanceOf(RECIPIENT), 1); 174 | assertEq(address(router).balance, 0); 175 | } 176 | 177 | function testMixedERC20AndERC20For1NFT() public { 178 | MethodParameters memory params = readFixture(json, "._2_ERC20s_FOR_1_NFT"); 179 | 180 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 181 | vm.startPrank(from); 182 | 183 | deployRouterAndPermit2(); 184 | vm.deal(from, BALANCE); 185 | 186 | deal(address(USDC), from, BALANCE); 187 | deal(address(DAI), from, BALANCE); 188 | USDC.approve(address(permit2), BALANCE); 189 | permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); 190 | DAI.approve(address(permit2), BALANCE); 191 | permit2.approve(address(DAI), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); 192 | assertEq(DAI.balanceOf(from), BALANCE); 193 | 194 | uint256 balanceOfBefore = USDC.balanceOf(from); 195 | uint256 ethBalanceOfBefore = address(from).balance; 196 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 197 | (bool success,) = address(router).call{value: params.value}(params.data); 198 | require(success, "call failed"); 199 | assertEq(balanceOfBefore - USDC.balanceOf(from), 187828076); 200 | assertGt(address(from).balance - ethBalanceOfBefore, 0); // v2 exactOut rounding imprecision 201 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 1); 202 | assertEq(address(router).balance, 0); 203 | } 204 | 205 | function testMixedERC20For1InvalidNFTReverts() public { 206 | MethodParameters memory params = readFixture(json, "._ERC20_FOR_1_INVALID_NFT"); 207 | 208 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 209 | vm.startPrank(from); 210 | 211 | deployRouterAndPermit2(); 212 | vm.deal(from, BALANCE); 213 | 214 | deal(address(USDC), from, BALANCE); 215 | USDC.approve(address(permit2), BALANCE); 216 | permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); 217 | assertEq(USDC.balanceOf(from), BALANCE); 218 | 219 | uint256 balanceOfBefore = USDC.balanceOf(from); 220 | uint256 ethBalanceOfBefore = address(from).balance; 221 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 222 | vm.expectRevert(bytes("error message")); 223 | (bool success,) = address(router).call{value: params.value}(params.data); 224 | assertFalse(success, "expectRevert: call did not revert"); 225 | assertEq(balanceOfBefore - USDC.balanceOf(from), 0); 226 | assertEq(ethBalanceOfBefore - address(from).balance, 0); 227 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 228 | assertEq(address(router).balance, 0); 229 | } 230 | 231 | function testMixedERC20SwapForNFTsPartialFill() public { 232 | MethodParameters memory params = readFixture(json, "._ERC20_FOR_NFTS_PARTIAL_FILL"); 233 | 234 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 235 | vm.startPrank(from); 236 | 237 | deployRouterAndPermit2(); 238 | vm.deal(from, BALANCE); 239 | 240 | deal(address(USDC), from, BALANCE); 241 | USDC.approve(address(permit2), BALANCE); 242 | permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); 243 | assertEq(USDC.balanceOf(from), BALANCE); 244 | 245 | uint256 balanceOfBefore = USDC.balanceOf(from); 246 | uint256 ethBalanceOfBefore = address(from).balance; 247 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 248 | assertEq(SEAPORT_NFT.balanceOf(RECIPIENT), 0); 249 | (bool success,) = address(router).call{value: params.value}(params.data); 250 | require(success, "call failed"); 251 | assertEq(balanceOfBefore - USDC.balanceOf(from), 412470713); 252 | assertEq(address(from).balance - ethBalanceOfBefore, 200000000000000000); // earned ETH back from partial fill 253 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 254 | assertEq(SEAPORT_NFT.balanceOf(RECIPIENT), 1); 255 | assertEq(address(router).balance, 0); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /test/forge/SwapNFTCallParameters.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {Test, stdJson, console2} from "forge-std/Test.sol"; 5 | import {ERC20} from "solmate/src/tokens/ERC20.sol"; 6 | import {ERC721} from "solmate/src/tokens/ERC721.sol"; 7 | import {ERC1155} from "solmate/src/tokens/ERC1155.sol"; 8 | import {UniversalRouter} from "universal-router/UniversalRouter.sol"; 9 | import {DeployRouter} from "./utils/DeployRouter.sol"; 10 | import {MethodParameters, Interop} from "./utils/Interop.sol"; 11 | import {ICryptopunksMarket} from "./utils/ICryptopunksMarket.sol"; 12 | 13 | contract swapNFTCallParametersTest is Test, Interop, DeployRouter { 14 | using stdJson for string; 15 | 16 | ERC20 private constant GALA = ERC20(0x15D4c048F83bd7e37d49eA4C83a07267Ec4203dA); 17 | // calldata for router.execute with command APPROVE_ERC20, approving Opensea Conduit to spend GALA 18 | bytes constant APPROVE_GALA_DATA = 19 | hex"24856bc3000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001220000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000015D4c048F83bd7e37d49eA4C83a07267Ec4203dA0000000000000000000000000000000000000000000000000000000000000000"; 20 | 21 | function setUp() public { 22 | fromPrivateKey = 0x1234; 23 | from = vm.addr(fromPrivateKey); 24 | string memory root = vm.projectRoot(); 25 | json = vm.readFile(string.concat(root, "/test/forge/interop.json")); 26 | } 27 | 28 | function testFoundationBuyItem() public { 29 | MethodParameters memory params = readFixture(json, "._FOUNDATION_BUY_ITEM"); 30 | 31 | vm.createSelectFork(vm.envString("FORK_URL"), 15725945); 32 | vm.startPrank(from); 33 | 34 | deployRouterAndPermit2(); 35 | ERC721 token = ERC721(0xEf96021Af16BD04918b0d87cE045d7984ad6c38c); 36 | uint256 balance = 1 ether; 37 | vm.deal(from, balance); 38 | assertEq(from.balance, balance); 39 | assertEq(token.balanceOf(RECIPIENT), 0); 40 | 41 | (bool success,) = address(router).call{value: params.value}(params.data); 42 | require(success, "call failed"); 43 | assertEq(token.balanceOf(RECIPIENT), 1); 44 | assertEq(from.balance, balance - params.value); 45 | } 46 | 47 | function testNftxBuyItems() public { 48 | MethodParameters memory params = readFixture(json, "._NFTX_BUY_ITEMS"); 49 | 50 | vm.createSelectFork(vm.envString("FORK_URL"), 17029001); 51 | vm.startPrank(from); 52 | 53 | deployRouterAndPermit2(); 54 | ERC721 token = ERC721(0x5Af0D9827E0c53E4799BB226655A1de152A425a5); 55 | uint256 balance = 32 ether; 56 | vm.deal(from, balance); 57 | assertEq(from.balance, balance); 58 | assertEq(token.balanceOf(RECIPIENT), 0); 59 | 60 | (bool success,) = address(router).call{value: params.value}(params.data); 61 | require(success, "call failed"); 62 | assertEq(token.balanceOf(RECIPIENT), 1); 63 | assertEq(from.balance, balance - params.value); 64 | } 65 | 66 | function testElementBuyItemsERC721() public { 67 | address taker = 0x75B6568025f463a98fB01082eEb6dCe04efA3Ae4; 68 | MethodParameters memory params = readFixture(json, "._ELEMENT_BUY_ITEM_721"); 69 | 70 | vm.createSelectFork(vm.envString("FORK_URL"), 16627213); 71 | vm.startPrank(from); 72 | 73 | deployRouterAndPermit2(); 74 | 75 | ERC721 token = ERC721(0x4C69dBc3a2Aa3476c3F7a1227ab70950DB1F4858); 76 | uint256 balance = 32 ether; 77 | vm.deal(from, balance); 78 | assertEq(from.balance, balance); 79 | assertEq(token.balanceOf(taker), 0); 80 | 81 | (bool success,) = address(router).call{value: params.value}(params.data); 82 | 83 | require(success, "call failed"); 84 | assertEq(token.balanceOf(taker), 1); 85 | assertEq(from.balance, balance - params.value); 86 | } 87 | 88 | function testElementBuyitmesERC712WithFees() public { 89 | MethodParameters memory params = readFixture(json, "._ELEMENT_BUY_ITEM_721_WITH_FEES"); 90 | 91 | vm.createSelectFork(vm.envString("FORK_URL"), 16870205); 92 | vm.startPrank(from); 93 | 94 | deployRouterAndPermit2(); 95 | ERC721 token = ERC721(0x4C69dBc3a2Aa3476c3F7a1227ab70950DB1F4858); 96 | uint256 balance = 32 ether; 97 | vm.deal(from, balance); 98 | assertEq(from.balance, balance); 99 | assertEq(token.balanceOf(RECIPIENT), 0); 100 | 101 | (bool success,) = address(router).call{value: params.value}(params.data); 102 | 103 | require(success, "call failed"); 104 | assertEq(token.balanceOf(RECIPIENT), 1); 105 | assertEq(from.balance, balance - params.value); 106 | } 107 | 108 | function testLooksRareV2BuyItemsERC721() public { 109 | MethodParameters memory params = readFixture(json, "._LOOKSRARE_V2_BUY_ITEM_721"); 110 | 111 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 112 | vm.startPrank(from); 113 | 114 | deployRouterAndPermit2(); 115 | 116 | ERC721 token = ERC721(0xAA107cCFe230a29C345Fd97bc6eb9Bd2fccD0750); 117 | uint256 balance = 32 ether; 118 | vm.deal(from, balance); 119 | assertEq(from.balance, balance); 120 | assertEq(token.balanceOf(RECIPIENT), 0); 121 | 122 | (bool success,) = address(router).call{value: params.value}(params.data); 123 | 124 | require(success, "call failed"); 125 | assertEq(token.balanceOf(RECIPIENT), 1); 126 | assertEq(from.balance, balance - params.value); 127 | } 128 | 129 | function testLooksRareV2BatchBuyItemsERC721() public { 130 | MethodParameters memory params = readFixture(json, "._LOOKSRARE_V2_BATCH_BUY_ITEM_721"); 131 | 132 | vm.createSelectFork(vm.envString("FORK_URL"), 17037139); 133 | vm.startPrank(from); 134 | 135 | deployRouterAndPermit2(); 136 | 137 | ERC721 token = ERC721(0xAA107cCFe230a29C345Fd97bc6eb9Bd2fccD0750); 138 | uint256 balance = 32 ether; 139 | vm.deal(from, balance); 140 | assertEq(from.balance, balance); 141 | assertEq(token.balanceOf(RECIPIENT), 0); 142 | 143 | (bool success,) = address(router).call{value: params.value}(params.data); 144 | 145 | require(success, "call failed"); 146 | assertEq(token.balanceOf(RECIPIENT), 2); 147 | assertEq(from.balance, balance - params.value); 148 | } 149 | 150 | function testSeaportV1_5BuyItemsETH() public { 151 | MethodParameters memory params = readFixture(json, "._SEAPORT_V1_5_BUY_ITEMS_ETH"); 152 | uint256 tokenId0 = 564868729088757849349201848336735231016960; 153 | uint256 tokenId1 = 580862000334041957131980454886028336955392; 154 | 155 | vm.createSelectFork(vm.envString("FORK_URL"), 17179617); 156 | vm.startPrank(from); 157 | 158 | deployRouterAndPermit2(); 159 | ERC1155 token = ERC1155(0xc36cF0cFcb5d905B8B513860dB0CFE63F6Cf9F5c); 160 | uint256 balance = 55 ether; 161 | vm.deal(from, balance); 162 | assertEq(from.balance, balance); 163 | assertEq(token.balanceOf(RECIPIENT, tokenId0), 0); 164 | assertEq(token.balanceOf(RECIPIENT, tokenId1), 0); 165 | 166 | (bool success,) = address(router).call{value: params.value}(params.data); 167 | require(success, "call failed"); 168 | assertEq(token.balanceOf(RECIPIENT, tokenId0), 1); 169 | assertEq(token.balanceOf(RECIPIENT, tokenId1), 1); 170 | assertEq(from.balance, balance - params.value); 171 | } 172 | 173 | function testSeaportBuyItemsERC20PermitApprove() public { 174 | MethodParameters memory params = readFixture(json, "._SEAPORT_V1_4_BUY_ITEMS_ERC20_PERMIT_AND_APPROVE"); 175 | assertEq(params.value, 0); 176 | uint256 tokenId = 425352958651173079329218259289710264320000; 177 | 178 | vm.createSelectFork(vm.envString("FORK_URL"), 16784347); 179 | vm.startPrank(from); 180 | 181 | deployRouterAndPermit2(); 182 | 183 | vm.deal(from, 1 ether); // for gas 184 | assertEq(from.balance, 1 ether); 185 | 186 | uint256 balance = 55 ether; 187 | deal(address(GALA), from, balance); 188 | GALA.approve(address(permit2), balance); 189 | assertEq(GALA.balanceOf(from), balance); 190 | 191 | ERC1155 token = ERC1155(0xc36cF0cFcb5d905B8B513860dB0CFE63F6Cf9F5c); 192 | assertEq(token.balanceOf(RECIPIENT, tokenId), 0); 193 | 194 | (bool success,) = address(router).call{value: params.value}(params.data); 195 | require(success, "call failed"); 196 | assertLt(GALA.balanceOf(from), balance); 197 | assertEq(GALA.balanceOf(address(router)), 0); 198 | assertEq(token.balanceOf(RECIPIENT, tokenId), 1); 199 | } 200 | 201 | function testSeaportBuyItemsERC20Permit() public { 202 | MethodParameters memory params = readFixture(json, "._SEAPORT_V1_4_BUY_ITEMS_ERC20_PERMIT_NO_APPROVE"); 203 | assertEq(params.value, 0); 204 | uint256 tokenId = 425352958651173079329218259289710264320000; 205 | 206 | vm.createSelectFork(vm.envString("FORK_URL"), 16784347); 207 | vm.startPrank(from); 208 | 209 | deployRouterAndPermit2(); 210 | 211 | vm.deal(from, 1 ether); // for gas 212 | assertEq(from.balance, 1 ether); 213 | 214 | uint256 balance = 55 ether; 215 | deal(address(GALA), from, balance); 216 | GALA.approve(address(permit2), balance); 217 | assertEq(GALA.balanceOf(from), balance); 218 | 219 | // a tx to pre-approve GALA for Seaport 220 | (bool success,) = address(router).call(APPROVE_GALA_DATA); 221 | require(success, "call failed"); 222 | 223 | ERC1155 token = ERC1155(0xc36cF0cFcb5d905B8B513860dB0CFE63F6Cf9F5c); 224 | assertEq(token.balanceOf(RECIPIENT, tokenId), 0); 225 | 226 | (success,) = address(router).call{value: params.value}(params.data); 227 | require(success, "call failed"); 228 | assertLt(GALA.balanceOf(from), balance); 229 | assertEq(GALA.balanceOf(address(router)), 0); 230 | assertEq(token.balanceOf(RECIPIENT, tokenId), 1); 231 | } 232 | 233 | function testSeaportV1_4BuyItemsETH() public { 234 | MethodParameters memory params = readFixture(json, "._SEAPORT_V1_4_BUY_ITEMS_ETH"); 235 | 236 | vm.createSelectFork(vm.envString("FORK_URL"), 16820453); 237 | vm.startPrank(from); 238 | 239 | deployRouterAndPermit2(); 240 | ERC1155 token = ERC1155(0x4f3AdeF2F4096740774A955E912B5F03F2C7bA2b); 241 | uint256 balance = 55 ether; 242 | vm.deal(from, balance); 243 | assertEq(from.balance, balance); 244 | assertEq(token.balanceOf(RECIPIENT, 1), 0); 245 | 246 | (bool success,) = address(router).call{value: params.value}(params.data); 247 | require(success, "call failed"); 248 | assertEq(token.balanceOf(RECIPIENT, 1), 2); 249 | // the order is for 3 tokens, but only 2 succeed, so 1/3 of the ETH is returned 250 | assertEq(from.balance, balance - (params.value * 2 / 3)); 251 | } 252 | 253 | function testCryptopunkBuyItems() public { 254 | MethodParameters memory params = readFixture(json, "._CRYPTOPUNK_BUY_ITEM"); 255 | // older block 15360000 does not work 256 | vm.createSelectFork(vm.envString("FORK_URL"), 15898323); 257 | vm.startPrank(from); 258 | 259 | deployRouterAndPermit2(); 260 | ICryptopunksMarket token = ICryptopunksMarket(0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB); 261 | uint256 balance = 80 ether; 262 | 263 | vm.deal(from, balance); 264 | assertEq(from.balance, balance); 265 | assertEq(token.balanceOf(RECIPIENT), 0); 266 | 267 | (bool success,) = address(router).call{value: params.value}(params.data); 268 | require(success, "call failed"); 269 | assertEq(token.balanceOf(RECIPIENT), 1); 270 | 271 | assertEq(token.punkIndexToAddress(2976), RECIPIENT); 272 | assertEq(from.balance, balance - params.value); 273 | } 274 | 275 | function testX2Y2Buy721Item() public { 276 | MethodParameters memory params = readFixture(json, "._X2Y2_721_BUY_ITEM"); 277 | 278 | vm.createSelectFork(vm.envString("FORK_URL"), 15360000); 279 | vm.startPrank(from); 280 | 281 | deployRouter(address(0)); 282 | 283 | ERC721 token = ERC721(0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85); 284 | uint256 balance = 1 ether; 285 | vm.deal(from, balance); 286 | assertEq(from.balance, balance); 287 | assertEq(token.balanceOf(RECIPIENT), 0); 288 | 289 | (bool success,) = address(router).call{value: params.value}(params.data); 290 | 291 | require(success, "call failed"); 292 | assertEq(token.balanceOf(RECIPIENT), 1); 293 | assertEq(from.balance, balance - params.value); 294 | } 295 | 296 | function testX2Y2Buy1155Item() public { 297 | MethodParameters memory params = readFixture(json, "._X2Y2_1155_BUY_ITEM"); 298 | vm.createSelectFork(vm.envString("FORK_URL"), 15978300); 299 | vm.startPrank(from); 300 | 301 | deployRouter(address(0)); 302 | 303 | ERC1155 token = ERC1155(0x93317E87a3a47821803CAADC54Ae418Af80603DA); 304 | uint256 balance = params.value; 305 | vm.deal(from, balance); 306 | assertEq(from.balance, balance); 307 | assertEq(token.balanceOf(RECIPIENT, 0), 0); 308 | 309 | (bool success,) = address(router).call{value: params.value}(params.data); 310 | 311 | require(success, "call failed"); 312 | assertEq(token.balanceOf(RECIPIENT, 0), 1); 313 | assertEq(from.balance, balance - params.value); 314 | } 315 | 316 | function testNFT20BuyItems() public { 317 | MethodParameters memory params = readFixture(json, "._NFT20_BUY_ITEM"); 318 | 319 | vm.createSelectFork(vm.envString("FORK_URL"), 15770228); 320 | vm.startPrank(from); 321 | 322 | deployRouterAndPermit2(); 323 | 324 | ERC721 token = ERC721(0x6d05064fe99e40F1C3464E7310A23FFADed56E20); 325 | uint256 balance = 20583701229648230; 326 | vm.deal(from, balance); 327 | assertEq(from.balance, balance); 328 | assertEq(token.balanceOf(RECIPIENT), 0); 329 | 330 | (bool success,) = address(router).call{value: params.value}(params.data); 331 | 332 | require(success, "call failed"); 333 | assertEq(token.balanceOf(RECIPIENT), 3); 334 | assertEq(from.balance, balance - params.value); 335 | } 336 | 337 | function testSudoswapBuyItems() public { 338 | MethodParameters memory params = readFixture(json, "._SUDOSWAP_BUY_ITEM"); 339 | 340 | vm.createSelectFork(vm.envString("FORK_URL"), 15740629); 341 | vm.startPrank(from); 342 | 343 | deployRouterAndPermit2(); 344 | 345 | ERC721 token = ERC721(0xfA9937555Dc20A020A161232de4D2B109C62Aa9c); 346 | uint256 balance = 73337152777777783; 347 | vm.deal(from, balance); 348 | assertEq(from.balance, balance); 349 | assertEq(token.balanceOf(RECIPIENT), 0); 350 | 351 | (bool success,) = address(router).call{value: params.value}(params.data); 352 | 353 | require(success, "call failed"); 354 | assertEq(token.balanceOf(RECIPIENT), 3); 355 | assertEq(from.balance, balance - params.value); 356 | } 357 | 358 | function testPartialFillBetweenProtocols() public { 359 | MethodParameters memory params = readFixture(json, "._PARTIAL_FILL"); 360 | 361 | vm.createSelectFork(vm.envString("FORK_URL"), 17030829); 362 | vm.startPrank(from); 363 | 364 | deployRouterAndPermit2(); 365 | ERC721 LOOKS_RARE_NFT = ERC721(0xAA107cCFe230a29C345Fd97bc6eb9Bd2fccD0750); 366 | ERC721 SEAPORT_NFT = ERC721(0xcee3C4F9f52cE89e310F19b363a9d4F796B56A68); 367 | 368 | uint256 balance = 54 ether; 369 | uint256 failedAmount = 200000000000000000; 370 | vm.deal(from, balance); 371 | assertEq(from.balance, balance); 372 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 373 | assertEq(SEAPORT_NFT.balanceOf(RECIPIENT), 0); 374 | 375 | (bool success,) = address(router).call{value: params.value}(params.data); 376 | require(success, "call failed"); 377 | assertEq(LOOKS_RARE_NFT.balanceOf(RECIPIENT), 0); 378 | assertEq(SEAPORT_NFT.balanceOf(RECIPIENT), 1); 379 | 380 | assertEq(from.balance, balance - params.value + failedAmount); 381 | } 382 | 383 | function testPartialFillWithinProtocol() public { 384 | MethodParameters memory params = readFixture(json, "._PARTIAL_FILL_WITHIN_PROTOCOL"); 385 | 386 | vm.createSelectFork(vm.envString("FORK_URL"), 15725945); 387 | vm.startPrank(from); 388 | 389 | deployRouterAndPermit2(); 390 | ERC721 token = ERC721(0xEf96021Af16BD04918b0d87cE045d7984ad6c38c); 391 | uint256 balance = 0.02 ether; 392 | uint256 failedAmount = 0.01 ether; 393 | vm.deal(from, balance); 394 | assertEq(from.balance, balance); 395 | assertEq(token.balanceOf(RECIPIENT), 0); 396 | 397 | (bool success,) = address(router).call{value: params.value}(params.data); 398 | require(success, "call failed"); 399 | assertEq(token.balanceOf(RECIPIENT), 1); 400 | assertEq(from.balance, balance - params.value + failedAmount); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /test/forge/utils/DeployRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {console2} from "forge-std/console2.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {UniversalRouter} from "universal-router/UniversalRouter.sol"; 7 | import {RouterParameters} from "universal-router/base/RouterImmutables.sol"; 8 | import {ERC20} from "solmate/src/tokens/ERC20.sol"; 9 | import {Permit2} from "permit2/src/Permit2.sol"; 10 | import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; 11 | 12 | contract DeployRouter is Test { 13 | address public constant LOOKS_TOKEN = 0xf4d2888d29D722226FafA5d9B24F9164c092421E; 14 | address public constant V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; 15 | address public constant V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; 16 | bytes32 public constant PAIR_INIT_CODE_HASH = 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f; 17 | bytes32 public constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; 18 | address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 19 | address public constant SEAPORT_V1_5 = 0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC; 20 | address public constant SEAPORT_V1_4 = 0x00000000000001ad428e4906aE43D8F9852d0dD6; 21 | address public constant NFTX_ZAP = 0x941A6d105802CCCaa06DE58a13a6F49ebDCD481C; 22 | address public constant X2Y2 = 0x74312363e45DCaBA76c59ec49a7Aa8A65a67EeD3; 23 | address public constant FOUNDATION = 0xcDA72070E455bb31C7690a170224Ce43623d0B6f; 24 | address public constant SUDOSWAP = 0x2B2e8cDA09bBA9660dCA5cB6233787738Ad68329; 25 | address public constant NFT20_ZAP = 0xA42f6cADa809Bcf417DeefbdD69C5C5A909249C0; 26 | address public constant CRYPTOPUNKS = 0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB; 27 | address public constant LOOKS_RARE_V2 = 0x0000000000E655fAe4d56241588680F86E3b2377; 28 | address public constant ROUTER_REWARDS_DISTRIBUTOR = 0x0000000000000000000000000000000000000000; 29 | address public constant LOOKSRARE_REWARDS_DISTRIBUTOR = 0x0554f068365eD43dcC98dcd7Fd7A8208a5638C72; 30 | address public constant OPENSEA_CONDUIT = 0x1E0049783F008A0085193E00003D00cd54003c71; 31 | address public constant ELEMENT_MARKET = 0x20F780A973856B93f63670377900C1d2a50a77c4; 32 | 33 | address internal constant RECIPIENT = 0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa; 34 | address internal constant FEE_RECIPIENT = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; 35 | address internal constant MAINNET_PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; 36 | 37 | address internal constant FORGE_ROUTER_ADDRESS = 0xE808C1cfeebb6cb36B537B82FA7c9EEf31415a05; 38 | 39 | UniversalRouter public router; 40 | Permit2 public permit2; 41 | 42 | address from; 43 | uint256 fromPrivateKey; 44 | string json; 45 | 46 | function deployRouter(address _permit2) public { 47 | router = new UniversalRouter( 48 | RouterParameters({ 49 | permit2: _permit2, 50 | weth9: WETH9, 51 | seaportV1_5: SEAPORT_V1_5, 52 | seaportV1_4: SEAPORT_V1_4, 53 | openseaConduit: OPENSEA_CONDUIT, 54 | nftxZap: NFTX_ZAP, 55 | x2y2: X2Y2, 56 | foundation: FOUNDATION, 57 | sudoswap: SUDOSWAP, 58 | elementMarket: ELEMENT_MARKET, 59 | nft20Zap: NFT20_ZAP, 60 | cryptopunks: CRYPTOPUNKS, 61 | looksRareV2: LOOKS_RARE_V2, 62 | routerRewardsDistributor: ROUTER_REWARDS_DISTRIBUTOR, 63 | looksRareRewardsDistributor: LOOKSRARE_REWARDS_DISTRIBUTOR, 64 | looksRareToken: LOOKS_TOKEN, 65 | v2Factory: V2_FACTORY, 66 | v3Factory: V3_FACTORY, 67 | pairInitCodeHash: PAIR_INIT_CODE_HASH, 68 | poolInitCodeHash: POOL_INIT_CODE_HASH 69 | }) 70 | ); 71 | } 72 | 73 | function deployRouterAndPermit2() public { 74 | permit2 = new Permit2(); 75 | deployRouter(address(permit2)); 76 | require(FORGE_ROUTER_ADDRESS == address(router)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/forge/utils/ICryptopunksMarket.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.4; 3 | 4 | /// @title Interface for CryptoPunksMarket 5 | interface ICryptopunksMarket { 6 | function balanceOf(address) external returns (uint256); 7 | 8 | function punkIndexToAddress(uint256) external returns (address); 9 | 10 | function buyPunk(uint256 punkIndex) external payable; 11 | 12 | function transferPunk(address to, uint256 punkIndex) external; 13 | } 14 | -------------------------------------------------------------------------------- /test/forge/utils/Interop.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {Test, stdJson} from "forge-std/Test.sol"; 5 | 6 | struct MethodParametersRaw { 7 | bytes data; 8 | string value; 9 | } 10 | 11 | struct MethodParameters { 12 | bytes data; 13 | uint256 value; 14 | } 15 | 16 | contract Interop is Test { 17 | function readFixture(string memory json, string memory key) internal returns (MethodParameters memory params) { 18 | // stdjson awkwardly doesn't currently parse string ints 19 | // so have to do a lil hack to read as string then parse 20 | // ref https://book.getfoundry.sh/cheatcodes/parse-json#decoding-json-objects-a-tip 21 | MethodParametersRaw memory raw = abi.decode(vm.parseJson(json, key), (MethodParametersRaw)); 22 | params = MethodParameters(raw.data, vm.parseUint(raw.value)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/forge/writeInterop.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { hexToDecimalString } from '../utils/hexToDecimalString' 3 | import { MethodParameters } from '@uniswap/v3-sdk' 4 | 5 | const INTEROP_FILE = './test/forge/interop.json' 6 | 7 | // updates the interop file with a new fixture 8 | export function registerFixture(key: string, data: MethodParameters) { 9 | let interop: { [key: string]: any } = fs.existsSync(INTEROP_FILE) 10 | ? JSON.parse(fs.readFileSync(INTEROP_FILE).toString()) 11 | : {} 12 | 13 | interop[key] = { 14 | calldata: data.calldata, 15 | value: hexToDecimalString(data.value), 16 | } 17 | fs.writeFileSync(INTEROP_FILE, JSON.stringify(interop, null, 2)) 18 | } 19 | -------------------------------------------------------------------------------- /test/mixedTrades.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { UnwrapWETH } from '../src/entities/protocols/unwrapWETH' 3 | import { SwapRouter, ROUTER_AS_RECIPIENT, WETH_ADDRESS, SeaportTrade } from '../src' 4 | import { utils, Wallet } from 'ethers' 5 | import { LooksRareV2Data, LooksRareV2Trade } from '../src/entities/protocols/looksRareV2' 6 | import { looksRareV2Orders } from './orders/looksRareV2' 7 | import { seaportV1_4DataETHRecent } from './orders/seaportV1_4' 8 | import { Trade as V2Trade, Route as RouteV2, Pair } from '@uniswap/v2-sdk' 9 | import { Trade as V3Trade, Route as RouteV3, Pool } from '@uniswap/v3-sdk' 10 | import { generatePermitSignature, makePermit } from './utils/permit2' 11 | 12 | import { UniswapTrade } from '../src' 13 | import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' 14 | import { registerFixture } from './forge/writeInterop' 15 | import { buildTrade, getUniswapPools, swapOptions, DAI, ETHER, WETH, USDC } from './utils/uniswapData' 16 | import { 17 | FORGE_PERMIT2_ADDRESS, 18 | FORGE_ROUTER_ADDRESS, 19 | FORGE_SENDER_ADDRESS, 20 | TEST_RECIPIENT_ADDRESS, 21 | } from './utils/addresses' 22 | import { ETH_ADDRESS } from '../src/utils/constants' 23 | import { hexToDecimalString } from './utils/hexToDecimalString' 24 | 25 | describe('SwapRouter.swapCallParameters', () => { 26 | const wallet = new Wallet(utils.zeroPad('0x1234', 32)) 27 | 28 | describe('erc20 --> nft', async () => { 29 | const looksRareV2Data: LooksRareV2Data = { 30 | apiOrder: looksRareV2Orders[0], 31 | taker: TEST_RECIPIENT_ADDRESS, 32 | } 33 | const looksRareV2Trade = new LooksRareV2Trade([looksRareV2Data]) 34 | const looksRareV2Value = looksRareV2Trade.getTotalPrice() 35 | 36 | const invalidLooksRareV2Data: LooksRareV2Data = { 37 | ...looksRareV2Data, 38 | apiOrder: { ...looksRareV2Data.apiOrder, itemIds: ['1'] }, 39 | } 40 | 41 | const invalidLooksRareV2Trade = new LooksRareV2Trade([invalidLooksRareV2Data]) 42 | 43 | const seaportTrade = new SeaportTrade([seaportV1_4DataETHRecent]) 44 | const seaportValue = seaportTrade.getTotalPrice(ETH_ADDRESS) 45 | 46 | let WETH_USDC_V3: Pool 47 | let USDC_DAI_V2: Pair 48 | let WETH_USDC_V2: Pair 49 | 50 | beforeEach(async () => { 51 | ;({ WETH_USDC_V3, USDC_DAI_V2, WETH_USDC_V2 } = await getUniswapPools(15360000)) 52 | }) 53 | 54 | it('erc20 -> 1 looksrare nft', async () => { 55 | const erc20Trade = buildTrade([ 56 | await V3Trade.fromRoute( 57 | new RouteV3([WETH_USDC_V3], USDC, ETHER), 58 | CurrencyAmount.fromRawAmount(ETHER, looksRareV2Value.toString()), 59 | TradeType.EXACT_OUTPUT 60 | ), 61 | ]) 62 | const opts = swapOptions({ recipient: ROUTER_AS_RECIPIENT }) 63 | const uniswapTrade = new UniswapTrade(erc20Trade, opts) 64 | 65 | const methodParameters = SwapRouter.swapCallParameters([uniswapTrade, looksRareV2Trade], { 66 | sender: FORGE_SENDER_ADDRESS, 67 | }) 68 | registerFixture('_ERC20_FOR_1_LOOKSRARE_NFT', methodParameters) 69 | expect(methodParameters.value).to.eq('0x00') 70 | }) 71 | 72 | it('weth -> 1 looksrare nft with Permit', async () => { 73 | const permit2Data = makePermit(WETH_ADDRESS(1), looksRareV2Value.toString(), '0', FORGE_ROUTER_ADDRESS) 74 | const signature = await generatePermitSignature(permit2Data, wallet, 1, FORGE_PERMIT2_ADDRESS) 75 | const UnwrapWETHData = { 76 | ...permit2Data, 77 | signature, 78 | } 79 | const UnwrapWETHCommand = new UnwrapWETH(looksRareV2Value, 1, UnwrapWETHData) 80 | 81 | const methodParameters = SwapRouter.swapCallParameters([UnwrapWETHCommand, looksRareV2Trade], { 82 | sender: FORGE_ROUTER_ADDRESS, 83 | }) 84 | registerFixture('_PERMIT_AND_WETH_FOR_1_LOOKSRARE_NFT', methodParameters) 85 | expect(methodParameters.value).to.eq('0x00') 86 | }) 87 | 88 | it('weth -> 1 looksrare nft without Permit', async () => { 89 | const UnwrapWETHCommand = new UnwrapWETH(looksRareV2Value, 1) 90 | 91 | const methodParameters = SwapRouter.swapCallParameters([UnwrapWETHCommand, looksRareV2Trade], { 92 | sender: FORGE_SENDER_ADDRESS, 93 | }) 94 | registerFixture('_WETH_FOR_1_LOOKSRARE_NFT', methodParameters) 95 | expect(methodParameters.value).to.eq('0x00') 96 | }) 97 | 98 | it('erc20 + eth -> 1 looksrare nft', async () => { 99 | const halfOrderPriceWETH = CurrencyAmount.fromRawAmount(WETH, looksRareV2Trade.getTotalPrice().div(2).toString()) 100 | const halfLooksRarePriceUSDC = (await WETH_USDC_V3.getInputAmount(halfOrderPriceWETH))[0] 101 | const erc20Trade = buildTrade([ 102 | await V3Trade.fromRoute( 103 | new RouteV3([WETH_USDC_V3], USDC, ETHER), 104 | halfLooksRarePriceUSDC, // do not send enough USDC to cover entire cost 105 | TradeType.EXACT_INPUT 106 | ), 107 | ]) 108 | const opts = swapOptions({ recipient: ROUTER_AS_RECIPIENT }) 109 | const uniswapTrade = new UniswapTrade(erc20Trade, opts) 110 | 111 | const methodParameters = SwapRouter.swapCallParameters([uniswapTrade, looksRareV2Trade], { 112 | sender: FORGE_SENDER_ADDRESS, 113 | }) 114 | registerFixture('_ERC20_AND_ETH_FOR_1_LOOKSRARE_NFT', methodParameters) 115 | expect(methodParameters.value).to.not.eq('0') 116 | }) 117 | 118 | it('erc20 -> 1 looksRare nft & 1 seaport nft', async () => { 119 | const totalValue = looksRareV2Value.add(seaportValue).toString() 120 | 121 | const erc20Trade = buildTrade([ 122 | await V3Trade.fromRoute( 123 | new RouteV3([WETH_USDC_V3], USDC, ETHER), 124 | CurrencyAmount.fromRawAmount(ETHER, totalValue), 125 | TradeType.EXACT_OUTPUT 126 | ), 127 | ]) 128 | const opts = swapOptions({ recipient: ROUTER_AS_RECIPIENT }) 129 | const uniswapTrade = new UniswapTrade(erc20Trade, opts) 130 | 131 | const methodParameters = SwapRouter.swapCallParameters([uniswapTrade, looksRareV2Trade, seaportTrade], { 132 | sender: FORGE_SENDER_ADDRESS, 133 | }) 134 | registerFixture('_ERC20_FOR_1_LOOKSRARE_NFT_1_SEAPORT_NFT', methodParameters) 135 | expect(methodParameters.value).to.eq('0x00') 136 | }) 137 | 138 | it('erc20 + eth -> 1 looksRare nft & 1 seaport nft1', async () => { 139 | const erc20Trade = buildTrade([ 140 | await V3Trade.fromRoute( 141 | new RouteV3([WETH_USDC_V3], USDC, ETHER), 142 | CurrencyAmount.fromRawAmount(ETHER, looksRareV2Value.toString()), 143 | TradeType.EXACT_OUTPUT 144 | ), 145 | ]) 146 | const opts = swapOptions({ recipient: ROUTER_AS_RECIPIENT }) 147 | const uniswapTrade = new UniswapTrade(erc20Trade, opts) 148 | 149 | const methodParameters = SwapRouter.swapCallParameters([uniswapTrade, looksRareV2Trade, seaportTrade], { 150 | sender: FORGE_SENDER_ADDRESS, 151 | }) 152 | registerFixture('_ERC20_AND_ETH_FOR_1_LOOKSRARE_NFT_1_SEAPORT_NFT', methodParameters) 153 | expect(hexToDecimalString(methodParameters.value)).to.eq(seaportValue.toString()) 154 | }) 155 | 156 | it('2 erc20s -> 1 NFT', async () => { 157 | const erc20Trade1 = buildTrade([ 158 | await V3Trade.fromRoute( 159 | new RouteV3([WETH_USDC_V3], USDC, ETHER), 160 | CurrencyAmount.fromRawAmount(ETHER, looksRareV2Value.div(2).toString()), 161 | TradeType.EXACT_OUTPUT 162 | ), 163 | ]) 164 | const erc20Trade2 = buildTrade([ 165 | new V2Trade( 166 | new RouteV2([USDC_DAI_V2, WETH_USDC_V2], DAI, ETHER), 167 | CurrencyAmount.fromRawAmount(ETHER, looksRareV2Value.div(2).toString()), 168 | TradeType.EXACT_OUTPUT 169 | ), 170 | ]) 171 | const opts = swapOptions({ recipient: ROUTER_AS_RECIPIENT }) 172 | const uniswapTrade1 = new UniswapTrade(erc20Trade1, opts) 173 | const uniswapTrade2 = new UniswapTrade(erc20Trade2, opts) 174 | 175 | const methodParameters = SwapRouter.swapCallParameters([uniswapTrade1, uniswapTrade2, looksRareV2Trade], { 176 | sender: FORGE_SENDER_ADDRESS, 177 | }) 178 | registerFixture('_2_ERC20s_FOR_1_NFT', methodParameters) 179 | expect(methodParameters.value).to.eq('0x00') 180 | }) 181 | 182 | it('erc20 -> 1 invalid NFT', async () => { 183 | const erc20Trade = buildTrade([ 184 | await V3Trade.fromRoute( 185 | new RouteV3([WETH_USDC_V3], USDC, ETHER), 186 | CurrencyAmount.fromRawAmount(ETHER, looksRareV2Value.toString()), 187 | TradeType.EXACT_OUTPUT 188 | ), 189 | ]) 190 | const opts = swapOptions({ recipient: ROUTER_AS_RECIPIENT }) 191 | const uniswapTrade = new UniswapTrade(erc20Trade, opts) 192 | 193 | const methodParameters = SwapRouter.swapCallParameters([uniswapTrade, invalidLooksRareV2Trade], { 194 | sender: FORGE_SENDER_ADDRESS, 195 | }) 196 | registerFixture('_ERC20_FOR_1_INVALID_NFT', methodParameters) 197 | expect(methodParameters.value).to.eq('0x00') 198 | }) 199 | 200 | it('erc20 -> 2 NFTs partial fill', async () => { 201 | const totalValue = looksRareV2Value.add(seaportValue).toString() 202 | 203 | const erc20Trade = buildTrade([ 204 | await V3Trade.fromRoute( 205 | new RouteV3([WETH_USDC_V3], USDC, ETHER), 206 | CurrencyAmount.fromRawAmount(ETHER, totalValue), 207 | TradeType.EXACT_OUTPUT 208 | ), 209 | ]) 210 | const opts = swapOptions({ recipient: ROUTER_AS_RECIPIENT }) 211 | const uniswapTrade = new UniswapTrade(erc20Trade, opts) 212 | 213 | // invalid looks rare trade to make it a partial fill 214 | const methodParameters = SwapRouter.swapCallParameters([uniswapTrade, invalidLooksRareV2Trade, seaportTrade], { 215 | sender: FORGE_SENDER_ADDRESS, 216 | }) 217 | registerFixture('_ERC20_FOR_NFTS_PARTIAL_FILL', methodParameters) 218 | expect(methodParameters.value).to.eq('0x00') 219 | }) 220 | }) 221 | }) 222 | -------------------------------------------------------------------------------- /test/nftTrades.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { BigNumber, utils, Wallet } from 'ethers' 3 | import { hexToDecimalString } from './utils/hexToDecimalString' 4 | import { expandTo18DecimalsBN } from '../src/utils/numbers' 5 | import { SwapRouter } from '../src/swapRouter' 6 | import { TokenType } from '../src/entities/NFTTrade' 7 | import { FoundationTrade, FoundationData } from '../src/entities/protocols/foundation' 8 | import { SeaportTrade } from '../src/entities/protocols/seaport' 9 | import { seaportV1_5DataETH } from './orders/seaportV1_5' 10 | import { NFTXTrade, NFTXData } from '../src/entities/protocols/nftx' 11 | import { NFT20Trade, NFT20Data } from '../src/entities/protocols/nft20' 12 | import { x2y2Orders } from './orders/x2y2' 13 | import { SudoswapTrade, SudoswapData } from '../src/entities/protocols/sudoswap' 14 | import { CryptopunkTrade, CryptopunkData } from '../src/entities/protocols/cryptopunk' 15 | import { X2Y2Data, X2Y2Trade } from '../src/entities/protocols/x2y2' 16 | import { registerFixture } from './forge/writeInterop' 17 | import { seaportV1_4DataETH, seaportV1_4DataETHRecent, seaportV1_4DataERC20 } from './orders/seaportV1_4' 18 | import { FORGE_PERMIT2_ADDRESS, FORGE_ROUTER_ADDRESS, TEST_RECIPIENT_ADDRESS } from './utils/addresses' 19 | import { ETH_ADDRESS } from '../src/utils/constants' 20 | import { generatePermitSignature, makePermit } from './utils/permit2' 21 | import { ElementTrade } from '../src/entities/protocols/element-market' 22 | import { elementDataETH, elementDataETH_WithFees } from './orders/element' 23 | import { LooksRareV2Data, LooksRareV2Trade } from '../src/entities/protocols/looksRareV2' 24 | import { looksRareV2Orders } from './orders/looksRareV2' 25 | 26 | describe('SwapRouter', () => { 27 | const recipient = TEST_RECIPIENT_ADDRESS 28 | const looksRareV2Data: LooksRareV2Data = { 29 | apiOrder: looksRareV2Orders[0], 30 | taker: recipient, 31 | } 32 | 33 | describe('#swapNFTCallParameters', () => { 34 | it('returns hex number value in Method Parameters', async () => { 35 | const foundationData: FoundationData = { 36 | referrer: '0x459e213D8B5E79d706aB22b945e3aF983d51BC4C', 37 | tokenAddress: '0xEf96021Af16BD04918b0d87cE045d7984ad6c38c', 38 | tokenId: 32, 39 | price: expandTo18DecimalsBN(0.01), 40 | recipient, 41 | } 42 | 43 | const foundationTrade = new FoundationTrade([foundationData]) 44 | const methodParameters = SwapRouter.swapNFTCallParameters([foundationTrade]) 45 | expect(methodParameters.value).to.eq('0x2386f26fc10000') 46 | }) 47 | }) 48 | 49 | describe('Foundation', () => { 50 | // buyItem from block 15725945 51 | const foundationData: FoundationData = { 52 | referrer: '0x459e213D8B5E79d706aB22b945e3aF983d51BC4C', 53 | tokenAddress: '0xEf96021Af16BD04918b0d87cE045d7984ad6c38c', 54 | tokenId: 32, 55 | price: expandTo18DecimalsBN(0.01), 56 | recipient, 57 | } 58 | 59 | it('encodes a single foundation trade', async () => { 60 | const foundationTrade = new FoundationTrade([foundationData]) 61 | const methodParameters = SwapRouter.swapNFTCallParameters([foundationTrade]) 62 | const methodParametersV2 = SwapRouter.swapCallParameters(foundationTrade) 63 | registerFixture('_FOUNDATION_BUY_ITEM', methodParametersV2) 64 | expect(hexToDecimalString(methodParameters.value)).to.eq(foundationData.price.toString()) 65 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 66 | expect(methodParameters.value).to.eq(methodParametersV2.value) 67 | }) 68 | }) 69 | 70 | describe('NFTX', () => { 71 | // buyItems from block 17029002 72 | const price: BigNumber = BigNumber.from('2016360357822219079') 73 | const nftxPurchase: NFTXData = { 74 | recipient, 75 | vaultId: 392, // milady vault ID 76 | tokenAddress: '0x5Af0D9827E0c53E4799BB226655A1de152A425a5', 77 | tokenIds: [7132], 78 | value: price, 79 | swapCalldata: 80 | '0xd9627aa400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000001bfb8d0ff32c43470000000000000000000000000000000000000000000000000e27c49886e6000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000227c7df69d3ed1ae7574a1a7685fded90292eb48869584cd00000000000000000000000010000000000000000000000000000000000000110000000000000000000000000000000000000000000000465b3a7f1b643618cb', 81 | } 82 | 83 | it('encodes buying an NFT from a single NFTX vault', async () => { 84 | const nftxTrade = new NFTXTrade([nftxPurchase]) 85 | const methodParameters = SwapRouter.swapNFTCallParameters([nftxTrade]) 86 | const methodParametersV2 = SwapRouter.swapCallParameters(nftxTrade) 87 | registerFixture('_NFTX_BUY_ITEMS', methodParametersV2) 88 | expect(hexToDecimalString(methodParameters.value)).to.eq(price.toString()) 89 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 90 | expect(methodParameters.value).to.eq(methodParametersV2.value) 91 | }) 92 | }) 93 | 94 | describe('LooksRareV2', () => { 95 | it('encodes buying one ERC721 from LooksRare', async () => { 96 | // buy items from block 17030830 97 | const looksRareV2Data: LooksRareV2Data = { 98 | apiOrder: looksRareV2Orders[0], 99 | taker: recipient, 100 | } 101 | const looksRareV2Trade = new LooksRareV2Trade([looksRareV2Data]) 102 | const methodParameters = SwapRouter.swapNFTCallParameters([looksRareV2Trade]) 103 | const methodParametersV2 = SwapRouter.swapCallParameters(looksRareV2Trade) 104 | registerFixture('_LOOKSRARE_V2_BUY_ITEM_721', methodParametersV2) 105 | expect(hexToDecimalString(methodParameters.value)).to.eq(looksRareV2Data.apiOrder.price) 106 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 107 | expect(methodParameters.value).to.eq(methodParametersV2.value) 108 | }) 109 | 110 | it('encodes batch buying 2 ERC721s from LooksRare', async () => { 111 | // buy items from block 17037140 112 | const looksRareV2Data1: LooksRareV2Data = { 113 | apiOrder: looksRareV2Orders[1], 114 | taker: recipient, 115 | } 116 | const looksRareV2Data2: LooksRareV2Data = { 117 | apiOrder: looksRareV2Orders[2], 118 | taker: recipient, 119 | } 120 | const totalPrice = BigNumber.from(looksRareV2Data1.apiOrder.price).add(looksRareV2Data2.apiOrder.price) 121 | const looksRareV2Trade = new LooksRareV2Trade([looksRareV2Data1, looksRareV2Data2]) 122 | const methodParameters = SwapRouter.swapNFTCallParameters([looksRareV2Trade]) 123 | const methodParametersV2 = SwapRouter.swapCallParameters(looksRareV2Trade) 124 | registerFixture('_LOOKSRARE_V2_BATCH_BUY_ITEM_721', methodParametersV2) 125 | expect(hexToDecimalString(methodParameters.value)).to.eq(totalPrice.toString()) 126 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 127 | expect(methodParameters.value).to.eq(methodParametersV2.value) 128 | }) 129 | }) 130 | 131 | describe('Element Market', () => { 132 | // buy an ERC721 from block 16627214 133 | it('encodes buying one ERC721 from Element', async () => { 134 | const elementTrade = new ElementTrade([elementDataETH]) 135 | const methodParameters = SwapRouter.swapNFTCallParameters([elementTrade]) 136 | const methodParametersV2 = SwapRouter.swapCallParameters(elementTrade) 137 | registerFixture('_ELEMENT_BUY_ITEM_721', methodParametersV2) 138 | expect(hexToDecimalString(methodParameters.value)).to.eq(elementDataETH.order.erc20TokenAmount) 139 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 140 | expect(methodParameters.value).to.eq(methodParametersV2.value) 141 | }) 142 | 143 | it('encodes buying one ERC721 with fees from Element', async () => { 144 | const elementTrade = new ElementTrade([elementDataETH_WithFees]) 145 | const methodParameters = SwapRouter.swapNFTCallParameters([elementTrade]) 146 | const methodParametersV2 = SwapRouter.swapCallParameters(elementTrade) 147 | const value = elementTrade.getOrderPriceIncludingFees(elementDataETH_WithFees.order) 148 | registerFixture('_ELEMENT_BUY_ITEM_721_WITH_FEES', methodParametersV2) 149 | /// value should be equal to erc20amount plus fees 150 | expect(hexToDecimalString(methodParameters.value)).to.eq(value.toString()) 151 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 152 | expect(methodParameters.value).to.eq(methodParametersV2.value) 153 | expect(methodParameters.value).to.not.eq(elementDataETH_WithFees.order.erc20TokenAmount) 154 | }) 155 | }) 156 | 157 | describe('X2Y2', () => { 158 | const x2y2SignedOrder721 = x2y2Orders[0] 159 | const x2y2SignedOrder1155 = x2y2Orders[1] 160 | const ENS_NFT_ADDR = '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85' 161 | const CAMEO_ADDRESS = '0x93317E87a3a47821803CAADC54Ae418Af80603DA' 162 | 163 | const x2y2_721_Data: X2Y2Data = { 164 | signedInput: x2y2SignedOrder721.input, 165 | recipient, 166 | price: x2y2SignedOrder721.price, 167 | tokenId: x2y2SignedOrder721.token_id, 168 | tokenAddress: ENS_NFT_ADDR, 169 | tokenType: TokenType.ERC721, 170 | } 171 | 172 | const x2y2_1155_Data: X2Y2Data = { 173 | signedInput: x2y2SignedOrder1155.input, 174 | recipient, 175 | price: x2y2SignedOrder1155.price, 176 | tokenId: x2y2SignedOrder1155.token_id, 177 | tokenAddress: CAMEO_ADDRESS, 178 | tokenType: TokenType.ERC1155, 179 | tokenAmount: 1, 180 | } 181 | 182 | it('encodes buying one ERC-721 from X2Y2', async () => { 183 | const x2y2Trade = new X2Y2Trade([x2y2_721_Data]) 184 | const methodParameters = SwapRouter.swapNFTCallParameters([x2y2Trade]) 185 | const methodParametersV2 = SwapRouter.swapCallParameters(x2y2Trade) 186 | registerFixture('_X2Y2_721_BUY_ITEM', methodParametersV2) 187 | expect(hexToDecimalString(methodParameters.value)).to.eq(x2y2SignedOrder721.price) 188 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 189 | expect(methodParameters.value).to.eq(methodParametersV2.value) 190 | }) 191 | it('encodes buying one ERC-1155 from X2Y2', async () => { 192 | const x2y2Trade = new X2Y2Trade([x2y2_1155_Data]) 193 | const methodParameters = SwapRouter.swapNFTCallParameters([x2y2Trade]) 194 | const methodParametersV2 = SwapRouter.swapCallParameters(x2y2Trade) 195 | registerFixture('_X2Y2_1155_BUY_ITEM', methodParametersV2) 196 | expect(hexToDecimalString(methodParameters.value)).to.eq(x2y2SignedOrder1155.price) 197 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 198 | expect(methodParameters.value).to.eq(methodParametersV2.value) 199 | }) 200 | }) 201 | 202 | describe('SeaportV1_5', () => { 203 | it('encodes buying two NFTs from Seaport v1.5 with ETH', async () => { 204 | const seaportTrade = new SeaportTrade([seaportV1_5DataETH]) 205 | const value = seaportTrade.getTotalPrice(ETH_ADDRESS) 206 | const methodParameters = SwapRouter.swapNFTCallParameters([seaportTrade]) 207 | const methodParametersV2 = SwapRouter.swapCallParameters(seaportTrade) 208 | registerFixture('_SEAPORT_V1_5_BUY_ITEMS_ETH', methodParametersV2) 209 | expect(hexToDecimalString(methodParameters.value)).to.eq(value.toString()) 210 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 211 | expect(methodParameters.value).to.eq(methodParametersV2.value) 212 | }) 213 | }) 214 | 215 | describe('SeaportV1_4', () => { 216 | const wallet = new Wallet(utils.zeroPad('0x1234', 32)) 217 | 218 | it('encodes buying two NFTs from Seaport v1.4 with ETH', async () => { 219 | const seaportV1_4Trade = new SeaportTrade([seaportV1_4DataETH]) 220 | const value = seaportV1_4Trade.getTotalPrice(ETH_ADDRESS) 221 | const methodParameters = SwapRouter.swapNFTCallParameters([seaportV1_4Trade]) 222 | const methodParametersV2 = SwapRouter.swapCallParameters(seaportV1_4Trade) 223 | registerFixture('_SEAPORT_V1_4_BUY_ITEMS_ETH', methodParametersV2) 224 | expect(hexToDecimalString(methodParameters.value)).to.eq(value.toString()) 225 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 226 | expect(methodParameters.value).to.eq(methodParametersV2.value) 227 | }) 228 | 229 | it('encodes buying 1 NFT from Seaport with ERC20, with Permit and Approve', async () => { 230 | // get the basic seaport data for ERC20 trade 231 | let seaportData = seaportV1_4DataERC20 232 | 233 | // make permit 234 | const GALA_ADDRESS = '0x15D4c048F83bd7e37d49eA4C83a07267Ec4203dA' 235 | const permit2Data = makePermit(GALA_ADDRESS, undefined, undefined, FORGE_ROUTER_ADDRESS) 236 | const signature = await generatePermitSignature(permit2Data, wallet, 1, FORGE_PERMIT2_ADDRESS) 237 | seaportData.inputTokenProcessing = [ 238 | { 239 | token: GALA_ADDRESS, 240 | protocolApproval: true, 241 | permit2TransferFrom: true, 242 | permit2Permit: { 243 | ...permit2Data, 244 | signature, 245 | }, 246 | }, 247 | ] 248 | 249 | const seaportTrade = new SeaportTrade([seaportData]) 250 | const methodParameters = SwapRouter.swapCallParameters(seaportTrade) 251 | registerFixture('_SEAPORT_V1_4_BUY_ITEMS_ERC20_PERMIT_AND_APPROVE', methodParameters) 252 | expect(hexToDecimalString(methodParameters.value)).to.eq('0') 253 | }) 254 | 255 | it('encodes buying 1 NFT from Seaport with ERC20, with Permit', async () => { 256 | // get the basic seaport data for ERC20 trade 257 | let seaportData = seaportV1_4DataERC20 258 | 259 | // add permit and transfer 260 | const GALA_ADDRESS = '0x15D4c048F83bd7e37d49eA4C83a07267Ec4203dA' 261 | const permit2Data = makePermit(GALA_ADDRESS, undefined, undefined, FORGE_ROUTER_ADDRESS) 262 | const signature = await generatePermitSignature(permit2Data, wallet, 1, FORGE_PERMIT2_ADDRESS) 263 | seaportData.inputTokenProcessing = [ 264 | { 265 | token: GALA_ADDRESS, 266 | protocolApproval: false, // no approval 267 | permit2TransferFrom: true, 268 | permit2Permit: { 269 | ...permit2Data, 270 | signature, 271 | }, 272 | }, 273 | ] 274 | 275 | const seaportTrade = new SeaportTrade([seaportData]) 276 | const methodParameters = SwapRouter.swapCallParameters(seaportTrade) 277 | registerFixture('_SEAPORT_V1_4_BUY_ITEMS_ERC20_PERMIT_NO_APPROVE', methodParameters) 278 | expect(hexToDecimalString(methodParameters.value)).to.eq('0') 279 | }) 280 | }) 281 | 282 | describe('Cryptopunk', () => { 283 | // buyItem from block 15725945 284 | const cryptopunk: CryptopunkData = { 285 | tokenId: 2976, 286 | recipient, 287 | value: BigNumber.from('76950000000000000000'), 288 | } 289 | 290 | it('encodes a single cryptopunk trade', async () => { 291 | const cryptopunkTrade = new CryptopunkTrade([cryptopunk]) 292 | const methodParameters = SwapRouter.swapNFTCallParameters([cryptopunkTrade]) 293 | const methodParametersV2 = SwapRouter.swapCallParameters(cryptopunkTrade) 294 | registerFixture('_CRYPTOPUNK_BUY_ITEM', methodParametersV2) 295 | expect(hexToDecimalString(methodParameters.value)).to.eq(cryptopunk.value.toString()) 296 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 297 | expect(methodParameters.value).to.eq(methodParametersV2.value) 298 | }) 299 | }) 300 | 301 | describe('nft20', () => { 302 | // buyItem from block 15770228 303 | const nft20Data: NFT20Data = { 304 | tokenIds: [129, 193, 278], 305 | tokenAddress: '0x6d05064fe99e40f1c3464e7310a23ffaded56e20', 306 | tokenAmounts: [1, 1, 1], 307 | recipient, 308 | fee: 0, 309 | isV3: false, 310 | value: BigNumber.from('20583701229648230'), 311 | } 312 | 313 | it('encodes an NFT20 trade with three items', async () => { 314 | const nft20Trade = new NFT20Trade([nft20Data]) 315 | const methodParameters = SwapRouter.swapNFTCallParameters([nft20Trade]) 316 | const methodParametersV2 = SwapRouter.swapCallParameters(nft20Trade) 317 | registerFixture('_NFT20_BUY_ITEM', methodParametersV2) 318 | expect(hexToDecimalString(methodParameters.value)).to.eq(nft20Data.value.toString()) 319 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 320 | expect(methodParameters.value).to.eq(methodParametersV2.value) 321 | }) 322 | }) 323 | 324 | describe('sudoswap', () => { 325 | // buyItem from block 15770228 326 | const sudoswapData: SudoswapData = { 327 | swaps: [ 328 | { 329 | swapInfo: { 330 | pair: '0x339e7004372e04b1d59443f0ddc075efd9d80360', 331 | nftIds: [80, 35, 93], 332 | }, 333 | tokenAddress: '0xfa9937555dc20a020a161232de4d2b109c62aa9c', 334 | maxCost: '73337152777777783', 335 | }, 336 | ], 337 | nftRecipient: recipient, 338 | ethRecipient: recipient, 339 | deadline: '2000000000', 340 | } 341 | 342 | it('encodes an Sudoswap trade with three items', async () => { 343 | const sudoswapTrade = new SudoswapTrade([sudoswapData]) 344 | const methodParameters = SwapRouter.swapNFTCallParameters([sudoswapTrade]) 345 | const methodParametersV2 = SwapRouter.swapCallParameters(sudoswapTrade) 346 | registerFixture('_SUDOSWAP_BUY_ITEM', methodParametersV2) 347 | expect(hexToDecimalString(methodParameters.value)).to.eq(sudoswapData.swaps[0].maxCost.toString()) 348 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 349 | expect(methodParameters.value).to.eq(methodParametersV2.value) 350 | }) 351 | }) 352 | 353 | describe('Partial Fill', () => { 354 | const invalidLooksRareV2Data: LooksRareV2Data = { 355 | ...looksRareV2Data, 356 | apiOrder: { ...looksRareV2Data.apiOrder, itemIds: ['1'] }, 357 | } 358 | 359 | it('encodes partial fill for multiple trades between protocols', async () => { 360 | const invalidLooksRareV2Trade = new LooksRareV2Trade([invalidLooksRareV2Data]) 361 | const looksRareV2Value = invalidLooksRareV2Trade.getTotalPrice() 362 | const seaportTrade = new SeaportTrade([seaportV1_4DataETHRecent]) 363 | const seaportValue = seaportTrade.getTotalPrice(ETH_ADDRESS) 364 | const totalValue = looksRareV2Value.add(seaportValue).toString() 365 | 366 | const methodParameters = SwapRouter.swapNFTCallParameters([invalidLooksRareV2Trade, seaportTrade]) 367 | const methodParametersV2 = SwapRouter.swapCallParameters([invalidLooksRareV2Trade, seaportTrade]) 368 | registerFixture('_PARTIAL_FILL', methodParametersV2) 369 | expect(hexToDecimalString(methodParameters.value)).to.eq(totalValue) 370 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 371 | expect(methodParameters.value).to.eq(methodParametersV2.value) 372 | }) 373 | 374 | it('encodes partial fill for multiple swaps within the same protocol', async () => { 375 | // buyItem from block 15725945 376 | const foundationData1: FoundationData = { 377 | referrer: '0x459e213D8B5E79d706aB22b945e3aF983d51BC4C', 378 | tokenAddress: '0xEf96021Af16BD04918b0d87cE045d7984ad6c38c', 379 | tokenId: 32, 380 | price: expandTo18DecimalsBN(0.01), 381 | recipient, 382 | } 383 | 384 | // buyItem from block 15725945 385 | const foundationData2: FoundationData = { 386 | referrer: '0x459e213D8B5E79d706aB22b945e3aF983d51BC4C', 387 | tokenAddress: '0xEf96021Af16BD04918b0d87cE045d7984ad6c38c', 388 | tokenId: 100, // invalid not for sale 389 | price: expandTo18DecimalsBN(0.01), 390 | recipient, 391 | } 392 | 393 | const value = BigNumber.from(foundationData1.price).add(foundationData2.price) 394 | 395 | const foundationTrade = new FoundationTrade([foundationData1, foundationData2]) 396 | const methodParameters = SwapRouter.swapNFTCallParameters([foundationTrade]) 397 | const methodParametersV2 = SwapRouter.swapCallParameters(foundationTrade) 398 | registerFixture('_PARTIAL_FILL_WITHIN_PROTOCOL', methodParametersV2) 399 | expect(hexToDecimalString(methodParameters.value)).to.eq(value.toString()) 400 | expect(methodParameters.calldata).to.eq(methodParametersV2.calldata) 401 | expect(methodParameters.value).to.eq(methodParametersV2.value) 402 | }) 403 | }) 404 | }) 405 | -------------------------------------------------------------------------------- /test/orders/element.ts: -------------------------------------------------------------------------------- 1 | import { ElementData, OrderSignature, ERC721SellOrder } from '../../src/entities/protocols/element-market' 2 | import { ZERO_ADDRESS } from '../../src/utils/constants' 3 | import { TEST_RECIPIENT_ADDRESS } from '../utils/addresses' 4 | 5 | export const elementOrderETH: ERC721SellOrder = { 6 | maker: '0xABd6a19345943dD175026Cdb52902FD3392a3262', 7 | taker: '0x75B6568025f463a98fB01082eEb6dCe04efA3Ae4', 8 | expiry: '7199994275163324196', 9 | nonce: '3', 10 | erc20Token: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', 11 | erc20TokenAmount: '55000000000000000', 12 | fees: [], 13 | nft: '0x4C69dBc3a2Aa3476c3F7a1227ab70950DB1F4858', 14 | nftId: '998', 15 | } 16 | 17 | export const elementSignatureETH: OrderSignature = { 18 | signatureType: 0, 19 | v: 27, 20 | r: '0x59ceb2bc0e21029209e6cfa872b1224631b01da3e19d25fad9b929b8be4e6f60', 21 | s: '0x72cadb8ed8a5bf5938829f888ff60c9ebe163954dc15af3e5d6014e8f6801b83', 22 | } 23 | 24 | export const elementOrderETH_WithFees: ERC721SellOrder = { 25 | maker: '0xd9d9c1141239f2b7f0604cde48bf3d6e809f4aeb', 26 | taker: '0x0000000000000000000000000000000000000000', 27 | expiry: '7212658763627609514', 28 | nonce: '26', 29 | erc20Token: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 30 | erc20TokenAmount: '36340000000000000', 31 | fees: [ 32 | { 33 | recipient: '0x00ca62445b06a9adc1879a44485b4efdcb7b75f3', 34 | amount: '197500000000000', 35 | feeData: '0x', 36 | }, 37 | { 38 | recipient: '0x44403685c1335a42a1d88ecf781b270a20e973ee', 39 | amount: '2962500000000000', 40 | feeData: '0x', 41 | }, 42 | ], 43 | nft: '0x4c69dbc3a2aa3476c3f7a1227ab70950db1f4858', 44 | nftId: '2540', 45 | } 46 | 47 | export const elementOrderETH_WithFees_Signature: OrderSignature = { 48 | signatureType: 0, 49 | v: 28, 50 | r: '0x5b80d409a0085b624d82fa6c60a4a9ec28dd898f243ce7c058f9b109c9de927f', 51 | s: '0x3401627e461312e0069f4e8dab96c120e3b62f2fa1ce0e52927bca00fd70ef0c', 52 | } 53 | 54 | export const elementDataETH: ElementData = { 55 | order: elementOrderETH, 56 | signature: elementSignatureETH, 57 | recipient: elementOrderETH.taker, 58 | } 59 | 60 | export const elementDataETH_WithFees: ElementData = { 61 | order: elementOrderETH_WithFees, 62 | signature: elementOrderETH_WithFees_Signature, 63 | recipient: TEST_RECIPIENT_ADDRESS, 64 | } 65 | -------------------------------------------------------------------------------- /test/orders/looksRareV2.ts: -------------------------------------------------------------------------------- 1 | import { LRV2APIOrder } from '../../src/entities/protocols/looksRareV2' 2 | 3 | export const looksRareV2Orders: LRV2APIOrder[] = [ 4 | { 5 | id: 'MTE1MjkyMTUwNDYwNjg0NzMyNw==', 6 | hash: '0x348f5ea7d561cf3e56f3db0385645b56a3c52cbf32aa5a8cd3d36cc74063d903', 7 | quoteType: 1, 8 | globalNonce: '0', 9 | subsetNonce: '0', 10 | orderNonce: '0', 11 | collection: '0xaa107ccfe230a29c345fd97bc6eb9bd2fccd0750', 12 | currency: '0x0000000000000000000000000000000000000000', 13 | signer: '0x08ab6bc579745623520791f31a15c4eb4e29946b', 14 | strategyId: 0, 15 | collectionType: 0, 16 | startTime: 1681221684, 17 | endTime: 1683285203, 18 | price: '200000000000000000', 19 | additionalParameters: '0x', 20 | signature: 21 | '0x2c0472f01e3ced7a2d1d59bdaf4c3f5bb9c0d1b8371347ef83b2de04ceae78f11bf8b97a4254215bdebd9c4266265acdb062528e6dd863e332db19031e767f501b', 22 | createdAt: '2023-04-03T21:38:48.903Z', 23 | merkleRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', 24 | merkleProof: [], 25 | amounts: ['1'], 26 | itemIds: ['905'], 27 | status: 'VALID', 28 | }, 29 | { 30 | id: '', 31 | hash: '', 32 | quoteType: 1, 33 | globalNonce: '0', 34 | subsetNonce: '0', 35 | orderNonce: '0', 36 | collection: '0xaa107ccfe230a29c345fd97bc6eb9bd2fccd0750', 37 | currency: '0x0000000000000000000000000000000000000000', 38 | signer: '0x101f791249f225a9130d7e6020be444d281aae6c', 39 | strategyId: 0, 40 | collectionType: 0, 41 | startTime: 1681281786, 42 | endTime: 1683873169, 43 | price: '200000000000000000', 44 | additionalParameters: '0x', 45 | signature: 46 | '0x2a6e1d159bda2e2e2db65328599eb22fb224f5760d52cb5cddd03e0a69b99c9059cf2ffdb990ef49a5cf0cee063f51d562b66b6d02fc6a4ff6a48d144109c8381b', 47 | createdAt: '', 48 | merkleRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', 49 | merkleProof: [], 50 | amounts: ['1'], 51 | itemIds: ['3417'], 52 | status: 'VALID', 53 | }, 54 | { 55 | id: '', 56 | hash: '', 57 | quoteType: 1, 58 | globalNonce: '0', 59 | subsetNonce: '0', 60 | orderNonce: '0', 61 | collection: '0xaa107ccfe230a29c345fd97bc6eb9bd2fccd0750', 62 | currency: '0x0000000000000000000000000000000000000000', 63 | signer: '0xfbb3daa40f1da3b847292c7544afef2a6d2106c8', 64 | strategyId: 0, 65 | collectionType: 0, 66 | startTime: 1681211346, 67 | endTime: 1683280045, 68 | price: '209900000000000000', 69 | additionalParameters: '0x', 70 | signature: 71 | '0xb85c978f8593f4841caff4bfb30872ee726e35ba43cb382a8cad40fff163275910ab2d0496d9be1954e4eb60710be850574a41adad24a5921de506c1acc5a1821b', 72 | createdAt: '', 73 | merkleRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', 74 | merkleProof: [], 75 | amounts: ['1'], 76 | itemIds: ['933'], 77 | status: 'VALID', 78 | }, 79 | ] 80 | -------------------------------------------------------------------------------- /test/orders/seaportV1_4.ts: -------------------------------------------------------------------------------- 1 | import { SeaportData, ConsiderationItem } from '../../src/entities/protocols/seaport' 2 | import { BigNumber } from 'ethers' 3 | import { TEST_RECIPIENT_ADDRESS } from '../utils/addresses' 4 | 5 | export const seaportV1_4DataETH: SeaportData = { 6 | items: [ 7 | { 8 | parameters: { 9 | offerer: '0xab0d2ad721399c2e8ec6f340d1e09cbbed7c5f2b', 10 | offer: [ 11 | { 12 | itemType: 3, 13 | token: '0x4f3adef2f4096740774a955e912b5f03f2c7ba2b', 14 | identifierOrCriteria: '1', 15 | startAmount: '3', 16 | endAmount: '3', 17 | }, 18 | ], 19 | consideration: [ 20 | { 21 | itemType: 0, 22 | token: '0x0000000000000000000000000000000000000000', 23 | identifierOrCriteria: '0', 24 | startAmount: '80550000000000000', 25 | endAmount: '80550000000000000', 26 | recipient: '0xab0d2ad721399c2e8ec6f340d1e09cbbed7c5f2b', 27 | }, 28 | { 29 | itemType: 0, 30 | token: '0x0000000000000000000000000000000000000000', 31 | identifierOrCriteria: '0', 32 | startAmount: '450000000000000', 33 | endAmount: '450000000000000', 34 | recipient: '0x0000a26b00c1f0df003000390027140000faa719', 35 | }, 36 | { 37 | itemType: 0, 38 | token: '0x0000000000000000000000000000000000000000', 39 | identifierOrCriteria: '0', 40 | startAmount: '9000000000000000', 41 | endAmount: '9000000000000000', 42 | recipient: '0x4401a1667dafb63cff06218a69ce11537de9a101', 43 | }, 44 | ], 45 | startTime: '1678725221', 46 | endTime: '1678811621', 47 | orderType: 1, 48 | zone: '0x004c00500000ad104d7dbd00e3ae0a5c00560c00', 49 | zoneHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 50 | salt: '24446860302761739304752683030156737591518664810215442929816957436415552570299', 51 | conduitKey: '0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000', 52 | totalOriginalConsiderationItems: 3, 53 | }, 54 | signature: 55 | '0x898c4e840db735a6ffb9f4a42920aa36a182940d85c44af97bd0c0bc672573d6b08a70a06c55a125d9ec3c484950b6e86981b4ac937037375f56d4df237bbf9f', 56 | }, 57 | ], 58 | recipient: TEST_RECIPIENT_ADDRESS, 59 | protocolAddress: '0x00000000000001ad428e4906aE43D8F9852d0dD6', 60 | } 61 | 62 | export const seaportV1_4DataETHRecent: SeaportData = { 63 | items: [ 64 | { 65 | parameters: { 66 | offerer: '0xdc84079993e56499eed18b938071f551750d0e89', 67 | zone: '0x004c00500000ad104d7dbd00e3ae0a5c00560c00', 68 | offer: [ 69 | { 70 | itemType: 2, 71 | token: '0xcee3c4f9f52ce89e310f19b363a9d4f796b56a68', 72 | identifierOrCriteria: '277', 73 | startAmount: '1', 74 | endAmount: '1', 75 | }, 76 | ], 77 | consideration: [ 78 | { 79 | itemType: 0, 80 | token: '0x0000000000000000000000000000000000000000', 81 | identifierOrCriteria: '0', 82 | startAmount: '17542000000000000', 83 | endAmount: '17542000000000000', 84 | recipient: '0xdc84079993e56499eed18b938071f551750d0e89', 85 | }, 86 | { 87 | itemType: 0, 88 | token: '0x0000000000000000000000000000000000000000', 89 | identifierOrCriteria: '0', 90 | startAmount: '489999999999999', 91 | endAmount: '489999999999999', 92 | recipient: '0x0000a26b00c1f0df003000390027140000faa719', 93 | }, 94 | { 95 | itemType: 0, 96 | token: '0x0000000000000000000000000000000000000000', 97 | identifierOrCriteria: '0', 98 | startAmount: '1567999999999999', 99 | endAmount: '1567999999999999', 100 | recipient: '0x1c12aea4bc03469ce2d10227f6e6e63099f42424', 101 | }, 102 | ], 103 | orderType: 0, 104 | startTime: '1681289091', 105 | endTime: '1689065091', 106 | zoneHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 107 | salt: '24446860302761739304752683030156737591518664810215442929818314405008806322811', 108 | conduitKey: '0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000', 109 | totalOriginalConsiderationItems: '3', 110 | }, 111 | signature: 112 | '0x3ad4ad346a8a807051b3601ec311af516f6cc15db1654e082c33e5721de4c1ac9b9254087fc55aeebc142703782998b2b06859a450eb616fd144ae519030bb45', 113 | }, 114 | ], 115 | recipient: TEST_RECIPIENT_ADDRESS, 116 | protocolAddress: '0x00000000000001ad428e4906aE43D8F9852d0dD6', 117 | } 118 | 119 | export const seaportV1_4DataERC20: SeaportData = { 120 | items: [ 121 | { 122 | parameters: { 123 | offerer: '0x5e755d47c1874da844b31e08ba70f11d047f96d6', 124 | offer: [ 125 | { 126 | itemType: 3, 127 | token: '0xc36cf0cfcb5d905b8b513860db0cfe63f6cf9f5c', 128 | identifierOrCriteria: '425352958651173079329218259289710264320000', 129 | startAmount: '2', 130 | endAmount: '2', 131 | }, 132 | ], 133 | consideration: [ 134 | { 135 | itemType: 1, 136 | token: '0x15d4c048f83bd7e37d49ea4c83a07267ec4203da', 137 | identifierOrCriteria: '0', 138 | startAmount: '223020000000', 139 | endAmount: '223020000000', 140 | recipient: '0x5e755d47c1874da844b31e08ba70f11d047f96d6', 141 | }, 142 | { 143 | itemType: 1, 144 | token: '0x15d4c048f83bd7e37d49ea4c83a07267ec4203da', 145 | identifierOrCriteria: '0', 146 | startAmount: '24780000000', 147 | endAmount: '24780000000', 148 | recipient: '0xa92abb0d0dd1e8e73006fc3b6229b7bd9e0d5c61', 149 | }, 150 | ], 151 | startTime: '1678218282', 152 | endTime: '1680893082', 153 | orderType: 1, 154 | zone: '0x004c00500000ad104d7dbd00e3ae0a5c00560c00', 155 | zoneHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 156 | salt: '24446860302761739304752683030156737591518664810215442929813247523325878245709', 157 | conduitKey: '0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000', 158 | totalOriginalConsiderationItems: '2', 159 | }, 160 | signature: 161 | '0x6fd0032bb132c3724b730d55deb59924b8674405ae5e523a95a56b5a258af1d9cc9de9a90aef35ba9311afaf37eb8b0904f2f32e799abf63f073470c595aeefb', 162 | }, 163 | ], 164 | recipient: TEST_RECIPIENT_ADDRESS, 165 | protocolAddress: '0x00000000000001ad428e4906aE43D8F9852d0dD6', 166 | } 167 | -------------------------------------------------------------------------------- /test/orders/seaportV1_5.ts: -------------------------------------------------------------------------------- 1 | import { ConsiderationItem, SeaportData } from '../../src/entities/protocols/seaport' 2 | import { BigNumber } from 'ethers' 3 | import { TEST_RECIPIENT_ADDRESS } from '../utils/addresses' 4 | 5 | export const seaportV1_5DataETH: SeaportData = { 6 | items: [ 7 | { 8 | parameters: { 9 | offerer: '0x7bca4682999b71d813d541a9cbf73e35216f1417', 10 | offer: [ 11 | { 12 | endAmount: '1', 13 | identifierOrCriteria: '564868729088757849349201848336735231016960', 14 | itemType: 3, 15 | startAmount: '1', 16 | token: '0xc36cf0cfcb5d905b8b513860db0cfe63f6cf9f5c', 17 | }, 18 | ], 19 | consideration: [ 20 | { 21 | endAmount: '57312500000000000', 22 | identifierOrCriteria: '0', 23 | itemType: 0, 24 | recipient: '0x7bca4682999b71d813d541a9cbf73e35216f1417', 25 | startAmount: '57312500000000000', 26 | token: '0x0000000000000000000000000000000000000000', 27 | }, 28 | { 29 | endAmount: '1637500000000000', 30 | identifierOrCriteria: '0', 31 | itemType: 0, 32 | recipient: '0x0000a26b00c1f0df003000390027140000faa719', 33 | startAmount: '1637500000000000', 34 | token: '0x0000000000000000000000000000000000000000', 35 | }, 36 | { 37 | endAmount: '6550000000000000', 38 | identifierOrCriteria: '0', 39 | itemType: 0, 40 | recipient: '0x9cfb24366131c42d041139c8abbea45f6527a9b2', 41 | startAmount: '6550000000000000', 42 | token: '0x0000000000000000000000000000000000000000', 43 | }, 44 | ], 45 | orderType: 1, 46 | startTime: '1683084765', 47 | endTime: '1683171165', 48 | zone: '0x004c00500000ad104d7dbd00e3ae0a5c00560c00', 49 | zoneHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 50 | salt: '24446860302761739304752683030156737591518664810215442929802117345480319970273', 51 | conduitKey: '0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000', 52 | totalOriginalConsiderationItems: '3', 53 | }, 54 | signature: 55 | '0xa0cfc9291bb705f32a7d4bea77e9ef4dece18d4424864abad4ea26c81a9e9d144a9dbb7fd18f6819be34a7ed4d6714ddf402255cc5d59e5789c8afef80b7380a', 56 | }, 57 | { 58 | parameters: { 59 | offerer: '0xbadb011bea1305f52f85664a755ed5921bf818ea', 60 | offer: [ 61 | { 62 | itemType: 3, 63 | token: '0xc36cf0cfcb5d905b8b513860db0cfe63f6cf9f5c', 64 | identifierOrCriteria: '580862000334041957131980454886028336955392', 65 | startAmount: '1', 66 | endAmount: '1', 67 | }, 68 | ], 69 | consideration: [ 70 | { 71 | itemType: 0, 72 | token: '0x0000000000000000000000000000000000000000', 73 | identifierOrCriteria: '0', 74 | startAmount: '95375000000000000', 75 | endAmount: '95375000000000000', 76 | recipient: '0xbadb011bea1305f52f85664a755ed5921bf818ea', 77 | }, 78 | { 79 | itemType: 0, 80 | token: '0x0000000000000000000000000000000000000000', 81 | identifierOrCriteria: '0', 82 | startAmount: '2725000000000000', 83 | endAmount: '2725000000000000', 84 | recipient: '0x0000a26b00c1f0df003000390027140000faa719', 85 | }, 86 | { 87 | itemType: 0, 88 | token: '0x0000000000000000000000000000000000000000', 89 | identifierOrCriteria: '0', 90 | startAmount: '10900000000000000', 91 | endAmount: '10900000000000000', 92 | recipient: '0x9cfb24366131c42d041139c8abbea45f6527a9b2', 93 | }, 94 | ], 95 | orderType: 1, 96 | startTime: '1683052606', 97 | endTime: '1685731006', 98 | zone: '0x004c00500000ad104d7dbd00e3ae0a5c00560c00', 99 | zoneHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 100 | salt: '24446860302761739304752683030156737591518664810215442929811933767042559172667', 101 | conduitKey: '0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000', 102 | totalOriginalConsiderationItems: '3', 103 | }, 104 | signature: 105 | '0x8a73c1158a78eee531d4a8dd4be4b33edbf64a1cfa65020c9108102c17bc9a7c159ddaca7223478b0aaf3cfe113c3a3448dcd86996efe72d574b1c02c5ed83cf', 106 | }, 107 | ], 108 | recipient: TEST_RECIPIENT_ADDRESS, 109 | protocolAddress: '0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC', 110 | } 111 | 112 | export function calculateSeaportValue(considerations: ConsiderationItem[], token: string): BigNumber { 113 | return considerations.reduce( 114 | (amt: BigNumber, consideration: ConsiderationItem) => 115 | consideration.token == token ? amt.add(consideration.startAmount) : amt, 116 | BigNumber.from(0) 117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /test/orders/x2y2.ts: -------------------------------------------------------------------------------- 1 | export const x2y2Orders = [ 2 | { 3 | input: 4 | '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000004800000000000000000000000000000000000000000000000000000be0c907f46280000000000000000000000000000000000000000000000000000000063656a8d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a873bdd49f7f9cc0a5458416a12973fab208f8d0000000000000000000000000000000000000000000000000000000000000000f9ad9c0ae9720780f84e926d8626f073e7c2a4ce9f3b77ade5a27a913ef09a425a71932a5cbb411886d82178a3fc0cbf11ba452abcc95a7901a7d4085fb5fc3f000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000047f67d1e1552334d404c80f5cbef820000000000000000000000000d4f16530fbcd336b4f0d4d1717487a65098be7cd0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000063ea6d9e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001c09d2388f6e390220f9d861cf0d793c77ef44bd8d97897b12c36ace9931acd72a5395fc36d3f1c5500e050ed703f7112a01e99295a093424a1a2fa66930e24a995000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000494654067e10000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000057f1887a8bf19b14fc0df6fd9b2acc9af147ea850e2d28628b4a177f893b3c31e4f64a3fefc6cfa562ec3c71867725934686fb26000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000494654067e10000fe98b8bf04d1bc66dd380d36c901fa2c5b2ec3097b59729fcdb29dee694c6fbe000000000000000000000000f849de01b080adc3a814fabe1e2087475cf2e35400000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388000000000000000000000000d823c605807cc5e6bd6fc0d7e4eea50d3e2d66cd', 5 | order_id: 9002704, 6 | token_id: '6412166724678289871743717335944131474762824270526419296462192909998329625382', 7 | price: '330000000000000000', 8 | }, 9 | { 10 | input: 11 | '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000004a00000000000000000000000000000000000000000000000000000dda10ff3d56f000000000000000000000000000000000000000000000000000000006374395b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a873bdd49f7f9cc0a5458416a12973fab208f8d00000000000000000000000000000000000000000000000000000000000000008c5185cd2f0ed88cd5962e6ede9a81b8fcc26ccba22dde10d784b08f5fcf43cf257349b38fc2af2d30aae4f2daa5ad177e0578ec7581fa41bb8c23a34f644d76000000000000000000000000000000000000000000000000000000000000001b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000006eea9a96b66513fd634c399565f53fc90000000000000000000000008a3acc2d82c9a19efdbddb803add7bf7713c45450000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000063fb0888000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001c0fc596083a0132d07cfd02dce0f55ce677b91cc59ebf6d57927593f02b18017192e9379735ef777f756112207f5d6edbba91efb5cdf062c7488a29d961f518418000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000001df9dc8e4ad8000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000093317e87a3a47821803caadc54ae418af80603da000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001df9dc8e4ad80009c48d3d0911702d217e91c00db0e8b5ef4dd6db0fa6d66837460bfb6a230cc71000000000000000000000000024ac22acdb367a3ae52a3d94ac6649fdc1f077900000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388000000000000000000000000d823c605807cc5e6bd6fc0d7e4eea50d3e2d66cd', 12 | order_id: 10637561, 13 | token_id: '0', 14 | price: '135000000000000000', 15 | }, 16 | ] 17 | -------------------------------------------------------------------------------- /test/utils/addresses.ts: -------------------------------------------------------------------------------- 1 | import { UNIVERSAL_ROUTER_ADDRESS, PERMIT2_ADDRESS as MAINNET_PERMIT2_ADDRESS } from '../../src/utils/constants' 2 | 3 | export const MAINNET_ROUTER_ADDRESS = UNIVERSAL_ROUTER_ADDRESS(1) 4 | export const FORGE_ROUTER_ADDRESS = '0xe808c1cfeebb6cb36b537b82fa7c9eef31415a05' 5 | export const FORGE_PERMIT2_ADDRESS = '0x4a873bdd49f7f9cc0a5458416a12973fab208f8d' 6 | 7 | export const FORGE_SENDER_ADDRESS = '0xcf03dd0a894ef79cb5b601a43c4b25e3ae4c67ed' 8 | 9 | export const TEST_RECIPIENT_ADDRESS = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 10 | export const TEST_FEE_RECIPIENT_ADDRESS = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 11 | 12 | export const PERMIT2_ADDRESS = 13 | process.env.USE_MAINNET_DEPLOYMENT === 'true' ? MAINNET_PERMIT2_ADDRESS : FORGE_PERMIT2_ADDRESS 14 | // Universal Router address in tests 15 | export const ROUTER_ADDRESS = 16 | process.env.USE_MAINNET_DEPLOYMENT === 'true' ? MAINNET_ROUTER_ADDRESS : FORGE_ROUTER_ADDRESS 17 | -------------------------------------------------------------------------------- /test/utils/hexToDecimalString.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from 'ethers' 2 | 3 | export function hexToDecimalString(hex: BigNumberish) { 4 | return BigNumber.from(hex).toString() 5 | } 6 | -------------------------------------------------------------------------------- /test/utils/permit2.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { BigNumber, utils, Wallet } from 'ethers' 3 | import { PermitSingle } from '@uniswap/permit2-sdk' 4 | import { defaultAbiCoder } from 'ethers/lib/utils' 5 | import { encodePermit } from '../../src/utils/inputTokens' 6 | import { RoutePlanner } from '../../src/utils/routerCommands' 7 | import { USDC } from './uniswapData' 8 | import { generatePermitSignature, makePermit } from './permit2' 9 | 10 | const PERMIT_STRUCT = 11 | '((address token,uint160 amount,uint48 expiration,uint48 nonce) details, address spender, uint256 sigDeadline)' 12 | 13 | // note: these tests aren't testing much but registering calldata to interop file 14 | // for use in forge fork tests 15 | describe('Permit2', () => { 16 | const wallet = new Wallet(utils.zeroPad('0x1234', 32)) 17 | 18 | describe('v2', () => { 19 | it('does not sanitize a normal permit', async () => { 20 | const inputUSDC = utils.parseUnits('1000', 6).toString() 21 | const permit = makePermit(USDC.address, inputUSDC) 22 | const signature = await generatePermitSignature(permit, wallet, 1) 23 | const sanitized = getSanitizedSignature(permit, signature) 24 | expect(sanitized).to.equal(signature) 25 | }) 26 | 27 | it('does not sanitize a triple length permit', async () => { 28 | const inputUSDC = utils.parseUnits('1000', 6).toString() 29 | const permit = makePermit(USDC.address, inputUSDC) 30 | const signature = await generatePermitSignature(permit, wallet, 1) 31 | const multisigSignature = signature + signature.slice(2) + signature.slice(2) 32 | const sanitized = getSanitizedSignature(permit, multisigSignature) 33 | expect(sanitized).to.equal(multisigSignature) 34 | }) 35 | 36 | it('does not sanitize a short permit', async () => { 37 | const inputUSDC = utils.parseUnits('1000', 6).toString() 38 | const permit = makePermit(USDC.address, inputUSDC) 39 | const tinySignature = '0x12341234132412341344' 40 | const sanitized = getSanitizedSignature(permit, tinySignature) 41 | expect(sanitized).to.equal(tinySignature) 42 | }) 43 | 44 | it('sanitizes a malformed permit', async () => { 45 | const inputUSDC = utils.parseUnits('1000', 6).toString() 46 | const permit = makePermit(USDC.address, inputUSDC) 47 | const originalSignature = await generatePermitSignature(permit, wallet, 1) 48 | 49 | const { recoveryParam } = utils.splitSignature(originalSignature) 50 | // slice off current v 51 | let signature = originalSignature.substring(0, originalSignature.length - 2) 52 | // append recoveryParam as v 53 | signature += BigNumber.from(recoveryParam).toHexString().slice(2) 54 | const sanitized = getSanitizedSignature(permit, signature) 55 | expect(sanitized).to.equal(originalSignature) 56 | }) 57 | }) 58 | 59 | function getSanitizedSignature(permit: PermitSingle, signature: string): string { 60 | const planner = new RoutePlanner() 61 | encodePermit(planner, Object.assign({}, permit, { signature: signature })) 62 | const decoded = defaultAbiCoder.decode([PERMIT_STRUCT, 'bytes'], planner.inputs[0]) 63 | return decoded[1] 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /test/utils/permit2.ts: -------------------------------------------------------------------------------- 1 | import { ethers, Wallet } from 'ethers' 2 | import { AllowanceTransfer, PermitSingle } from '@uniswap/permit2-sdk' 3 | import { Permit2Permit } from '../../src/utils/inputTokens' 4 | import { PERMIT2_ADDRESS, ROUTER_ADDRESS } from './addresses' 5 | import { MAX_UINT160 } from '../../src/utils/constants' 6 | 7 | const TEST_DEADLINE = '3000000000000' 8 | 9 | /// returns signature bytes 10 | export async function generatePermitSignature( 11 | permit: PermitSingle, 12 | signer: Wallet, 13 | chainId: number, 14 | permitAddress: string = PERMIT2_ADDRESS 15 | ): Promise { 16 | const { domain, types, values } = AllowanceTransfer.getPermitData(permit, permitAddress, chainId) 17 | return await signer._signTypedData(domain, types, values) 18 | } 19 | 20 | export async function generateEip2098PermitSignature( 21 | permit: PermitSingle, 22 | signer: Wallet, 23 | chainId: number, 24 | permitAddress: string = PERMIT2_ADDRESS 25 | ): Promise { 26 | const sig = await generatePermitSignature(permit, signer, chainId, permitAddress) 27 | const split = ethers.utils.splitSignature(sig) 28 | return split.compact 29 | } 30 | 31 | export function toInputPermit(signature: string, permit: PermitSingle): Permit2Permit { 32 | return { 33 | ...permit, 34 | signature, 35 | } 36 | } 37 | 38 | export function makePermit( 39 | token: string, 40 | amount: string = MAX_UINT160.toString(), 41 | nonce: string = '0', 42 | routerAddress: string = ROUTER_ADDRESS 43 | ): PermitSingle { 44 | return { 45 | details: { 46 | token, 47 | amount, 48 | expiration: TEST_DEADLINE, 49 | nonce, 50 | }, 51 | spender: routerAddress, 52 | sigDeadline: TEST_DEADLINE, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/utils/uniswapData.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi' 2 | import { ethers } from 'ethers' 3 | import { MixedRouteTrade, MixedRouteSDK, Trade as RouterTrade } from '@uniswap/router-sdk' 4 | import { Trade as V2Trade, Pair, Route as RouteV2, computePairAddress } from '@uniswap/v2-sdk' 5 | import { 6 | Trade as V3Trade, 7 | Pool, 8 | Route as RouteV3, 9 | nearestUsableTick, 10 | TickMath, 11 | TICK_SPACINGS, 12 | FeeAmount, 13 | } from '@uniswap/v3-sdk' 14 | import { SwapOptions } from '../../src' 15 | import { CurrencyAmount, TradeType, Ether, Token, Percent, Currency } from '@uniswap/sdk-core' 16 | import IUniswapV3Pool from '@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json' 17 | import { TEST_RECIPIENT_ADDRESS } from './addresses' 18 | 19 | const V2_FACTORY = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' 20 | const V2_ABI = [ 21 | { 22 | constant: true, 23 | inputs: [], 24 | name: 'getReserves', 25 | outputs: [ 26 | { 27 | internalType: 'uint112', 28 | name: 'reserve0', 29 | type: 'uint112', 30 | }, 31 | { 32 | internalType: 'uint112', 33 | name: 'reserve1', 34 | type: 'uint112', 35 | }, 36 | { 37 | internalType: 'uint32', 38 | name: 'blockTimestampLast', 39 | type: 'uint32', 40 | }, 41 | ], 42 | payable: false, 43 | stateMutability: 'view', 44 | type: 'function', 45 | }, 46 | ] 47 | 48 | const FORK_BLOCK = 16075500 49 | 50 | export const ETHER = Ether.onChain(1) 51 | export const WETH = new Token(1, '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 18, 'WETH', 'Wrapped Ether') 52 | export const DAI = new Token(1, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'dai') 53 | export const USDC = new Token(1, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 6, 'USDC', 'USD Coin') 54 | export const FEE_AMOUNT = FeeAmount.MEDIUM 55 | 56 | type UniswapPools = { 57 | WETH_USDC_V2: Pair 58 | USDC_DAI_V2: Pair 59 | WETH_USDC_V3: Pool 60 | WETH_USDC_V3_LOW_FEE: Pool 61 | USDC_DAI_V3: Pool 62 | } 63 | 64 | export async function getUniswapPools(forkBlock?: number): Promise { 65 | const fork = forkBlock ?? FORK_BLOCK 66 | const WETH_USDC_V2 = await getPair(WETH, USDC, fork) 67 | const USDC_DAI_V2 = await getPair(USDC, DAI, fork) 68 | 69 | const WETH_USDC_V3 = await getPool(WETH, USDC, FEE_AMOUNT, fork) 70 | const WETH_USDC_V3_LOW_FEE = await getPool(WETH, USDC, FeeAmount.LOW, fork) 71 | const USDC_DAI_V3 = await getPool(USDC, DAI, FeeAmount.LOW, fork) 72 | 73 | return { 74 | WETH_USDC_V2, 75 | USDC_DAI_V2, 76 | WETH_USDC_V3, 77 | WETH_USDC_V3_LOW_FEE, 78 | USDC_DAI_V3, 79 | } 80 | } 81 | 82 | function getProvider(): ethers.providers.BaseProvider { 83 | return new ethers.providers.JsonRpcProvider(process.env['FORK_URL']) 84 | } 85 | 86 | export async function getPair(tokenA: Token, tokenB: Token, blockNumber: number): Promise { 87 | const pairAddress = computePairAddress({ factoryAddress: V2_FACTORY, tokenA, tokenB }) 88 | const contract = new ethers.Contract(pairAddress, V2_ABI, getProvider()) 89 | const { reserve0, reserve1 } = await contract.getReserves({ blockTag: blockNumber }) 90 | const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] // does safety checks 91 | return new Pair(CurrencyAmount.fromRawAmount(token0, reserve0), CurrencyAmount.fromRawAmount(token1, reserve1)) 92 | } 93 | 94 | export async function getPool(tokenA: Token, tokenB: Token, feeAmount: FeeAmount, blockNumber: number): Promise { 95 | const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] // does safety checks 96 | const poolAddress = Pool.getAddress(token0, token1, feeAmount) 97 | const contract = new ethers.Contract(poolAddress, IUniswapV3Pool.abi, getProvider()) 98 | let liquidity = await contract.liquidity({ blockTag: blockNumber }) 99 | let { sqrtPriceX96, tick } = await contract.slot0({ blockTag: blockNumber }) 100 | liquidity = JSBI.BigInt(liquidity.toString()) 101 | sqrtPriceX96 = JSBI.BigInt(sqrtPriceX96.toString()) 102 | 103 | return new Pool(token0, token1, feeAmount, sqrtPriceX96, liquidity, tick, [ 104 | { 105 | index: nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[feeAmount]), 106 | liquidityNet: liquidity, 107 | liquidityGross: liquidity, 108 | }, 109 | { 110 | index: nearestUsableTick(TickMath.MAX_TICK, TICK_SPACINGS[feeAmount]), 111 | liquidityNet: JSBI.multiply(liquidity, JSBI.BigInt('-1')), 112 | liquidityGross: liquidity, 113 | }, 114 | ]) 115 | } 116 | 117 | // use some sane defaults 118 | export function swapOptions(options: Partial): SwapOptions { 119 | // If theres a fee this counts as "slippage" for the amount out, so take it into account 120 | let slippageTolerance = new Percent(5, 100) 121 | if (!!options.fee) slippageTolerance = slippageTolerance.add(options.fee.fee) 122 | return Object.assign( 123 | { 124 | slippageTolerance, 125 | recipient: TEST_RECIPIENT_ADDRESS, 126 | }, 127 | options 128 | ) 129 | } 130 | 131 | // alternative constructor to create from protocol-specific sdks 132 | export function buildTrade( 133 | trades: ( 134 | | V2Trade 135 | | V3Trade 136 | | MixedRouteTrade 137 | )[] 138 | ): RouterTrade { 139 | return new RouterTrade({ 140 | v2Routes: trades 141 | .filter((trade) => trade instanceof V2Trade) 142 | .map((trade) => ({ 143 | routev2: trade.route as RouteV2, 144 | inputAmount: trade.inputAmount, 145 | outputAmount: trade.outputAmount, 146 | })), 147 | v3Routes: trades 148 | .filter((trade) => trade instanceof V3Trade) 149 | .map((trade) => ({ 150 | routev3: trade.route as RouteV3, 151 | inputAmount: trade.inputAmount, 152 | outputAmount: trade.outputAmount, 153 | })), 154 | mixedRoutes: trades 155 | .filter((trade) => trade instanceof MixedRouteTrade) 156 | .map((trade) => ({ 157 | mixedRoute: trade.route as MixedRouteSDK, 158 | inputAmount: trade.inputAmount, 159 | outputAmount: trade.outputAmount, 160 | })), 161 | tradeType: trades[0].tradeType, 162 | }) 163 | } 164 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "strictPropertyInitialization": false, 9 | "resolveJsonModule": true, 10 | "outDir": "dist", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "lib": ["esnext.string"], 14 | "typeRoots": ["./typechain", "./node_modules/@types"], 15 | "types": ["@types/mocha", "node"] 16 | }, 17 | "include": ["src", "test"] 18 | } 19 | --------------------------------------------------------------------------------