Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
# High Gas Agent
# NFT Sleep Minting Agent

## Description

This agent detects transactions with high gas consumption
This agent detects transactions that may indicate NFT sleep minting.
Sleep minting is a technique where someone will Mint an NFT directly to the wallet of a famous artist and then later take that NFT back. This causes NFT marketplaces to show that an NFT was created by a famous artist (as the person who minted it) even though they did not.

## Supported Chains

- Ethereum
- List any other chains this agent can support e.g. BSC

## Alerts

Describe each of the type of alerts fired by this agent
- SLEEPMINT-1
- Fired when an NFT is transfered by an address that is not the owner of the NFT.
- i.e., the transaction sender != the transferFrom argument in an emitted Transfer() event
- Severity is always set to "unknown".
- This type of transfer may be OK. There are cases where an address is approved to transfer an NFT on another person's behalf.
- Type is always set to "suspicious".

- FORTA-1
- Fired when a transaction consumes more gas than 1,000,000 gas
- Severity is always set to "medium" (mention any conditions where it could be something else)
- Type is always set to "suspicious" (mention any conditions where it could be something else)
- Mention any other type of metadata fields included with this alert
- SLEEPMINT-2
- Fired when there is an NFT approve where the address that sent the transaction is different from the current NFT owner.
- Severity is always set to "medium".
- Type is always set to "suspicious".

## Test Data

The agent behaviour can be verified with the following transactions:

