Skip to content

Commit

Permalink
feat: third party ownership validation (#1796)
Browse files Browse the repository at this point in the history
  • Loading branch information
marianogoldman authored Aug 13, 2024
1 parent 9368aab commit f1efb8f
Show file tree
Hide file tree
Showing 11 changed files with 522 additions and 72 deletions.
2 changes: 1 addition & 1 deletion content/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@dcl/catalyst-api-specs": "^3.2.6",
"@dcl/catalyst-contracts": "^4.4.1",
"@dcl/catalyst-storage": "^4.2.0",
"@dcl/content-validator": "^5.5.0",
"@dcl/content-validator": "^5.6.1",
"@dcl/crypto": "^3.4.5",
"@dcl/hashing": "^3.0.4",
"@dcl/schemas": "^13.2.1",
Expand Down
129 changes: 62 additions & 67 deletions content/src/logic/checker.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ItemChecker, L1Checker, L2Checker } from '@dcl/content-validator'
import { inputBlockNumberFormatter, inputCallFormatter } from './formatters'
import { ContractFactory, HTTPProvider, RequestManager, RPCSendableMessage, toBatchPayload, toData } from 'eth-connect'
import { ContractFactory, HTTPProvider, RequestManager, RPCSendableMessage, toData } from 'eth-connect'
import { checkerAbi, l1Contracts, l2Contracts } from '@dcl/catalyst-contracts'
import { code } from '@dcl/catalyst-contracts/dist/checkerByteCode'
import { parseUrn } from '@dcl/urn-resolver'
import { EthAddress } from '@dcl/schemas'
import { ILoggerComponent } from '@well-known-components/interfaces'
import { sendBatch } from './contract-helpers'

type CollectionItem = {
contract: string
Expand Down Expand Up @@ -36,27 +37,31 @@ export async function createL1Checker(provider: HTTPProvider, network: 'mainnet'
return checker.multicall.unpackOutput(data)
}

return {
async checkLAND(ethAddress: string, parcels: [number, number][], block: number): Promise<boolean[]> {
const multicallPayload = await Promise.all(
parcels.map(async ([x, y]) => {
const payload = checker.checkLAND.toPayload(ethAddress, contracts.land, contracts.state, x, y)
return payload.data
})
)
async function checkLAND(ethAddress: string, parcels: [number, number][], block: number): Promise<boolean[]> {
const multicallPayload = await Promise.all(
parcels.map(async ([x, y]) => {
const payload = checker.checkLAND.toPayload(ethAddress, contracts.land, contracts.state, x, y)
return payload.data
})
)

return callMulticallCheckerMethod(multicallPayload, block)
},
async checkNames(ethAddress: string, names: string[], block: number): Promise<boolean[]> {
const multicallPayload = await Promise.all(
names.map(async (name) => {
const payload = await checker.checkName.toPayload(ethAddress, contracts.registrar, name)
return payload.data
})
)
return callMulticallCheckerMethod(multicallPayload, block)
}

return callMulticallCheckerMethod(multicallPayload, block)
}
async function checkNames(ethAddress: string, names: string[], block: number): Promise<boolean[]> {
const multicallPayload = await Promise.all(
names.map(async (name) => {
const payload = await checker.checkName.toPayload(ethAddress, contracts.registrar, name)
return payload.data
})
)

return callMulticallCheckerMethod(multicallPayload, block)
}

return {
checkLAND,
checkNames
}
}

Expand Down Expand Up @@ -97,40 +102,40 @@ export async function createL2Checker(provider: HTTPProvider, network: 'polygon'
return method.unpackOutput(data)
}

return {
async validateWearables(
ethAddress: string,
contractAddress: string,
assetId: string,
hashes: string[],
block: number
): Promise<boolean> {
const factories = contracts.factories
.filter(({ sinceBlock }) => block >= sinceBlock)
.map(({ address }) => address)
const commitees = contracts.commitees
.filter(({ sinceBlock }) => block >= sinceBlock)
.map(({ address }) => address)
const multicallPayload = await Promise.all(
hashes.map(async (hash) => {
const payload = checker.validateWearables.toPayload(
ethAddress,
factories,
contractAddress,
assetId,
hash,
commitees
)
return payload.data
})
)
async function validateWearables(
ethAddress: string,
contractAddress: string,
assetId: string,
hashes: string[],
block: number
): Promise<boolean> {
const factories = contracts.factories.filter(({ sinceBlock }) => block >= sinceBlock).map(({ address }) => address)
const commitees = contracts.commitees.filter(({ sinceBlock }) => block >= sinceBlock).map(({ address }) => address)
const multicallPayload = await Promise.all(
hashes.map(async (hash) => {
const payload = checker.validateWearables.toPayload(
ethAddress,
factories,
contractAddress,
assetId,
hash,
commitees
)
return payload.data
})
)

const result = (await callMulticallCheckerMethod(multicallPayload, block)) as boolean[]
return result.some((r) => r)
},
async validateThirdParty(tpId: string, root: Buffer, block: number): Promise<boolean> {
return callCheckerMethod(checker.validateThirdParty, [contracts.thirdParty, tpId, root], block)
}
const result = (await callMulticallCheckerMethod(multicallPayload, block)) as boolean[]
return result.some((r) => r)
}

async function validateThirdParty(tpId: string, root: Buffer, block: number): Promise<boolean> {
return callCheckerMethod(checker.validateThirdParty, [contracts.thirdParty, tpId, root], block)
}

return {
validateWearables,
validateThirdParty
}
}

Expand All @@ -144,20 +149,6 @@ const itemCheckerAbi = [
}
]

