KREAd is an application that uses and implements composable NFTs on Agoric, the application allows users to customize their own characters by equipping and unequipping items.
fix performance issues identified by the agoric OpCo team ( a detailed description of the issues and possible solutions can be found here;
address some UI bugs identified by the Kryha team;
assure the dapp compatibly with the latest agoric-sdk versions.
We advise to explore the code analysis for a detailed description of the KREAd features, with the support of sequence diagrams.
Performance issues
The initial effort will be focused on the contract side, more specifically, address the following task:
rewrite the contract to hold a separate Character and Item Purse for each character, and hold the equipped Items in that Purse, instead of in an open Seat.
requires a KREAd contract upgrade to be useful
requires replacing all the InventorySeats as remediation, perhaps one character at a time (as user actions touch them)
The changes made to the KREAd contract has to ensure the following rules:
offer safety
each user can only interact with their own characters/items
the items equipped follows the correct types limitations
the correct behaviour of characters, items and market features
the characters are not exposed to features external to KREAd
Proposed solution:
The solution designed will require multiple changes to the current implementation.
Wash Trades
The first change intend to remove the need to, when minting a character, creating 2 identical characters (A and B), where the character A is allocated in the userSeat of the user who made the offer, and the character B, along with the default Items, will be allocated in the inventorySeat managed by the Kread contract.
Based on this comment from Chris Hibbert, it is explained that we can indeed execute "wash trades", wish means that the user can give and request the same asset when building an offer. Although It is important to noticed that the proposal will need a different keyword for the want and give Character amount.
Items Purse
The second change intend to remove the need for the inventorySeat, that holds the duplicated Character along with the equipped Items.
To remove the dependency on the inventorySeat, we intend to use the Items issuer to make an empty Purse for each Character that will hold the equipped Items. This purse will be included in the Character record, replacing the inventory seat attribute. This way we can deposit and withdraw the required Items to assure the normal flow of KREAd without the performance load of the previous design.
ERTP issuer
To complement the change above, we wish to replace the ZCFMint created for both Character and Item into an IssuerKit.
The reason behind this decision is based on the Vats where the Purses will be stored. When using the makeZCFMint method, the Purses created from the issuer returned will be stored in the Zoe Vat, while with the makeIssuerKit method, the Purses created will be stored in the KREAd Vat.
We expect that this will reduce the load on Zoe and improve its performance.
Market Purse
When an user wish to sell a Character or an Item, the asset will be escrowed in Zoe, which contributes for the performance issue described above.
To address this issue, one additional change we intend to implement is to create a dedicated Purse for every sellRecord, that will hold the asset until the sell is executed or the user decides to cancel it. This Purse will be recorded in the contract state, by including it in the MarketEntryGuard, along with the seller Seat.
Design representation
Since the purpose of the next diagrams are to display the changes we intend to do to the original design, we will omit some details of the normal flow of the KREAd features that are not relevant.
Mint Character
sequenceDiagram
box Blue User
participant Usw as SmartWallet
end
participant Zoe
box Green KREAd
participant kreadKit
participant State
end
kreadKit ->> kreadKit: itemKit = makeIssuerKit()
Usw->>Zoe: mint offerSpec
Zoe->>kreadKit: Proposal { give Price }
Zoe-->>Usw: UserSeat
kreadKit->>kreadKit: characterMint.mintPayment(baseCharacter)
kreadKit->>kreadKit: itemMint.mintPayment(baseItems)
create participant Purse
kreadKit->>Purse: itemsIssuer.makeEmptyPurse()
Purse-->>kreadKit: provide inventoryPurse
create participant Seat
kreadKit->>Seat: zcf.makeEmptySeatKit()
Seat-->>kreadKit: provide userSeat and zcfSeat
kreadKit->>Zoe: transfer Character
kreadKit->>Seat: transfer Items
kreadKit->>Zoe: exit userSeat
kreadKit->>Seat: exit inventorySeat
kreadKit->>Seat: inventorySeat.getPayouts()
destroy Seat
Seat-->>kreadKit: return Items payment
kreadKit->>Purse: deposit Items into Purse
Zoe-->>Usw: Character payout
kreadKit->>State: remove baseCharacter from character.base
note over kreadKit: the new Character has the attribute Items purse
kreadKit->>State: add the new Character to character.entries
kreadKit->>State: remove baseItems from items.base
kreadKit->>State: add Purse to items.entries
Equip Item
On this diagram, we can observe an inventorySeat being created, the reason for that is to ensure the offer safety required for this process. More specifically, we need to ensure that the user is providing the respective Item payment that was specified in the proposal.
sequenceDiagram
box Blue User
participant Usw as SmartWallet
end
participant Zoe
box Green KREAd
participant kreadKit
participant State
end
Usw->>Zoe: equip offerSpec
Zoe->>kreadKit: Proposal {give CharacterIn + Item } {want CharacterOut}
Zoe-->>Usw: UserSeat
kreadKit->>kreadKit: validate Character
kreadKit->>State: get characterRecord from character.entries
State-->>kreadKit: return characterRecord
kreadKit->>State: get respective Purse from items.entries
create participant Purse
State->>Purse: get Purse
Purse-->>kreadKit: provide inventoryPurse
create participant Seat
kreadKit->>Seat: zcf.makeEmptySeatKit()
Seat-->>kreadKit: provide userSeat and zcfSeat
kreadKit->>Zoe: transfer Character
kreadKit->>Seat: transfer Items
kreadKit->>Zoe: exit userSeat
kreadKit->>Seat: exit inventorySeat
kreadKit->>Seat: inventorySeat.getPayouts()
destroy Seat
Seat-->>kreadKit: return Items payment
kreadKit->>Purse: deposit Items into Purse
Zoe-->>Usw: Character payout
Unequip Item
sequenceDiagram
box Blue User
participant Usw as SmartWallet
end
participant Zoe
box Green KREAd
participant kreadKit
participant State
end
Usw->>Zoe: unequip offerSpec
Zoe->>kreadKit: Proposal {give CharacterIn} {want CharacterOut + Item}
Zoe-->>Usw: UserSeat
kreadKit->>kreadKit: validate Character
kreadKit->>State: get characterRecord from character.entries
State-->>kreadKit: return characterRecord
create participant Purse
kreadKit->>Purse: get respective Purse
Purse-->>kreadKit: provide inventoryPurse
kreadKit->>Purse: Purse.withdraw(Item)
Purse-->>kreadKit: return Item payment
kreadKit->>Zoe: transfer Character + Item
kreadKit->>Zoe: exit userSeat
Zoe-->>Usw: Character + Item payout
Sell Character
sequenceDiagram
box Blue User
participant Usw as SmartWallet
end
participant Zoe
box Green KREAd
participant kreadKit
participant State
end
Usw->>Zoe: sellCharacter offerSpec
Zoe->>kreadKit: Proposal {give Character} {want Price}
Zoe-->>Usw: UserSeat
kreadKit->>kreadKit: validate Character
create participant Purse
kreadKit->>Purse: itemsIssuer.makeEmptyPurse()
Note right of kreadKit: Why purse from item?
Purse-->>kreadKit: provide characterMarketPurse
create participant Seat
kreadKit->>Seat: zcf.makeEmptySeatKit()
Seat-->>kreadKit: provide userSeat and zcfSeat
kreadKit->>Seat: transfer Character
kreadKit->>Seat: exit inventorySeat
kreadKit->>Seat: inventorySeat.getPayouts()
destroy Seat
Seat-->>kreadKit: return payment
kreadKit->>Purse: deposit Character into Purse
note over kreadKit: the marketEntryGuard will include the Purse
kreadKit->>State: add new sell entry to market.characterEntries
Buy Character
sequenceDiagram
box Blue User
participant Usw as SmartWallet
end
participant Zoe
box Green KREAd
participant kreadKit
participant State
end
Usw->>Zoe: buyCharacter offerSpec
Zoe->>kreadKit: Proposal {give Price} {want Character}
Zoe-->>Usw: UserSeat
kreadKit->>kreadKit: validate Character
kreadKit->>State: get sell record from market.characterEntries
State-->>kreadKit: return sell record
create participant Purse
kreadKit->>Purse: get respective Purse
Purse-->>kreadKit: provide characterMarketPurse
kreadKit->>Purse: purse.withdraw(Character)
Purse-->>kreadKit: return Character payment
create participant Seat
kreadKit->>Seat: get respective seller seat
Seat-->>kreadKit: provide userSeat
kreadKit->>Seat: transfer Price
kreadKit->>Zoe: transfer Character
destroy Seat
kreadKit->>Seat: exit sellerSeat
kreadKit->>Zoe: exit userSeat
kreadKit->>State: remove sellRecord to market.characterEntries
Contract state
The major difference in the contact state is the inventory Purse that was included into the CharacterEntryGuard
classDiagram
baggage *-- contractState
contractState *-- charactersState
contractState *-- itemsState
contractState *-- marketState
charactersState *-- characters
charactersState *-- baseCharacters
itemsState *-- items
itemsState *-- baseItems
marketState *-- characterMarket
marketState *-- itemMarket
marketState *-- marketMetrics
baseCharacters o-- BaseCharacterGuard
characters o-- CharacterEntryGuard
items o-- ItemRecorderGuard
baseItems o-- ItemGuard
characterMarket o-- MarketEntryGuard
itemMarket o-- MarketEntryGuard
marketMetrics o-- MarketMetricsGuard
CharacterEntryGuard o-- HistoryGuard
ItemRecorderGuard o-- HistoryGuard
CharacterEntryGuard o-- CharacterGuard
ItemRecorderGuard o-- ItemGuard
MarketEntryGuard o-- CharacterGuard
MarketEntryGuard o-- ItemGuard
class characters {
+String keyShape
+CharacterEntryGuard valueShape
}
class baseCharacters {
+Number keyShape
+BaseCharacterGuard valueShape
}
class items {
+Number keyShape
+ItemRecorderGuard valueShape
}
class baseItems {
+String keyShape
+List~ItemGuard~ valueShape
}
class characterMarket {
+String keyShape
+MarketEntryGuard valueShape
}
class itemMarket {
+Number keyShape
+MarketEntryGuard valueShape
}
class marketMetrics {
+String keyShape
+MarketMetricsGuard valueShape
}
class BaseCharacterGuard {
+String title
+String description
+String origin
+Number level
+String artistMetadata
+String image
+String characterTraits
}
class CharacterEntryGuard {
+String name
+CharacterGuard character
+Purse inventory
+RecorderKit inventoryKit
+List~HistoryGuard~ history
}
class ItemRecorderGuard {
+Number id
+ItemGuard item
+List~HistoryGuard~ history
}
class MarketEntryGuard {
+String or Number id
+Seat seat
+Purse purse
+RecorderKit recorderKit
+Amount askingPrice
+Amount royalty
+Amount platformFee
+CharacterGuard or ItemGuard asset
+bool isFirstSale
}
class MarketMetricsGuard {
+Number amountSold
+Number collectionSize
+Number averageLevel
+Number marketplaceAverageLevel
+Number latestSalePrice
+Number putForSaleCount
}
class HistoryGuard {
+String type
+Any data
+Timestamp timestamp
}
class CharacterGuard {
+String title
+String description
+String origin
+Number level
+String artistMetadata
+String image
+String characterTraits
+String name
+Number keyId
+Number id
+timestamp date
}
class ItemGuard {
+String name
+String category
+String description
+bool functional
+String origin
+String image
+String thumbnail
+Number rarity
+Number level
+Number filtering
+Number weight
+Number sense
+Number reserves
+Number durability
+String colors
+List~String~ artistMetadata
}
Migration
There are users who already minted characters and items with the existing way. We must implement a mechanism to migrate those users to the latest version. This means;
We must let the user know their brand for KREAdITEM will change
We must burn the characters living in inventorySeat
We must mint items using new KREAdITEM with the exact amounts that are equipped to the character
Send those newly minted items to a dedicated purse linked to each user character
Burn old items
Exit inventorySeat
We should perform this migration as old users try to interact with the application after the upgrade instead of trying to move all characters and items at once(for the sake of Zoe).
Characters and Inventory
Regarding the steps to migrate the duplicated Character and equipped Items to the new version, the steps to follow should be:
Iterate over all entries at the characterState, and for each entry, fetch the inventorySeat.
Then burn the Characters and Items held there and mint the replacement Items.
Those Items should be deposited in the dedicated Purse
Exit the inventorySeat
Items not equipped
Regarding the unequipped Items, when the user tries to equip or swap the Item, it should:
Verify the Item brand
Burn the Items and mint the replacement.
Those Items should be deposited in the dedicated Purse.
Assets on sale at the market
The assets registered in the marketState can be separated into 2 categories, based on the seller seat:
Characters and Items owned by users
Items owned by the KREAd internalSellSeat, minted at contract initialisation
For the Items owned by the KREAd, the migration process should remove those sell records from the marketState and exit the internalSellSeat.
The publishItemCollection method could be invoked again at contract upgrade, which would mint the new collection with the updated KREAdITEM issuer and deposit it in the dedicated market Purse
IMPORTANT: it is important to review the original collection used to remove any Items that is not listed in the market (that will prevent duplicating an Item that was already purchased by an user)
For the Characters and Items owned by users, the migration process could be, progressively:
removing those sell records from the marketState and exit the userSeat. The owner should then be notified to execute a second sell offer, and at this moment, his asset would be burn and the replacement would be minted with the updated KREAdITEM issuer and deposit it in the dedicated market Purse.
keep the sell seats open but the sell records would be updated with the replacement minted with the updated KREAdITEM issuer and deposit it in the dedicated market Purse.
Note: I am not sure how could we burn the asset escrowed on Zoe.
Open Questions:
While brainstorming this design, one idea proposed was to create an inventoryKit, to separate the logic behind the inventory management from the kreadKit.
I agree that it makes sense, but I consider that if we follow that pattern, the same should be done with the Character and maybe the Market as well.
So, to reduce the development effort, I built the diagrams above with a pattern more similar to the original code.
Other design pattern that seems very useful for this scenario is the use of continuous invitation. Although one question raised is if this pattern would help to remove the necessity of including the Character in the offer when interacting with an Item.
The continuous invitation pattern would help to manage the user capabilities, but may not solve the need to ensure that the user still hold the Character in its wallet.
If we decide to replace the Character and Item ZCFMint with an IssuerKit, what would be the possible implications at the migration process? Will the users be willing to burn their current assets and receive a replacement? Would the contract be capable of supporting assets from both issuers?
Should we implement measures to ensure that the Purse balances are always aligned with the contract state?
Will the design implemented for the market create a possible vulnerability that we were not exposed with the original design?
Is there any considerations we should have regarding the KREAd world/story line
Problem Definition
KREAd dapp require maintenance effort to:
We advise to explore the code analysis for a detailed description of the KREAd features, with the support of sequence diagrams.
Performance issues
The initial effort will be focused on the contract side, more specifically, address the following task:
The changes made to the KREAd contract has to ensure the following rules:
Proposed solution:
The solution designed will require multiple changes to the current implementation.
Wash Trades The first change intend to remove the need to, when minting a character, creating 2 identical characters (A and B), where the character A is allocated in the userSeat of the user who made the offer, and the character B, along with the default Items, will be allocated in the inventorySeat managed by the Kread contract.
Based on this comment from Chris Hibbert, it is explained that we can indeed execute "wash trades", wish means that the user can give and request the same asset when building an offer. Although It is important to noticed that the proposal will need a different keyword for the want and give Character amount.
Items Purse The second change intend to remove the need for the inventorySeat, that holds the duplicated Character along with the equipped Items.
To remove the dependency on the inventorySeat, we intend to use the Items issuer to make an empty Purse for each Character that will hold the equipped Items. This purse will be included in the Character record, replacing the inventory seat attribute. This way we can deposit and withdraw the required Items to assure the normal flow of KREAd without the performance load of the previous design.
ERTP issuer To complement the change above, we wish to replace the ZCFMint created for both Character and Item into an IssuerKit. The reason behind this decision is based on the Vats where the Purses will be stored. When using the makeZCFMint method, the Purses created from the issuer returned will be stored in the Zoe Vat, while with the makeIssuerKit method, the Purses created will be stored in the KREAd Vat. We expect that this will reduce the load on Zoe and improve its performance.
Market Purse When an user wish to sell a Character or an Item, the asset will be escrowed in Zoe, which contributes for the performance issue described above. To address this issue, one additional change we intend to implement is to create a dedicated Purse for every sellRecord, that will hold the asset until the sell is executed or the user decides to cancel it. This Purse will be recorded in the contract state, by including it in the MarketEntryGuard, along with the seller Seat.
Design representation
Since the purpose of the next diagrams are to display the changes we intend to do to the original design, we will omit some details of the normal flow of the KREAd features that are not relevant.
Mint Character
Equip Item
On this diagram, we can observe an inventorySeat being created, the reason for that is to ensure the offer safety required for this process. More specifically, we need to ensure that the user is providing the respective Item payment that was specified in the proposal.
Unequip Item
Sell Character
Buy Character
Contract state
The major difference in the contact state is the inventory Purse that was included into the CharacterEntryGuard
Migration
There are users who already minted characters and items with the existing way. We must implement a mechanism to migrate those users to the latest version. This means;
We should perform this migration as old users try to interact with the application after the upgrade instead of trying to move all characters and items at once(for the sake of Zoe).
Characters and Inventory
Regarding the steps to migrate the duplicated Character and equipped Items to the new version, the steps to follow should be:
Items not equipped
Regarding the unequipped Items, when the user tries to equip or swap the Item, it should:
Assets on sale at the market
The assets registered in the marketState can be separated into 2 categories, based on the seller seat:
For the Items owned by the KREAd, the migration process should remove those sell records from the marketState and exit the internalSellSeat. The publishItemCollection method could be invoked again at contract upgrade, which would mint the new collection with the updated KREAdITEM issuer and deposit it in the dedicated market Purse IMPORTANT: it is important to review the original collection used to remove any Items that is not listed in the market (that will prevent duplicating an Item that was already purchased by an user)
For the Characters and Items owned by users, the migration process could be, progressively:
Open Questions:
While brainstorming this design, one idea proposed was to create an inventoryKit, to separate the logic behind the inventory management from the kreadKit.
I agree that it makes sense, but I consider that if we follow that pattern, the same should be done with the Character and maybe the Market as well. So, to reduce the development effort, I built the diagrams above with a pattern more similar to the original code.
Other design pattern that seems very useful for this scenario is the use of continuous invitation. Although one question raised is if this pattern would help to remove the necessity of including the Character in the offer when interacting with an Item. The continuous invitation pattern would help to manage the user capabilities, but may not solve the need to ensure that the user still hold the Character in its wallet.
If we decide to replace the Character and Item ZCFMint with an IssuerKit, what would be the possible implications at the migration process? Will the users be willing to burn their current assets and receive a replacement? Would the contract be capable of supporting assets from both issuers?
Should we implement measures to ensure that the Purse balances are always aligned with the contract state?
Will the design implemented for the market create a possible vulnerability that we were not exposed with the original design?
Is there any considerations we should have regarding the KREAd world/story line