diff --git a/packages/deploy/integration_test/marketplace/config.ts b/packages/deploy/integration_test/marketplace/config.ts new file mode 100644 index 0000000000..6943fa3318 --- /dev/null +++ b/packages/deploy/integration_test/marketplace/config.ts @@ -0,0 +1,26 @@ +export const GRID_SIZE = 408; + +//ERC20 to exchange for +export const erc20PriceForBundle = 10000000000; + +// Configuration for ERC721 land tokens (array of [x,y] values) +export const erc721 = [ + // Add (x, y) pairs as needed +]; + +// Configuration for quad land (array of [x, y, size] values) +export const quad = [ + // Add (size,x, y) triples as needed +]; + +// Configuration for ERC1155 assets (array of [tokenId, supply] values) +export const erc1155 = [ + // Add [tier, amount, metadataHash] as needed +]; + +// Price distribution setup +export const priceDistribution = { + erc721Prices: [[]], + erc1155Prices: [[]], + quadPrices: [], +}; diff --git a/packages/deploy/integration_test/marketplace/executingBundleMatchOrders.ts b/packages/deploy/integration_test/marketplace/executingBundleMatchOrders.ts new file mode 100644 index 0000000000..fffa06fee8 --- /dev/null +++ b/packages/deploy/integration_test/marketplace/executingBundleMatchOrders.ts @@ -0,0 +1,398 @@ +import 'dotenv/config'; +import {ethers, ZeroAddress} from 'ethers'; +import hre from 'hardhat'; +import {AssetERC20, Asset, AssetBundle} from './utils/assets'; +import {OrderDefault, signOrder, Order} from './utils/order'; +import { + createAssetMintSignature, + generateTokenId, +} from './utils/createAssetMintSignature'; +import { + GRID_SIZE, + erc721, + quad, + erc1155, + erc20PriceForBundle, + priceDistribution, +} from './config'; + +// Sand token are exchanged with a bundle using matchOrder +async function main() { + const networkName = hre.network.name; + if (networkName == 'hardhat') { + throw new TypeError('Invalid network'); + } + const {getChainId} = hre; + const chainId = await getChainId(); + + const {deployments, getNamedAccounts} = hre; + const {read, execute} = deployments; + const {deployer, sandAdmin, assetAdmin} = await getNamedAccounts(); + + const PolygonSand = await deployments.get('PolygonSand'); + const PolygonSandAddress = PolygonSand.address; + + const PolygonLand = await deployments.get('PolygonLand'); + const PolygonLandAddress = PolygonLand.address; + + const Asset = await deployments.get('Asset'); + const AssetAddress = Asset.address; + + const AssetCreate = await deployments.get('AssetCreate'); + const AssetCreateAddress = AssetCreate.address; + + const Exchange = await deployments.get('Exchange'); + const ExchangeAddress = Exchange.address; + const OrderValidator = await deployments.get('OrderValidator'); + + const ERC20_ROLE = await read('OrderValidator', 'ERC20_ROLE'); + const TSB_PRIMARY_MARKET_SELLER_ROLE = await read( + 'Exchange', + 'TSB_PRIMARY_MARKET_SELLER_ROLE' + ); + + const mnemonicAmoyInstance = ethers.Mnemonic.fromPhrase( + process.env.MNEMONIC_AMOY + ); + + const signerDeployer = ethers.HDNodeWallet.fromMnemonic( + mnemonicAmoyInstance, + `m/44'/60'/0'/0/0` + ); + + const signerSandAdmin = ethers.HDNodeWallet.fromMnemonic( + mnemonicAmoyInstance, + `m/44'/60'/0'/0/2` + ); + + const signerAssetCreate = new ethers.Wallet( + process.env.ASSET_V2_CREATE_WALLET_SIGNATURE + ); + + const erc721Id = [], + erc1155Id = [], + erc1155Supply = [], + quadId = [], + quadSize = [], + quadXs = [], + quadYs = []; + + // Sand token is granted ERC20 role + if ( + !(await read('OrderValidator', 'hasRole', ERC20_ROLE, PolygonSandAddress)) + ) { + await execute( + 'OrderValidator', + {from: sandAdmin, log: true}, + 'grantRole', + ERC20_ROLE, + PolygonSandAddress + ); + } + + // initial sandAdmin Sand balance + console.log( + 'sandAdmin initial PolygonSand balance : ', + (await read('PolygonSand', 'balanceOf', sandAdmin)).toString() + ); + + // initial deployer Sand balance + console.log( + 'deployer initial PolygonSand balance : ', + (await read('PolygonSand', 'balanceOf', deployer)).toString() + ); + + // allow sandAdmin to mint land + if (!(await read('PolygonLand', 'isMinter', sandAdmin))) { + await execute( + 'PolygonLand', + {from: deployer, log: true}, + 'setMinter', + sandAdmin, + true + ); + } + + // mint quad of size 1 with (x,y) + for (let i = 0; i < erc721.length; i++) { + const x = erc721[i][0]; + const y = erc721[i][1]; + await execute( + 'PolygonLand', + {from: sandAdmin, log: true}, + 'mintQuad', + sandAdmin, + 1, + x, + y, + '0x' + ); + + const id = idInPath(0, 1, x, y); + console.log( + `land owner of id: ${id} : `, + await read('PolygonLand', 'ownerOf', id) + ); + + erc721Id.push(id); + } + + // mint ERC1155 asset with provided tier and amount + for (let i = 0; i < erc1155.length; i++) { + const tier = erc1155[i][0]; + const amount = erc1155[i][1]; + const metadataHash = erc1155[i][2]; + + let nonce = ( + await read('AssetCreate', 'signatureNonces', assetAdmin) + ).toString(); + + const signature = await createAssetMintSignature( + assetAdmin, + tier, + amount, + nonce, + true, + metadataHash, + AssetCreateAddress, + signerAssetCreate + ); + + await execute( + 'Catalyst', + {from: assetAdmin, log: true}, + 'mint', + assetAdmin, + tier, + amount + ); + + await execute( + 'AssetCreate', + {from: assetAdmin, log: true}, + 'createAsset', + signature, + tier, + amount, + true, + metadataHash, + assetAdmin + ); + + nonce = ( + await read('AssetCreate', 'signatureNonces', assetAdmin) + ).toString(); + + const tokenId = await generateTokenId(assetAdmin, tier, nonce, 1); + + console.log( + `balance of sandAdmin asset with id:${tokenId} : `, + (await read('Asset', 'balanceOf', sandAdmin, tokenId)).toString() + ); + console.log( + `balance of deployer asset with id:${tokenId} : `, + (await read('Asset', 'balanceOf', deployer, tokenId)).toString() + ); + erc1155Id.push(tokenId); + erc1155Supply.push(amount); + } + + // mint quad of given size with (x,y) + for (let i = 0; i < quad.length; i++) { + const size = quad[i][0]; + const x = quad[i][1]; + const y = quad[i][2]; + + await execute( + 'PolygonLand', + {from: sandAdmin, log: true}, + 'mintQuad', + sandAdmin, + size, + x, + y, + '0x' + ); + + for (let j = 0; j < size * size; j++) { + const id = idInPath(j, size, x, y); + console.log( + `land owner of id:${id} : `, + await read('PolygonLand', 'ownerOf', id) + ); + quadId.push(id); + } + + quadSize.push(size); + quadXs.push(x); + quadYs.push(y); + } + + // approve Exchange contract with Sand token by deployer + await execute( + 'PolygonSand', + {from: deployer, log: true}, + 'approve', + ExchangeAddress, + erc20PriceForBundle + ); + + // approve Exchange contract with Land token by sandAdmin + await execute( + 'PolygonLand', + {from: sandAdmin, log: true}, + 'setApprovalForAll', + ExchangeAddress, + true + ); + + // approve Exchange contract with Asset token by sandAdmin + await execute( + 'Asset', + {from: sandAdmin, log: true}, + 'setApprovalForAll', + ExchangeAddress, + true + ); + + const makerAsset: Asset = await AssetERC20( + PolygonSandAddress, + erc20PriceForBundle + ); + + const bundledERC721 = [ + { + erc721Address: PolygonLandAddress, + ids: erc721Id, + }, + ]; + + const bundledERC1155 = [ + { + erc1155Address: AssetAddress, + ids: erc1155Id, + supplies: erc1155Supply, + }, + ]; + + const quads = { + sizes: quadSize, + xs: quadXs, + ys: quadYs, + data: '0x', + }; + + // Create bundle for passing as right order + const bundleData = { + bundledERC721, + bundledERC1155, + quads, + priceDistribution, + }; + + const takerAsset: Asset = await AssetBundle(bundleData, 1); + + const orderLeft: Order = await OrderDefault( + deployer, + makerAsset, // ERC20 + ZeroAddress, + takerAsset, // Bundle + 1, + 0, + 0 + ); + const orderRight: Order = await OrderDefault( + sandAdmin, + takerAsset, // Bundle + ZeroAddress, + makerAsset, // ERC20 + 1, + 0, + 0 + ); + + const makerSig: string = await signOrder( + orderLeft, + signerDeployer, + chainId, + OrderValidator + ); + const takerSig: string = await signOrder( + orderRight, + signerSandAdmin, + chainId, + OrderValidator + ); + + // bundle seller (sandAdmin) is provided with + if ( + !(await read( + 'Exchange', + 'hasRole', + TSB_PRIMARY_MARKET_SELLER_ROLE, + sandAdmin + )) + ) { + await execute( + 'Exchange', + {from: sandAdmin, log: true}, + 'grantRole', + TSB_PRIMARY_MARKET_SELLER_ROLE, + sandAdmin + ); + } + + const tx = await execute( + 'Exchange', + {from: deployer, log: true, gasLimit: 1000000}, + 'matchOrders', + [ + { + orderLeft, // passing ERC20 as left order + signatureLeft: makerSig, + orderRight, // passing Bundle as right order + signatureRight: takerSig, + }, + ] + ); + console.log('transaction hash for bundle exchange : ', tx.transactionHash); + + console.log('After executing match Orders'); + console.log( + 'sandAdmin PolygonSand balance after exchange : ', + (await read('PolygonSand', 'balanceOf', sandAdmin)).toString() + ); + console.log( + 'deployer PolygonSand balance after exchange : ', + (await read('PolygonSand', 'balanceOf', deployer)).toString() + ); + for (let i = 0; i < erc721Id.length; i++) { + console.log( + `land owner of id:${erc721Id[i]} after exchange : `, + await read('PolygonLand', 'ownerOf', erc721Id[i]) + ); + } + + for (let i = 0; i < erc1155Id.length; i++) { + console.log( + `balance of sandAdmin asset with id:${erc1155Id[i]} after exchange : `, + (await read('Asset', 'balanceOf', sandAdmin, erc1155Id[i])).toString() + ); + console.log( + `balance of deployer asset with id:${erc1155Id[i]} after exchange : `, + (await read('Asset', 'balanceOf', deployer, erc1155Id[i])).toString() + ); + } + + for (let i = 0; i < quadId.length; i++) { + console.log( + `land owner of id:${quadId[i]} after excahnge : `, + await read('PolygonLand', 'ownerOf', quadId[i]) + ); + } +} + +function idInPath(i: number, size: number, x: number, y: number): number { + return x + (i % size) + (y + Math.floor(i / size)) * GRID_SIZE; +} +void main(); diff --git a/packages/deploy/integration_test/marketplace/utils/assets.ts b/packages/deploy/integration_test/marketplace/utils/assets.ts new file mode 100644 index 0000000000..834cd67ec1 --- /dev/null +++ b/packages/deploy/integration_test/marketplace/utils/assets.ts @@ -0,0 +1,126 @@ +// AssetXXX represents something you want to trade, for example: +// "20 eth" == AssetETH(20), "11 Sand" == AssetERC20(SandTokenAddress, 11) +// some NFT" == AssetERC721(nftContractAddress, tokenId), etc +// SEE: LibAsset.sol +import {AbiCoder, AddressLike, BytesLike, keccak256, Numeric} from 'ethers'; + +export enum AssetClassType { + INVALID_ASSET_CLASS = '0x0', + ERC20_ASSET_CLASS = '0x1', + ERC721_ASSET_CLASS = '0x2', + ERC1155_ASSET_CLASS = '0x3', + BUNDLE_ASSET_CLASS = '0x4', +} + +export const ASSET_TYPE_TYPEHASH = keccak256( + Buffer.from('AssetType(uint256 assetClass,bytes data)') +); +export const ASSET_TYPEHASH = keccak256( + Buffer.from( + 'Asset(AssetType assetType,uint256 value)AssetType(uint256 assetClass,bytes data)' + ) +); + +export type LibPart = { + account: AddressLike; + value: Numeric; +}; + +export type FeeRecipients = { + recipient: AddressLike; + bps: Numeric; +}; + +export type AssetType = { + assetClass: AssetClassType; + data: BytesLike; +}; + +export type PriceDistribution = { + erc721Prices: Numeric[][]; + erc1155Prices: Numeric[][]; + quadPrices: Numeric[]; +}; + +export type Asset = { + assetType: AssetType; + value: Numeric; +}; + +export const FeeRecipientsData = async ( + recipient: AddressLike, + bps: Numeric +): Promise => ({ + recipient, + bps, +}); + +export const LibPartData = async ( + account: AddressLike, + basisPoints: Numeric +): Promise => ({ + account, + basisPoints, +}); + +export const AssetERC20 = async ( + tokenContractAddress: string, + value: Numeric, + recipient?: string +): Promise => { + const baseParams: string[] = ['address']; + const baseValues: (string | number)[] = [tokenContractAddress]; + + if (recipient) { + baseParams.push('uint256'); + baseValues.push(0); + baseParams.push('address'); + baseValues.push(recipient); + } + + return { + assetType: { + assetClass: AssetClassType.ERC20_ASSET_CLASS, + data: AbiCoder.defaultAbiCoder().encode(baseParams, baseValues), + }, + value, + }; +}; + +export function hashAssetType(a: AssetType) { + if (a.assetClass === AssetClassType.INVALID_ASSET_CLASS) { + throw new Error('Invalid assetClass' + a.assetClass); + } + return keccak256( + AbiCoder.defaultAbiCoder().encode( + ['bytes32', 'uint256', 'bytes32'], + [ASSET_TYPE_TYPEHASH, a.assetClass, keccak256(a.data)] + ) + ); +} + +export function hashAsset(a: Asset) { + return keccak256( + AbiCoder.defaultAbiCoder().encode( + ['bytes32', 'bytes32', 'uint256'], + [ASSET_TYPEHASH, hashAssetType(a.assetType), a.value] + ) + ); +} + +export const AssetBundle = async ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bundleInformation: any, // TODO: type, + value: number +): Promise => ({ + assetType: { + assetClass: AssetClassType.BUNDLE_ASSET_CLASS, + data: AbiCoder.defaultAbiCoder().encode( + [ + 'tuple(tuple(address erc721Address, uint256[] ids)[] bundledERC721, tuple(address erc1155Address, uint256[] ids, uint256[] supplies)[] bundledERC1155, tuple(uint256[] sizes, uint256[] xs, uint256[] ys, bytes data) quads, tuple(uint256[][] erc721Prices, uint256[][] erc1155Prices, uint256[] quadPrices) priceDistribution)', + ], + [bundleInformation] + ), + }, + value, +}); diff --git a/packages/deploy/integration_test/marketplace/utils/createAssetMintSignature.ts b/packages/deploy/integration_test/marketplace/utils/createAssetMintSignature.ts new file mode 100644 index 0000000000..197565f48a --- /dev/null +++ b/packages/deploy/integration_test/marketplace/utils/createAssetMintSignature.ts @@ -0,0 +1,65 @@ +import {Signer} from 'ethers'; + +export async function createAssetMintSignature( + creator: string, + tier: number, + amount: number, + nonce: number, + revealed: boolean, + metadataHash: string, + AssetCreate: string, + signer: Signer +) { + const data = { + types: { + Mint: [ + {name: 'creator', type: 'address'}, + {name: 'nonce', type: 'uint16'}, + {name: 'tier', type: 'uint8'}, + {name: 'amount', type: 'uint256'}, + {name: 'revealed', type: 'bool'}, + {name: 'metadataHash', type: 'string'}, + ], + }, + domain: { + name: 'Sandbox Asset Create', + version: '1.0', + chainId: 80002, + verifyingContract: AssetCreate, + }, + message: { + creator, + nonce, + tier, + amount, + revealed, + metadataHash, + }, + }; + + const signature = await signer.signTypedData( + data.domain, + data.types, + data.message + ); + return signature; +} + +export async function generateTokenId( + creator: string, + tier: number, + creatorNonce: number, + revealNonce: number +): Promise { + // Convert the hexadecimal Ethereum address string to BigInt + const creatorAddress = BigInt(creator); + + const tokenId = + creatorAddress | + (BigInt(tier) << BigInt(160)) | + (BigInt(creatorNonce) << BigInt(168)) | + (BigInt(revealNonce) << BigInt(184)) | + (BigInt(0) << BigInt(200)); // bridged is always 0 + + return tokenId; +} diff --git a/packages/deploy/integration_test/marketplace/utils/order.ts b/packages/deploy/integration_test/marketplace/utils/order.ts new file mode 100644 index 0000000000..9e8dbc38d7 --- /dev/null +++ b/packages/deploy/integration_test/marketplace/utils/order.ts @@ -0,0 +1,165 @@ +// An order represents something offered (asset + who offers) plus what we want in exchange (asset + optionally for whom or everybody) +// SEE: LibOrder.sol +import {Asset, AssetType, hashAsset, hashAssetType} from './assets'; +import { + AbiCoder, + Contract, + keccak256, + Numeric, + Signer, + ZeroAddress, +} from 'ethers'; + +export const ORDER_TYPEHASH = keccak256( + Buffer.from( + 'Order(address maker,Asset makeAsset,address taker,Asset takeAsset,uint256 salt,uint256 start,uint256 end)Asset(AssetType assetType,uint256 value)AssetType(uint256 assetClass,bytes data)' + ) +); + +export const UINT256_MAX_VALUE = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + +export type Order = { + maker: string; + makeAsset: Asset; + taker: string; + takeAsset: Asset; + salt: Numeric; + start: Numeric; + end: Numeric; +}; + +export const OrderDefault = async ( + maker: string, + makeAsset: Asset, + taker: Signer | ZeroAddress, + takeAsset: Asset, + salt: Numeric, + start: Numeric, + end: Numeric +): Promise => ({ + maker, + makeAsset, + taker: + taker === ZeroAddress ? ZeroAddress : await (taker as Signer).getAddress(), + takeAsset, + salt, + start, + end, +}); + +export function hashKey(order: Order): string { + const encoded = AbiCoder.defaultAbiCoder().encode( + ['address', 'bytes32', 'bytes32', 'uint256'], + [ + order.maker, + hashAssetType(order.makeAsset.assetType), + hashAssetType(order.takeAsset.assetType), + order.salt, + ] + ); + return keccak256(encoded); +} + +export const getSymmetricOrder = async ( + o: Order, + taker?: Signer +): Promise => { + const ret = { + ...o, + makeAsset: o.takeAsset, + taker: o.maker, + takeAsset: o.makeAsset, + }; + if (taker) { + return {...ret, maker: await taker.getAddress()}; + } + if (o.taker === ZeroAddress) { + throw new Error( + 'Original order was for anybody, the taker is needed to create the order' + ); + } + return {...ret, maker: o.taker}; +}; + +export function hashOrder(order: Order): string { + const encoded = AbiCoder.defaultAbiCoder().encode( + [ + 'bytes32', + 'address', + 'bytes32', + 'address', + 'bytes32', + 'uint256', + 'uint256', + 'uint256', + ], + [ + ORDER_TYPEHASH, + order.maker, + hashAsset(order.makeAsset), + order.taker, + hashAsset(order.takeAsset), + order.salt, + order.start, + order.end, + ] + ); + return keccak256(encoded); +} + +export async function signOrder( + order: Order, + account: Signer, + chainId: Numeric, + verifyingContract: Contract +) { + return account.signTypedData( + { + name: 'The Sandbox Marketplace', + version: '1.0.0', + chainId: chainId, + verifyingContract: verifyingContract.address, + }, + { + AssetType: [ + {name: 'assetClass', type: 'uint256'}, + {name: 'data', type: 'bytes'}, + ], + Asset: [ + {name: 'assetType', type: 'AssetType'}, + {name: 'value', type: 'uint256'}, + ], + Order: [ + {name: 'maker', type: 'address'}, + {name: 'makeAsset', type: 'Asset'}, + {name: 'taker', type: 'address'}, + {name: 'takeAsset', type: 'Asset'}, + {name: 'salt', type: 'uint256'}, + {name: 'start', type: 'uint256'}, + {name: 'end', type: 'uint256'}, + ], + }, + order + ); +} + +export function isAssetTypeEqual(x: AssetType, y: AssetType): boolean { + return x.assetClass == y.assetClass && x.data == y.data; +} + +export function isAssetEqual(x: Asset, y: Asset): boolean { + return isAssetTypeEqual(x.assetType, y.assetType) && x.value == y.value; +} + +export function isOrderEqual(x: Order, order: Order): boolean { + return ( + x.maker === order.maker && + isAssetEqual(x.makeAsset, order.makeAsset) && + x.taker === order.taker && + isAssetEqual(x.takeAsset, order.takeAsset) && + x.salt == order.salt && + x.start == order.start && + x.end == order.end + ); +} diff --git a/packages/deploy/integration_test/README.md b/packages/deploy/integration_test/oft-sand/README.md similarity index 100% rename from packages/deploy/integration_test/README.md rename to packages/deploy/integration_test/oft-sand/README.md diff --git a/packages/deploy/integration_test/executeSendOnOFTAdapterSepolia.ts b/packages/deploy/integration_test/oft-sand/executeSendOnOFTAdapterSepolia.ts similarity index 100% rename from packages/deploy/integration_test/executeSendOnOFTAdapterSepolia.ts rename to packages/deploy/integration_test/oft-sand/executeSendOnOFTAdapterSepolia.ts diff --git a/packages/deploy/integration_test/executeSendOnOFTSandBase.ts b/packages/deploy/integration_test/oft-sand/executeSendOnOFTSandBase.ts similarity index 100% rename from packages/deploy/integration_test/executeSendOnOFTSandBase.ts rename to packages/deploy/integration_test/oft-sand/executeSendOnOFTSandBase.ts diff --git a/packages/deploy/integration_test/executeSendOnOFTSandBsc.ts b/packages/deploy/integration_test/oft-sand/executeSendOnOFTSandBsc.ts similarity index 100% rename from packages/deploy/integration_test/executeSendOnOFTSandBsc.ts rename to packages/deploy/integration_test/oft-sand/executeSendOnOFTSandBsc.ts diff --git a/packages/deploy/integration_test/executeTransferOnLZAndPolygonPortal.ts b/packages/deploy/integration_test/oft-sand/executeTransferOnLZAndPolygonPortal.ts similarity index 100% rename from packages/deploy/integration_test/executeTransferOnLZAndPolygonPortal.ts rename to packages/deploy/integration_test/oft-sand/executeTransferOnLZAndPolygonPortal.ts