function sendBatch(provider: HTTPProvider, batch: RPCSendableMessage[]) {
const payload = toBatchPayload(batch)
return new Promise<any>((resolve, reject) => {
provider.sendAsync(payload as any, (err: any, result: any) => {
if (err) {
reject(err)
return
}

resolve(result)
})
})
}

export async function createItemChecker(logs: ILoggerComponent, provider: HTTPProvider): Promise<ItemChecker> {
const logger = logs.getLogger('item-checker')
const requestManager = new RequestManager(provider)
Expand All @@ -178,6 +169,10 @@ export async function createItemChecker(logs: ILoggerComponent, provider: HTTPPr
}

async function checkItems(ethAddress: string, items: string[], block: number): Promise<boolean[]> {
if (items.length === 0) {
return []
}

const uniqueItems = Array.from(new Set(items))
const urns = await Promise.all(uniqueItems.map((item) => parseUrn(item)))

Expand Down
43 changes: 43 additions & 0 deletions content/src/logic/contract-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { HTTPProvider, RPCSendableMessage, toBatchPayload } from 'eth-connect'

export const erc721Abi = [
{
inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }],
name: 'ownerOf',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function'
}
]

export const erc1155Abi = [
{
inputs: [
{ internalType: 'address', name: 'account', type: 'address' },
{ internalType: 'uint256', name: 'id', type: 'uint256' }
],
name: 'balanceOf',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
}
]

export function sendBatch(provider: HTTPProvider, batch: RPCSendableMessage[]) {
const payload = toBatchPayload(batch)
return new Promise<any>((resolve, reject) => {
provider.sendAsync(payload as any, (err: any, result: any) => {
if (err) {
reject(err)
return
}

resolve(result)
})
})
}

export async function sendSingle(provider: HTTPProvider, message: RPCSendableMessage) {
const res = await sendBatch(provider, [message])
return res[0]
}
128 changes: 128 additions & 0 deletions content/src/logic/third-party-contract-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import fs from 'fs'
import path from 'path'

import RequestManager, { ContractFactory, HTTPProvider, toData } from 'eth-connect'
import { ILoggerComponent } from '@well-known-components/interfaces'
import { ContractAddress } from '@dcl/schemas'
import { erc1155Abi, erc721Abi, sendSingle } from './contract-helpers'

