Create Review Rating DAO with Multisig Tutorial
Introduction
The example illustrates the creation of a review rating platform for the food service industry, utilizing multisignature functionality on IOTA. Unlike conventional review platforms, which frequently conceal the algorithms used to rate reviews, this model leverages a transparent, on-chain algorithm that anyone can access and verify. IOTA's low transaction costs make it economically viable to submit, evaluate, and rank all reviews directly on-chain.
Multisig Setup for Moderators: The platform uses a multisig account to ensure that key actions like removing inappropriate reviews or approving reward distributions are authorized by multiple trusted moderators.
Personas
There are four actors in the typical workflow of the Reviews Rating example.
- Service: Review requester.
- Dashboard: Review hub.
- Reviewer: Review creator.
- Moderator: Review list editor.
Service Owners
Service owners, such as restaurants, list their offerings on the platform with the goal of attracting more customers through positive reviews. To incentivize this, they set aside a specific amount of IOTA in a reward pool. This pool is used to reward reviewers who provide high-rated feedback. A proof of experience (PoE) NFT serves as confirmation that a reviewer has used the service, and this NFT can later be burned to submit a verified review. Service owners issue unique identifiers, possibly through QR codes, to link reviews to individual customers.
Reviewers
Reviewers are customers who utilize the platform to share their experiences. They provide feedback through comments that highlight particular aspects of the service, along with a star rating to guide others. Reviews are evaluated, with the most helpful ones receiving the highest ratings. The top 10 highest-rated reviews for each service receive rewards from the service owner. The frequency of reward distribution is determined by the service owner, such as weekly or monthly disbursements.
Review Readers
Review readers use the platform to access feedback, helping them make informed decisions when choosing services. They contribute by voting on reviews, and these votes influence the algorithm that ranks the reviews. Authors of the highest-rated reviews are rewarded based on this ranking. Although not part of this guide, the system could be expanded to reward readers for participating by casting votes on reviews.
Moderators
Moderators operate within a multisignature setup, ensuring that significant actions, such as removing reviews or distributing rewards, require authorization from multiple parties. This approach enhances security and transparency, as decisions are made collectively, promoting accountability in the moderation process. Moderators review the content and have the authority to remove reviews containing inappropriate material.
Although this guide does not provide a built-in incentive structure for moderators, service owners could contribute to a compensation pool that rewards moderators over time. A staking mechanism could also be implemented, where individuals stake tokens to influence how rewards are allocated to moderators, akin to staking systems for on-chain validators. In this setup, moderator decisions would be influenced by a quorum based on the amount of staked tokens, creating incentives for moderators to perform their duties effectively.
Multisig Environment for Moderators
Moderators operate within a multisignature (multisig) framework, which means any key actions, such as removing inappropriate content or distributing rewards, must be collectively approved by multiple parties, rather than a single individual. This process significantly enhances the security of the system by preventing unilateral decisions and reducing the risk of malicious activity.
In practice, this collaborative decision-making model ensures that no single moderator can act alone. Instead, any action taken requires the agreement of multiple stakeholders. This not only mitigates the risk of abuse but also promotes greater accountability and transparency, as all decisions are publicly verifiable and must meet the required signature threshold before they are executed.
By enforcing these shared responsibilities, the multisig structure fosters a sense of trust within the community. It encourages balanced and thoughtful deliberation when dealing with sensitive issues like content moderation and rewards distribution, thus promoting a more fair and reliable system overall.
Moderators work within a multisig environment, meaning key actions—such as removing inappropriate reviews or distributing rewards—must be approved by multiple parties. This approach enhances security, encourages collaborative decision-making, and ensures transparency.
Creating a Multisig for Moderators
To implement multisig for moderators, follow these steps:
1. Create Keys for Each Moderator
Each moderator requires a unique key and corresponding IOTA address. To generate these keys and addresses, you can use any of the following key types: ed25519
, secp256k1
, or secp256r1
. Use the following commands to create a key and address for each moderator:
iota client new-address ed25519
iota client new-address secp256k1
iota client new-address secp256r1
2. Add Keys to IOTA Keystore
iota keytool list
This command will list the generated keys with their corresponding addresses and public keys.
expected output :
╭────────────────────────────────────────────────────────────────────────────────────────────╮
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ iotaAddress │ <IOTA-ADDRESS> │ │
│ │ publicBase64Key │ <PUBLIC-KEY> │ │
│ │ keyScheme │ ed25519 │ │
│ │ flag │ 0 │ │
│ │ peerId │ <PEER-ID> │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ iotaAddress │ <IOTA-ADDRESS> │ │
│ │ publicBase64Key │ <PUBLIC-KEY> │ │
│ │ keyScheme │ secp256k1 │ │
│ │ flag │ 0 │ │
│ │ peerId │ <PEER-ID> │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ iotaAddress │ <IOTA-ADDRESS> │ │
│ │ publicBase64Key │ <PUBLIC-KEY> │ │
│ │ keyScheme │ secp256r1 │ │
│ │ flag │ 0 │ │
│ │ peerId │ <PEER-ID> │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────────╯
3. Create a Multisig Address
Create the multisig address by combining the public keys of all moderators and setting their weights and a threshold:
iota keytool multi-sig-address --pks <PUBLIC-KEY-1> <PUBLIC-KEY-2> <PUBLIC-KEY-3> --weights 1 1 1 --threshold 2
This command sets up a multisig address where PUBLIC-KEY-1
, PUBLIC-KEY-2
& PUBLIC-KEY-3
are the public keys we retreived in step 2.
expected output :
╭─────────────────┬─────────────────────────────────────────────────────────────────────── ─────────────╮
│ multisigAddress │ <MULTISIG-ADDRESS> │
│ multisig │ ╭────────────────────────────────────────────────────────────────────────────────╮ │
│ │ │ ╭─────────────────┬──────────────────────────────────────────────────────────╮ │ │
│ │ │ │ address │ <IOTA-ADDRESS> │ │ │
│ │ │ │ publicBase64Key │ <PUBLIC-KEY> │ │ │
│ │ │ │ weight │ 1 │ │ │
│ │ │ ╰─────────────────┴──────────────────────────────────────────────────────────╯ │ │
│ │ │ ╭─────────────────┬──────────────────────────────────────────────────────────╮ │ │
│ │ │ │ address │ <IOTA-ADDRESS> │ │ │
│ │ │ │ publicBase64Key │ <PUBLIC-KEY> │ │ │
│ │ │ │ weight │ 2 │ │ │
│ │ │ ╰─────────────────┴──────────────────────────────────────────────────────────╯ │ │
│ │ │ ╭─────────────────┬──────────────────────────────────────────────────────────╮ │ │
│ │ │ │ address │ <IOTA-ADDRESS> │ │ │
│ │ │ │ publicBase64Key │ <PUBLIC-KEY> │ │ │
│ │ │ │ weight │ 3 │ │ │
│ │ │ ╰─────────────────┴──────────────────────────────────────────────────────────╯ │ │
│ │ ╰────────────────────────────────────────────────────────────────────────────────╯ │
╰─────────────────┴────────────────────────────────────────────────────────────────────────────────────╯
To remove a review, moderators must sign a transaction using the multisig setup. The transaction will require signatures from two out of the three moderators to be executed.
4. Serialize a Transaction
Serialize the transaction to remove a review to be signed by the multisig account:
iota client call --package <PACKAGE HERE> --module <MODULE HERE> --function "delete_review" --args <FUNCTION ARGUEMENTS> --gas-budget 10000000 --serialize-unsigned-transaction
Arguments:
--package
: The package ID containing the contract.--module
: The module where thedelete_review
function is defined.--function
: The function i.edelete_review
.--args
: Arguments required by the function.
5. Sign the Transaction
Each moderator signs the serialized transaction:
iota keytool sign --address <IOTA-ADDRESS> --data <TX_BYTES>
Arguments:
-address
: The IOTA address of the moderator signing the transaction.--data
: The serialized transaction bytes that need to be signed.
6. Combine Signatures
Once two moderators have signed, combine their signatures into a single multisig signature:
iota keytool multi-sig-combine-partial-sig --pks <PUBLIC-KEY-1> <PUBLIC-KEY-2> --weights 1 1 1 --threshold 2 --sigs <SIGNATURE-1> <SIGNATURE-2>
Arguments:
--pks
: Public keys of the moderators who have signed the transaction.--weights
: Weights assigned to each public key.--threshold
: Minimum number of signatures required to execute the transaction.--sigs
: Signatures from the moderators that are being combined.
7. Execute the Multisig Transaction
Finally, execute the multisig transaction using the combined signatures:
iota client execute-signed-tx --tx-bytes <TX_BYTES> --signatures <SERIALIZED-MULTISIG>
Arguments:
--tx-bytes
: Transaction bytes.--signatures
: Serialized multisig signature.
The transaction will now be processed, and the review will be removed from the platform. This multisig setup ensures that no single moderator can act unilaterally, thus maintaining fairness and accountability on the platform.
How Reviews are Scored
The reviews are scored on chain using the following criteria:
- Intrinsic score (IS): Length of review content.
- Extrinsic score (ES): Number of votes a review receives.
- Verification multiplier (VM): Reviews with Proof of Experience(PoE) receive a multiplier to improve rating.
Total Score = (IS + ES) * VM
Smart Contracts
There are several modules that create the backend logic for the example.
Dashboard.move
The dashboard.move
module defines the Dashboard
struct that groups services.
/// Dashboard is a collection of services
public struct Dashboard has key, store {
id: UID,
service_type: String
}
The services are grouped by attributes, which can be cuisine type, geographical location, operating hours, Google Maps ID, and so on. To keep it basic, the example stores only service_type
(for example, fast food, Chinese, Italian).
/// Create a new dashboard
public fun create_dashboard(
service_type: String,
ctx: &mut TxContext,
) {
let db = Dashboard {
id: object::new(ctx),
service_type
};
transfer::share_object(db);
}
A Dashboard
is a shared object, so any service owner can register their service to a dashboard.
A service owner should look for dashboards that best match their service attribute and register.
A dynamic field stores the list of services that are registered to a dashboard.
A service may be registered to multiple dashboards at the same time. For example, a Chinese-Italian fusion restaurant may be registered to both the Chinese and Italian dashboards.
create_dashboard
Function:
- Purpose: This function creates a new
Dashboard
for a specific service type. - Parameters:
service_type: String
: A string representing the type of service the dashboard is created for.ctx: &mut TxContext
: A mutable reference to the transaction context, used to generate unique IDs and manage the transaction environment.
- Process:
- A new
Dashboard
is created with an ID generated byobject::new(ctx)
, and theservice_type
is stored. - The
Dashboard
object is then shared with others usingtransfer::share_object(db)
, making it accessible for further use.
- A new
public fun register_service(db: &mut Dashboard, service_id: ID) {
df::add(&mut db.id, service_id, service_id);
}
register_service
Function:
- Purpose: This function registers a service to an existing
Dashboard
. - Parameters:
db: &mut Dashboard
: A mutable reference to theDashboard
to which the service is being registered.service_id: ID
: The unique identifier of the service being registered.
- Process:
- The function adds the
service_id
to theDashboard
's list of services by invokingdf::add(&mut db.id, service_id, service_id)
. - The
df::add
function associates theservice_id
with the dashboard'sid
, effectively linking the service to the dashboard.
- The function adds the
See Shared versus Owned Objects for more information on the differences between object types.
Review.move
A Review
is a shared object, so anyone can cast a vote on a review and update its total_score
field.
After total_score
is updated, the update_top_reviews
function can be called to update the top_reviews
field of the Service
object.
This module defines the Review
struct. In addition to the content of a review, all the elements that are required to compute total score are stored in a Review
object.
/// Represents a review of a service
public struct Review has key, store {
id: UID,
owner: address,
service_id: ID,
content: String,
// intrinsic score
len: u64,
// extrinsic score
votes: u64,
time_issued: u64,
// proof of experience
has_poe: bool,
// total score
total_score: u64,
// overall rating value; max=5
overall_rate: u8,
}
Review
Struct:
id: UID
: Unique identifier for the review.owner: address
: Address of the review owner.service_id: ID
: Identifier of the reviewed service.content: String
: The content of the review.len: u64
: Represents the length of the review content, considered the intrinsic score.votes: u64
: Number of upvotes or endorsements for the review, considered the extrinsic score.time_issued: u64
: Timestamp when the review was written.has_poe: bool
: A boolean flag indicating if the reviewer has proof of experience (PoE), which can affect the review's weight.total_score: u64
: The overall calculated score of the review.overall_rate: u8
: The overall rating given in the review.
/// Updates the total score of a review
fun update_total_score(rev: &mut Review) {
rev.total_score = rev.calculate_total_score();
}
update_total_score
Function:
- This function updates the
total_score
of the review by calling thecalculate_total_score
function.
/// Calculates the total score of a review
fun calculate_total_score(rev: &Review): u64 {
let intrinsic_score: u64 = rev.len;
let extrinsic_score: u64 = 10 * rev.votes;
let vm: u64 = if (rev.has_poe) { 2 } else { 1 };
(intrinsic_score + extrinsic_score) * vm
}
calculate_total_score
Function:
- This function calculates the
total_score
based on both intrinsic and extrinsic factors.intrinsic_score
: The score based on the length of the review (len
). It is capped at 150.extrinsic_score
: The score based on the number of votes, with each vote contributing 10 points.- VM (Vote Multiplier): If the reviewer has proof of experience (
has_poe
istrue
), the score is multiplied by 2; otherwise, it's multiplied by 1.
- The total score is the sum of the intrinsic and extrinsic scores, multiplied by the VM factor.
Service.move
This module defines the Service
struct that service owners manage.
/// Represents a service
public struct Service has key, store {
id: UID,
reward_pool: Balance<IOTA>, // IOTA currency
reward: u64,
top_reviews: vector<ID>,
reviews: ObjectTable<ID, Review>,
overall_rate: u64,
name: String
}
Reward Distribution
The same amount is rewarded to top reviewers, and the reward is distributed to 10 participants at most.
The pool of IOTA
tokens to be distributed to reviewers is stored in the reward_pool
field, and the amount of IOTA
tokens awarded to each participant is configured in reward
field.
Storage for Reviews
Because anyone can submit a review for a service, Service
is defined as a shared object. All the reviews are stored in the reviews
field, which has ObjectTable<ID, Review>
type. The reviews
are stored as children of the shared object, but they are still accessible by their ID
.
In other words, anyone can go to a transaction explorer and find a review object by its object ID, but they won't be able to use a review as an input to a transaction by its object ID.
The top rated reviews are stored in top_reviews
field, which has vector<ID>
type. A simple vector can store the top rated reviews because the maximum number of reviews that can be rewarded is 10.
The elements of top_reviews
are sorted by the total_score
of the reviews, with the highest rated reviews coming first. The vector contains the ID
of the reviews, which can be used to retrieve content and vote count from the relevant reviews
.
Casting Votes
A reader can cast a vote on a review to rate it as follows:
/// Upvotes a review
public fun upvote(
service: &mut Service,
review_id: ID,
_upvoter: address,
) {
let review = service.reviews.borrow_mut(review_id);
let total_score = review.upvote();
service.reorder(review_id, total_score);
}
upvote
Function
The upvote
function is responsible for recording an upvote for a specific review. It takes two key pieces of information: the review’s ID and a reference to the Service
that contains all the reviews.
-
Retrieving the Review: It fetches the review from the
service.reviews
using thereview_id
provided. Once the review is found, the function proceeds to update its score. -
Incrementing the Score: It calls the
upvote()
method on the review object. This method increases the total number of upvotes for that particular review. -
Reordering the Top Reviews: After the score is updated, the
upvote
function calls another function,service.reorder()
. This step ensures that the list of top reviews reflects the new score, potentially moving the upvoted review to a higher position or introducing it to the list if it wasn’t previously in the top reviews.
fun reorder(
service: &mut Service,
review_id: ID,
total_score: u64
) {
let (contains, idx) = service.top_reviews.index_of(&review_id);
if (!contains) {
service.update_top_reviews(review_id, total_score);
} else {
// remove existing review from vector and insert back
service.top_reviews.remove(idx);
let idx = service.find_idx(total_score);
service.top_reviews.insert(review_id, idx);
}
}
reorder
Function
The reorder
function takes care of updating the order of the top_reviews
list when a review’s score changes. It works as follows:
-
Checking if the Review is in Top Reviews: It first checks whether the review is already in the
top_reviews
list by finding its position or verifying if it is missing. -
Handling Reviews Already in the List: If the review is already in the
top_reviews
, the function removes it from its current position and calculates the new position based on its updated score. -
Adding New Reviews to Top Reviews: If the review isn’t currently in the
top_reviews
, it calls another function,service.update_top_reviews()
, to determine if the review now qualifies to be part of the top reviews based on its new score.
fun update_top_reviews(
service: &mut Service,
review_id: ID,
total_score: u64
) {
if (service.should_update_top_reviews(total_score)) {
let idx = service.find_idx(total_score);
service.top_reviews.insert(review_id, idx);
service.prune_top_reviews();
};
}
update_top_reviews
Function
This function ensures that the top reviews list is always up to date with the most highly rated reviews. It does the following:
-
Checking if the Review Qualifies: It first checks whether the review's score is high enough to place it within the top 10 reviews.
-
Inserting the Review: If the review qualifies, it finds the correct position for the new review by comparing its score with the reviews already in the list.
-
Ensuring Top Reviews Limit: After inserting the review, it checks if the number of top reviews exceeds the allowed limit. If it does, the review with the lowest score is removed from the list.
fun find_idx(service: &Service, total_score: u64): u64 {
let mut i = service.top_reviews.length();
while (0 < i) {
let review_id = service.top_reviews[i - 1];
if (service.get_total_score(review_id) > total_score) {
break
};
i = i - 1;
};
i
}
find_idx
Function
The find_idx
function determines where a review should be placed in the top_reviews
list based on its total score. Here’s how it works:
-
Looping Through Reviews: It loops through the current
top_reviews
list, comparing the new review’s score with each existing review. -
Finding the Right Spot: The loop stops when it finds a review with a higher score, meaning the new review should be inserted right before it. This ensures that the list remains sorted in descending order of scores.
fun prune_top_reviews(
service: &mut Service
) {
while (service.top_reviews.length() > MAX_REVIEWERS_TO_REWARD) {
service.top_reviews.pop_back();
};
}
prune_top_reviews
Function
Finally, the prune_top_reviews
function ensures that the top_reviews
list never exceeds a certain number of entries (defined by MAX_REVIEWERS_TO_REWARD
).
-
Checking the Length: It checks if the length of the
top_reviews
list is greater than the maximum allowed number of top reviews. -
Removing the Lowest Review: If the list is too long, the function removes the review with the lowest score by popping the last review from the list.
Whenever someone casts a vote on a review, the total_score
of the review is updated and the update_top_reviews
function updates the top_reviews
field, as needed.
Casting a vote also triggers a reordering of the top_reviews
field to ensure that the top rated reviews are always at the top.
Authorization
/// A capability that can be used to perform admin operations on a service
struct AdminCap has key, store {
id: UID,
service_id: ID
}
/// Represents a moderator that can be used to delete reviews
struct Moderator has key {
id: UID,
}
This example follows a capabilities pattern to manage authorizations.
For example, SERVICE OWNERS
are given AdminCap
and MODERATORS
are given Moderator
such that only they are allowed to perform privileged operations.
To learn more about the capabilities pattern, see The Move Book.
Deployment
The deployment of the contract is done by the admin, which is a multi-sig address. To deploy the contract, navigate to the publish.sh
script on the path, examples/move/reviews_rating/publish.sh
within the repository and follow the steps below.
1. Create the .env
File
You need to create a .env
file that contains important environment variables required for the deployment. Here's an example of what the .env file should look like:
# Add Iota network url
IOTA_NETWORK="iota-network-url"
# Add faucet link
IOTA_FAUCET="iota-network-faucet-link"
# Path to move package
MOVE_PACKAGE_PATH=.
Replace iota-network-url
and iota-network-faucet-url
with the IOTA network URL and faucet for tokens.
2. Run the publish.sh
Script
The publish.sh
script automates the process of deploying a Move contract to the IOTA Alphanet. Here’s how you can run it:
sh publish.sh
3. Output
On successful deployment below text will be returned.
Contract Deployment finished!
Conclusion
This tutorial explains how to build a decentralized review rating platform using multisig on IOTA. Service owners can list their services, while reviewers provide feedback that is rated by readers. Moderators maintain quality control, ensuring that only appropriate reviews remain. Reviews are scored based on their content, upvotes, and proof of experience (PoE), with top reviews earning rewards. The multisig setup enhances security, as key actions like removing reviews or distributing rewards require approval from multiple parties. By storing the review algorithm and data on-chain using Move-based smart contracts, the platform guarantees transparency and fairness.