Skip to content

Commit ec61106

Browse files
committed
coderabbit followup
1 parent 24264c7 commit ec61106

2 files changed

Lines changed: 99 additions & 19 deletions

File tree

contracts/src/VouchMe.sol

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ pragma solidity ^0.8.20;
33

44
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
55
import "@openzeppelin/contracts/access/Ownable.sol";
6+
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
67
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
78
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
89
import "@openzeppelin/contracts/utils/Strings.sol";
910

10-
contract VouchMe is ERC721URIStorage, Ownable {
11+
contract VouchMe is ERC721URIStorage, Ownable, ReentrancyGuard {
1112
using ECDSA for bytes32;
1213
using Strings for uint256;
1314

@@ -85,7 +86,7 @@ contract VouchMe is ERC721URIStorage, Ownable {
8586
string calldata giverName,
8687
string calldata profileUrl,
8788
bytes calldata signature
88-
) external payable returns (uint256) {
89+
) external payable nonReentrant returns (uint256) {
8990
// Hash the message that was signed
9091
bytes32 messageHash = keccak256(
9192
abi.encodePacked(
@@ -112,28 +113,13 @@ contract VouchMe is ERC721URIStorage, Ownable {
112113
_removeTestimonialFromList(existingTokenId, senderAddress, msg.sender);
113114
}
114115

115-
// Fee logic: only charge if monetization is enabled and user exceeds free threshold
116-
// This check happens BEFORE minting to ensure payment is received
116+
// Determine fee requirement before modifying testimonial state
117117
uint256 currentCount = _receivedTestimonials[msg.sender].length;
118118
uint256 requiredFee = _calculateRequiredFee(currentCount);
119119
if (requiredFee > 0) {
120120
require(msg.value >= requiredFee, "Insufficient fee payment");
121-
122-
// Transfer fee to treasury
123-
(bool sent, ) = treasury.call{value: requiredFee}("");
124-
require(sent, "Fee transfer failed");
125-
emit FeePaid(msg.sender, requiredFee);
126-
127-
// Refund excess payment
128-
uint256 excess = msg.value - requiredFee;
129-
if (excess > 0) {
130-
(bool refunded, ) = msg.sender.call{value: excess}("");
131-
require(refunded, "Refund failed");
132-
}
133121
} else if (msg.value > 0) {
134-
// Refund any payment when no fee is required
135-
(bool refunded, ) = msg.sender.call{value: msg.value}("");
136-
require(refunded, "Refund failed");
122+
// No fee is required, so any payment should be refunded later
137123
}
138124

139125
uint256 newTokenId = ++_tokenIdTracker; // Manually increment token ID
@@ -175,6 +161,22 @@ contract VouchMe is ERC721URIStorage, Ownable {
175161
if (existingTokenId != 0) {
176162
emit TestimonialUpdated(senderAddress, msg.sender, newTokenId);
177163
}
164+
165+
// Interactions: transfer fee/refund only after all state changes and events above.
166+
if (requiredFee > 0) {
167+
(bool sent, ) = treasury.call{value: requiredFee}("");
168+
require(sent, "Fee transfer failed");
169+
emit FeePaid(msg.sender, requiredFee);
170+
171+
uint256 excess = msg.value - requiredFee;
172+
if (excess > 0) {
173+
(bool refunded, ) = msg.sender.call{value: excess}("");
174+
require(refunded, "Refund failed");
175+
}
176+
} else if (msg.value > 0) {
177+
(bool refunded, ) = msg.sender.call{value: msg.value}("");
178+
require(refunded, "Refund failed");
179+
}
178180

179181
return newTokenId;
180182
}
@@ -419,6 +421,23 @@ contract VouchMe is ERC721URIStorage, Ownable {
419421
uint256 currentCount = _receivedTestimonials[user].length;
420422
return _calculateRequiredFee(currentCount);
421423
}
424+
425+
/**
426+
* @dev Returns the fee required for createTestimonial, accounting for replacement.
427+
* @param sender The testimonial sender address
428+
* @param receiver The testimonial receiver address (msg.sender in createTestimonial)
429+
* @return requiredFee The fee amount in wei after replacement adjustment
430+
*/
431+
function getRequiredFeeForCreate(address sender, address receiver) external view returns (uint256 requiredFee) {
432+
uint256 currentCount = _receivedTestimonials[receiver].length;
433+
434+
// createTestimonial removes an existing sender->receiver testimonial before fee calculation
435+
if (_testimonial[sender][receiver] != 0 && currentCount > 0) {
436+
currentCount -= 1;
437+
}
438+
439+
return _calculateRequiredFee(currentCount);
440+
}
422441

423442
/**
424443
* @dev Internal function to calculate the required fee based on current testimonial count

contracts/test/VouchMeMonetization.t.sol

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@ pragma solidity ^0.8.20;
44
import "forge-std/Test.sol";
55
import "../src/VouchMe.sol";
66

7+
contract RejectingContract {
8+
function submitTestimonial(
9+
VouchMe target,
10+
address senderAddress,
11+
string calldata content,
12+
string calldata giverName,
13+
string calldata profileUrl,
14+
bytes calldata signature
15+
) external payable {
16+
target.createTestimonial{value: msg.value}(
17+
senderAddress,
18+
content,
19+
giverName,
20+
profileUrl,
21+
signature
22+
);
23+
}
24+
}
25+
726
contract VouchMeMonetizationTest is Test {
827
VouchMe public vouchMe;
928

@@ -355,6 +374,48 @@ contract VouchMeMonetizationTest is Test {
355374
createTestimonial(charlie, alice, "Test 2");
356375
assertEq(vouchMe.getTestimonialCount(alice), 2);
357376
}
377+
378+
function testFeeTransferFailsWithRejectingTreasury() public {
379+
RejectingContract rejectingTreasury = new RejectingContract();
380+
381+
vouchMe.setTreasury(address(rejectingTreasury));
382+
vouchMe.setFreeThreshold(0);
383+
vouchMe.setFee(0.01 ether);
384+
385+
bytes memory signature = createValidSignature(bob, alice, CONTENT, GIVER_NAME, PROFILE_URL);
386+
387+
vm.prank(alice);
388+
vm.expectRevert("Fee transfer failed");
389+
vouchMe.createTestimonial{value: 0.01 ether}(bob, CONTENT, GIVER_NAME, PROFILE_URL, signature);
390+
}
391+
392+
function testRefundFailsWithRejectingReceiver() public {
393+
RejectingContract rejectingReceiver = new RejectingContract();
394+
395+
vouchMe.setTreasury(treasury);
396+
vouchMe.setFreeThreshold(0);
397+
vouchMe.setFee(0.01 ether);
398+
399+
vm.deal(address(rejectingReceiver), 1 ether);
400+
401+
bytes memory signature = createValidSignature(
402+
bob,
403+
address(rejectingReceiver),
404+
CONTENT,
405+
GIVER_NAME,
406+
PROFILE_URL
407+
);
408+
409+
vm.expectRevert("Refund failed");
410+
rejectingReceiver.submitTestimonial{value: 0.02 ether}(
411+
vouchMe,
412+
bob,
413+
CONTENT,
414+
GIVER_NAME,
415+
PROFILE_URL,
416+
signature
417+
);
418+
}
358419

359420
function testOwnershipTransfer() public {
360421
address newOwner = makeAddr("newOwner");

0 commit comments

Comments
 (0)