export enum ContractType {
ERC721 = 'erc721',
ERC1155 = 'erc1155',
UNKNOWN = 'unknown'
}

export function loadCacheFile(file: string): Record<string, ContractType> {
try {
if (!fs.existsSync(file)) {
saveCacheFile(file, {})
}
const fileContent = fs.readFileSync(file, 'utf-8')
return JSON.parse(fileContent)
} catch (_) {
return {}
}
}

export function saveCacheFile(file: string, data: any): void {
const jsonData = JSON.stringify(data, null, 2)
fs.writeFileSync(file, jsonData, 'utf-8')
}

export type ThirdPartyContractRegistry = {
isErc721(contractAddress: ContractAddress): boolean
isErc1155(contractAddress: ContractAddress): boolean
isUnknown(contractAddress: ContractAddress): boolean
ensureContractsKnown(contractAddresses: ContractAddress[]): Promise<void>
}

export async function createThirdPartyContractRegistry(
logs: ILoggerComponent,
provider: HTTPProvider,
network: 'mainnet' | 'sepolia' | 'polygon' | 'amoy',
storageRoot: string
): Promise<ThirdPartyContractRegistry> {
const logger = logs.getLogger('contract-registry')

const requestManager = new RequestManager(provider)
const erc721ContractFactory = new ContractFactory(requestManager, erc721Abi)
const erc1155ContractFactory = new ContractFactory(requestManager, erc1155Abi)

const file = path.join(storageRoot, `third-party-contracts-${network}.json`)
const data: Record<ContractAddress, ContractType> = loadCacheFile(file)

function isErc721(contractAddress: ContractAddress): boolean {
return data[contractAddress.toLowerCase()] === ContractType.ERC721
}

function isErc1155(contractAddress: ContractAddress): boolean {
return data[contractAddress.toLowerCase()] === ContractType.ERC1155
}

function isUnknown(contractAddress: ContractAddress): boolean {
return data[contractAddress.toLowerCase()] === ContractType.UNKNOWN
}

async function checkIfErc721(contractAddress: ContractAddress): Promise<boolean> {
// ERC-721 checks
const contract: any = await erc721ContractFactory.at(contractAddress)
try {
const r = await sendSingle(provider, await contract.ownerOf.toRPCMessage(0))

if (r.error?.code === 3) {
// NFT id doesn't exist, but it is an ERC-721
return true
}
if (!r.result) {
return false
}
return !!contract.ownerOf.unpackOutput(toData(r.result))
} catch (_) {
return false
}
}

async function checkIfErc1155(contractAddress: ContractAddress): Promise<boolean> {
// ERC-1155 checks
const contract: any = await erc1155ContractFactory.at(contractAddress)

try {
const r = await sendSingle(provider, await contract.balanceOf.toRPCMessage(contract.address, 0))

if (!r.result) {
return false
}
return !!contract.balanceOf.unpackOutput(toData(r.result))
} catch (_) {
return false
}
}

async function ensureContractsKnown(contractAddresses: ContractAddress[]) {
const needToFigureOut = contractAddresses
.map((contractAddress) => contractAddress.toLowerCase())
.filter((contractAddress) => !data[contractAddress])

if (needToFigureOut.length > 0) {
for (const contract of needToFigureOut) {
if (await checkIfErc1155(contract)) {
data[contract] = ContractType.ERC1155
} else if (await checkIfErc721(contract)) {
data[contract] = ContractType.ERC721
} else {
data[contract] = ContractType.UNKNOWN
}
}

logger.debug('Updating contract cache', { file, newContracts: needToFigureOut.join(', ') })
saveCacheFile(file, data)
}
}

return {
isErc721,
isErc1155,
isUnknown,
ensureContractsKnown
}
}
Loading

0 comments on commit f1efb8f

Please sign in to comment.