Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

48 changes: 32 additions & 16 deletions contracts/broadcast/DeployVouchMe.s.sol/534351/run-latest.json

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions contracts/script/ConfigureMonetization.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/VouchMe.sol";

contract ConfigureMonetization is Script {
function run() external {
address vouchMeAddress = vm.envAddress("VOUCHME_ADDRESS");
address treasury = vm.envAddress("TREASURY_ADDRESS");
uint256 freeThreshold = vm.envUint("FREE_THRESHOLD");
uint256 fee = vm.envUint("FEE_WEI");

vm.startBroadcast();

VouchMe vouchMe = VouchMe(vouchMeAddress);

// Safe ordering: treasury -> threshold -> fee.
// setFee requires treasury when fee > 0.
vouchMe.setTreasury(treasury);
vouchMe.setFreeThreshold(freeThreshold);
vouchMe.setFee(fee);

vm.stopBroadcast();

console.log("Configured monetization on:", vouchMeAddress);
console.log("Treasury:", treasury);
console.log("Free threshold:", freeThreshold);
console.log("Fee (wei):", fee);
}
}
13 changes: 13 additions & 0 deletions contracts/script/DeployVouchMe.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,23 @@ contract DeployVouchMe is Script {
vm.startBroadcast();

// Deploy the VouchMe contract
// The deployer (msg.sender) automatically becomes the owner
VouchMe vouchMe = new VouchMe();

// Note: Monetization is disabled by default:
// - fee = 0
// - treasury = address(0)
// - freeThreshold = type(uint256).max (unlimited)
//
// To enable monetization later, the owner can call:
// 1. setTreasury(treasuryAddress) - set where fees go
// 2. setFreeThreshold(5) - e.g., 5 free testimonials
// 3. setFee(0.001 ether) - e.g., 0.001 ETH per testimonial after threshold

vm.stopBroadcast();

console.log("VouchMe deployed at:", address(vouchMe));
console.log("Owner:", vouchMe.owner());
console.log("Monetization disabled (fee=0, treasury=address(0))");
}
}
132 changes: 129 additions & 3 deletions contracts/src/VouchMe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract VouchMe is ERC721URIStorage {
contract VouchMe is ERC721URIStorage, Ownable {
using ECDSA for bytes32;
using Strings for uint256;

uint256 private _tokenIdTracker; // Manually track token IDs
uint256 public totalProfiles; // Counter for total profiles created
uint256 public totalTestimonials; // Counter for total testimonials created

// Monetization parameters (initially disabled)
address public treasury; // Address to receive fees (address(0) = disabled)
uint256 public fee; // Fee amount in wei (0 = free)
uint256 public freeThreshold; // Number of free testimonials before fee kicks in (type(uint256).max = unlimited)

// Maps user address to their received testimonial token IDs
mapping(address => uint256[]) private _receivedTestimonials;
Expand Down Expand Up @@ -51,7 +57,18 @@ contract VouchMe is ERC721URIStorage {
event TestimonialUpdated(address sender, address receiver, uint256 newTokenId);
event ProfileUpdated(address user);

constructor() ERC721("VouchMe Testimonial", "VOUCH") {}
// Monetization events
event FeeUpdated(uint256 oldFee, uint256 newFee);
event TreasuryUpdated(address oldTreasury, address newTreasury);
event FreeThresholdUpdated(uint256 oldThreshold, uint256 newThreshold);
event FeePaid(address indexed payer, uint256 amount);

constructor() ERC721("VouchMe Testimonial", "VOUCH") Ownable(msg.sender) {
// Initialize with monetization disabled
treasury = address(0);
fee = 0;
freeThreshold = type(uint256).max; // Effectively unlimited free testimonials
}

/**
* @dev Creates a testimonial NFT based on a signed message
Expand All @@ -68,7 +85,7 @@ contract VouchMe is ERC721URIStorage {
string calldata giverName,
string calldata profileUrl,
bytes calldata signature
) external returns (uint256) {
) external payable returns (uint256) {
// Hash the message that was signed
bytes32 messageHash = keccak256(
abi.encodePacked(
Expand All @@ -94,6 +111,30 @@ contract VouchMe is ERC721URIStorage {
// Remove the existing testimonial
_removeTestimonialFromList(existingTokenId, senderAddress, msg.sender);
}

// Fee logic: only charge if monetization is enabled and user exceeds free threshold
// This check happens BEFORE minting to ensure payment is received
uint256 currentCount = _receivedTestimonials[msg.sender].length;
uint256 requiredFee = _calculateRequiredFee(currentCount);
if (requiredFee > 0) {
require(msg.value >= requiredFee, "Insufficient fee payment");

// Transfer fee to treasury
(bool sent, ) = treasury.call{value: requiredFee}("");
require(sent, "Fee transfer failed");
emit FeePaid(msg.sender, requiredFee);

// Refund excess payment
uint256 excess = msg.value - requiredFee;
if (excess > 0) {
(bool refunded, ) = msg.sender.call{value: excess}("");
require(refunded, "Refund failed");
}
} else if (msg.value > 0) {
// Refund any payment when no fee is required
(bool refunded, ) = msg.sender.call{value: msg.value}("");
require(refunded, "Refund failed");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

uint256 newTokenId = ++_tokenIdTracker; // Manually increment token ID

Expand Down Expand Up @@ -312,4 +353,89 @@ contract VouchMe is ERC721URIStorage {

emit TestimonialDeleted(tokenId, msg.sender);
}

// ============================================
// MONETIZATION - ADMIN FUNCTIONS
// ============================================

/**
* @dev Sets the fee amount for testimonials after free threshold
* @param _fee The fee amount in wei (0 to disable fees)
*/
function setFee(uint256 _fee) external onlyOwner {
// If setting a non-zero fee, treasury must be set first
require(_fee == 0 || treasury != address(0), "Set treasury before enabling fees");

uint256 oldFee = fee;
fee = _fee;
emit FeeUpdated(oldFee, _fee);
}

/**
* @dev Sets the treasury address to receive fees
* @param _treasury The treasury address
*/
function setTreasury(address _treasury) external onlyOwner {
require(_treasury != address(0), "Treasury cannot be zero address");

address oldTreasury = treasury;
treasury = _treasury;
emit TreasuryUpdated(oldTreasury, _treasury);
}
Comment thread
KanishkSogani marked this conversation as resolved.

/**
* @dev Sets the number of free testimonials before fees apply
* @param _threshold The threshold count (use type(uint256).max for unlimited)
*/
function setFreeThreshold(uint256 _threshold) external onlyOwner {
uint256 oldThreshold = freeThreshold;
freeThreshold = _threshold;
emit FreeThresholdUpdated(oldThreshold, _threshold);
}

// ============================================
// MONETIZATION - VIEW FUNCTIONS
// ============================================

/**
* @dev Returns the number of remaining free testimonials for a user
* @param user The address to check
* @return remaining The number of free testimonials remaining (0 if exceeded)
*/
function getRemainingFreeTestimonials(address user) external view returns (uint256 remaining) {
uint256 currentCount = _receivedTestimonials[user].length;
if (currentCount >= freeThreshold) {
return 0;
}
return freeThreshold - currentCount;
}

/**
* @dev Returns the fee required for a user to add their next testimonial
* @param user The address to check
* @return requiredFee The fee amount in wei (0 if free)
*/
function getRequiredFee(address user) external view returns (uint256 requiredFee) {
uint256 currentCount = _receivedTestimonials[user].length;
return _calculateRequiredFee(currentCount);
}

/**
* @dev Internal function to calculate the required fee based on current testimonial count
* @param currentCount The user's current testimonial count
* @return The fee amount in wei (0 if free or monetization disabled)
*/
function _calculateRequiredFee(uint256 currentCount) internal view returns (uint256) {
// Monetization is disabled if fee is 0 or treasury is not set
if (fee == 0 || treasury == address(0)) {
return 0;
}

// No fee required if under the free threshold
if (currentCount < freeThreshold) {
return 0;
}

return fee;
}
}
Loading
Loading