Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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))");
}
}
151 changes: 148 additions & 3 deletions contracts/src/VouchMe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.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, ReentrancyGuard {
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 +58,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 +86,7 @@ contract VouchMe is ERC721URIStorage {
string calldata giverName,
string calldata profileUrl,
bytes calldata signature
) external returns (uint256) {
) external payable nonReentrant returns (uint256) {
// Hash the message that was signed
bytes32 messageHash = keccak256(
abi.encodePacked(
Expand All @@ -94,6 +112,15 @@ contract VouchMe is ERC721URIStorage {
// Remove the existing testimonial
_removeTestimonialFromList(existingTokenId, senderAddress, msg.sender);
}

// Determine fee requirement before modifying testimonial state
uint256 currentCount = _receivedTestimonials[msg.sender].length;
uint256 requiredFee = _calculateRequiredFee(currentCount);
if (requiredFee > 0) {
require(msg.value >= requiredFee, "Insufficient fee payment");
} else if (msg.value > 0) {
// No fee is required, so any payment should be refunded later
}

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

Expand Down Expand Up @@ -134,6 +161,22 @@ contract VouchMe is ERC721URIStorage {
if (existingTokenId != 0) {
emit TestimonialUpdated(senderAddress, msg.sender, newTokenId);
}

// Interactions: transfer fee/refund only after all state changes and events above.
if (requiredFee > 0) {
(bool sent, ) = treasury.call{value: requiredFee}("");
require(sent, "Fee transfer failed");
emit FeePaid(msg.sender, requiredFee);

uint256 excess = msg.value - requiredFee;
if (excess > 0) {
(bool refunded, ) = msg.sender.call{value: excess}("");
require(refunded, "Refund failed");
}
} else if (msg.value > 0) {
(bool refunded, ) = msg.sender.call{value: msg.value}("");
require(refunded, "Refund failed");
}

return newTokenId;
}
Expand Down Expand Up @@ -312,4 +355,106 @@ 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 Returns the fee required for createTestimonial, accounting for replacement.
* @param sender The testimonial sender address
* @param receiver The testimonial receiver address (msg.sender in createTestimonial)
* @return requiredFee The fee amount in wei after replacement adjustment
*/
function getRequiredFeeForCreate(address sender, address receiver) external view returns (uint256 requiredFee) {
uint256 currentCount = _receivedTestimonials[receiver].length;

// createTestimonial removes an existing sender->receiver testimonial before fee calculation
if (_testimonial[sender][receiver] != 0 && currentCount > 0) {
currentCount -= 1;
}

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