- 0x1b71dcc24657989f920d627c7768f545d70fcb861c9a05824f7f5d056968aeee (1,094,700 gas)
- 0x8df0579bf65e859f87c45b485b8f1879c56bc818043c3a0d6870c410b5013266 (2,348,226 gas)
- SLEEPMINT-1 (Mainnet): 0x57f23fde8e4221174cfb1baf68a87858167fec228d9b32952532e40c367ef04e
- SLEEPMINT-1 (Rinkeby): 0x3fdd4435c13672803490eb424ca93094b461ae754bd152714d5b5f58381ccd4b
- SLEEPMINT-2 (Rinkeby): 0x53aa1bd7fa298fa1b96eeed2a4664db8934e27cd28ac0001a5bf5fa3b30c6360
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "forta-agent-starter",
"version": "0.0.1",
"description": "Forta Agent Typescript starter project",
"description": "Forta Agent for NFT Sleep Minting",
"scripts": {
"build": "tsc",
"start": "npm run start:dev",
Expand Down
172 changes: 155 additions & 17 deletions src/agent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,184 @@ import {
FindingSeverity,
Finding,
HandleTransaction,
createTransactionEvent
createTransactionEvent,
getEthersProvider,
} from "forta-agent"
import agent from "./agent"

describe("high gas agent", () => {
import {ethers} from 'ethers';


describe("NFT Sleep agent", () => {

let handleTransaction: HandleTransaction

const createTxEventWithGasUsed = (gasUsed: string) => createTransactionEvent({
transaction: {} as any,
receipt: { gasUsed } as any,
let abiCoder: ethers.utils.AbiCoder

let transferTopic = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
let approveTopic = '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925'

let txnSender = "0x87F6cA7862feA6411de6c0aFc1b4b23DD802bf00".toLowerCase()
let famousArtist = "0xc6b0562605D35eE710138402B878ffe6F2E23807".toLowerCase()
let thirdParty = "0xd8dB81216D8cf1236d36B4A1c328Fbd5CB2bD1e7".toLowerCase()
let NFTContractAddress: string;

// construct a transaction event for testing with event logs
const createTxEvent = (from:string, to:string, logs: any) => createTransactionEvent({
transaction: {from, to} as any,
receipt: { logs } as any,
block: {} as any,
})

beforeAll(() => {
// construct a log object for a transaction event
const createLog = (address: string, topics: string[]) => {
return {
address: address,
topics: topics,
data: '0x',
logIndex: 123,
blockNumber: 12170189,
blockHash: '0xbc94a2ea5b539dfd5115748137d2bab00a76f6e178f3eeb6ba225b26ed9288c4',
transactionIndex: 161,
transactionHash: '0x57f23fde8e4221174cfb1baf68a87858167fec228d9b32952532e40c367ef04e',
removed: false
}
}

beforeAll(async () => {
handleTransaction = agent.handleTransaction
abiCoder = new ethers.utils.AbiCoder()
const provider = new ethers.providers.JsonRpcProvider(getEthersProvider().connection)

// use an NFT contract on correct network to make sure first section of agent passes (checking if ERC-721)
let networkDetails = await provider.getNetwork()
NFTContractAddress = networkDetails.chainId === 4 ? "0x23414f4f9cb421b952c9050f961801bb2c8b8d58".toLowerCase() : "0x67D9417C9C3c250f61A83C7e8658daC487B56B09".toLowerCase()


})

describe("handleTransaction", () => {
it("returns empty findings if gas used is below threshold", async () => {
const txEvent = createTxEventWithGasUsed("1")

it("returns a finding of a transfer mismatch", async () => {

// create fake transfer event
const txEvent = createTxEvent(
txnSender,
NFTContractAddress,
[
createLog(
NFTContractAddress,
[
transferTopic,
abiCoder.encode(["address"],[famousArtist]),
abiCoder.encode(["address"],[thirdParty]),
abiCoder.encode(["uint256"],[1])
]
)
]
)

const findings = await handleTransaction(txEvent)

expect(findings).toStrictEqual([])
expect(findings).toStrictEqual([
Finding.fromObject({
name: "Sleep Minted an NFT",
description: `An NFT Transfer was initiated by ${txnSender} to transfer an NFT owned by ${famousArtist}`,
alertId: "SLEEPMINT-1",
severity: FindingSeverity.Unknown,
type: FindingType.Suspicious
}),
])
})

it("returns a finding if gas used is above threshold", async () => {
const txEvent = createTxEventWithGasUsed("1000001")

it("returns a finding of an approva mismatch", async () => {

// create fake approve event
const txEvent = createTxEvent(
txnSender,
NFTContractAddress,
[
createLog(
NFTContractAddress,
[
approveTopic,
abiCoder.encode(["address"],[famousArtist]),
abiCoder.encode(["address"],[txnSender]),
abiCoder.encode(["uint256"],[1])
])
]
)

const findings = await handleTransaction(txEvent)

expect(findings).toStrictEqual([
Finding.fromObject({
name: "High Gas Used",
description: `Gas Used: ${txEvent.gasUsed}`,
alertId: "FORTA-1",
type: FindingType.Suspicious,
severity: FindingSeverity.Medium
}),
name: "Sleep Minted an NFT",
description: `An NFT was approved for ${txnSender}, by ${txnSender}, but owned by ${famousArtist}.`,
alertId: "SLEEPMINT-2",
severity: FindingSeverity.Medium,
type: FindingType.Suspicious
}),
])
})



it("returns no findings if actual owner transfers the NFT", async () => {

// create honest approve event
const txEvent = createTxEvent(
famousArtist,
NFTContractAddress,
[
createLog(
NFTContractAddress,
[
transferTopic,
abiCoder.encode(["address"],[famousArtist]),
abiCoder.encode(["address"],[thirdParty]),
abiCoder.encode(["uint256"],[1])
])
]
)

const findings = await handleTransaction(txEvent)

expect(findings).toStrictEqual([])

})


it("returns no findings if actual owner approves another person to transfer the NFT", async () => {

// create honest approve event
const txEvent = createTxEvent(
famousArtist,
NFTContractAddress,
[
createLog(
NFTContractAddress,
[
approveTopic,
abiCoder.encode(["address"],[famousArtist]),
abiCoder.encode(["address"],[thirdParty]),
abiCoder.encode(["uint256"],[1])
])
]
)

const findings = await handleTransaction(txEvent)

expect(findings).toStrictEqual([])
})








})
})
73 changes: 27 additions & 46 deletions src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,50 @@
import {
BlockEvent,
Finding,
HandleBlock,
HandleTransaction,
TransactionEvent,
FindingSeverity,
FindingType,
getEthersProvider
} from 'forta-agent'

import { BigNumber } from 'bignumber.js'
import { ethers } from 'ethers'
import ERC721_ABI from "./ERC721_ABI.json";
import transferMismatch from './transfer.mismatch'
import approveMismatch from './approve.mismatch'

import {
ERC721_INTERFACE_ID,
} from './constants'

const ERC721_INTERFACE_ID = 0x5b5e139f
const transferEvent = "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)";
const approveEvent = ""
const mintEvent = ""
import { ethers, Contract } from 'ethers'
import ERC721_ABI from "./ERC721_ABI.json";

const handleTransaction: HandleTransaction = async (txEvent: TransactionEvent) => {
const findings: Finding[] = []
let contract: ethers.Contract;
let findings: Finding[] = []

// create an ethers contract object to check if it is an ERC-721 contract
const to: string = txEvent.to as string
let contract: Contract = new ethers.Contract(to, ERC721_ABI, getEthersProvider())

try {

contract = new ethers.Contract(to, ERC721_ABI, getEthersProvider())
let isNFT = await contract.supportsInterface(ERC721_INTERFACE_ID)
if (!isNFT){
throw 'not an ERC721 contract!'
}
let isNFT = await contract.supportsInterface(ERC721_INTERFACE_ID).catch((error: Error) => {
// contract does not support ERC-165 interface if this fails so return empty findings
console.log(error)
return findings
})

} catch (err){
console.log(err)
// check to see if the contract supports ERC-721
if (!isNFT){
return findings
}

const fromAddress = txEvent.from
const transfers = txEvent.filterLog(transferEvent, contract.address);

for (let transfer of transfers){
const transferFromAddress = transfer.args.from

if (transferFromAddress != fromAddress){
findings.push(Finding.fromObject({
name: "Sleeping Minted an NFT",
description: `An NFT Transfer was initiated by ${fromAddress} to transfer an NFT owned by ${transferFromAddress}`,
alertId: "SLEEPMINT-1",
severity: FindingSeverity.Unknown,
type: FindingType.Suspicious
}))
}
}
findings = (
await Promise.all([
transferMismatch.handleTransaction(txEvent),
approveMismatch.handleTransaction(txEvent),
])
).flat()

return findings
}

}

// const handleBlock: HandleBlock = async (blockEvent: BlockEvent) => {
// const findings: Finding[] = [];
// // detect some block condition
// return findings;
// }

export default {
handleTransaction,
// handleBlock
handleTransaction
}
Loading