diff --git a/fc-community-contracts/src/FactchainCommunity.sol b/fc-community-contracts/src/FactchainCommunity.sol index 89bb5c18..a46b7fc6 100644 --- a/fc-community-contracts/src/FactchainCommunity.sol +++ b/fc-community-contracts/src/FactchainCommunity.sol @@ -71,8 +71,8 @@ interface IFactchainCommunity is IFactchainCommunityEvents { contract FactchainCommunity is Ownable, IFactchainCommunity { uint8 internal constant POST_URL_MAX_LENGTH = 160; uint16 internal constant CONTENT_MAX_LENGTH = 500; - uint16 internal constant MINIMUM_STAKE_PER_RATING = 10_000; - uint32 internal constant MINIMUM_STAKE_PER_NOTE = 100_000; + uint64 public minimumStakePerNote = 1_000_000_000_000_000; + uint64 public minimumStakePerRating = 100_000_000_000_000; /// @notice Mapping of note identifier (postUrl + creatorAddress) to Note object mapping(string => mapping(address => Note)) public communityNotes; @@ -138,21 +138,21 @@ contract FactchainCommunity is Ownable, IFactchainCommunity { if (finalRating >= 3) { uint96 reward = uint96(finalRating - 2) * 10; // This will revert if contract current balance - // (address(this).balance) < MINIMUM_STAKE_PER_NOTE + reward - (bool result,) = payable(_creator).call{value: MINIMUM_STAKE_PER_NOTE + reward}(""); + // (address(this).balance) < minimumStakePerNote + reward + (bool result,) = payable(_creator).call{value: minimumStakePerNote + reward}(""); if (!result) revert FailedToReward(); userStats[_creator].ethRewarded += reward; - emit CreatorRewarded({postUrl: _postUrl, creator: _creator, reward: reward, stake: MINIMUM_STAKE_PER_NOTE}); + emit CreatorRewarded({postUrl: _postUrl, creator: _creator, reward: reward, stake: minimumStakePerNote}); } else if (finalRating < 2) { uint96 slash = finalRating * 10; // This will revert if contract current balance - // (address(this).balance) < MINIMUM_STAKE_PER_NOTE - slash - (bool result,) = payable(_creator).call{value: MINIMUM_STAKE_PER_NOTE - slash}(""); + // (address(this).balance) < minimumStakePerNote - slash + (bool result,) = payable(_creator).call{value: minimumStakePerNote - slash}(""); if (!result) revert FailedToSlash(); userStats[_creator].ethSlashed += slash; - emit CreatorSlashed({postUrl: _postUrl, creator: _creator, slash: slash, stake: MINIMUM_STAKE_PER_NOTE}); + emit CreatorSlashed({postUrl: _postUrl, creator: _creator, slash: slash, stake: minimumStakePerNote}); } } @@ -164,8 +164,8 @@ contract FactchainCommunity is Ownable, IFactchainCommunity { if (delta < 2) { uint96 reward = 2 - delta; // This will revert if contract current balance - // (address(this).balance) < MINIMUM_STAKE_PER_RATING + reward - (bool result,) = payable(rater).call{value: MINIMUM_STAKE_PER_RATING + reward}(""); + // (address(this).balance) < minimumStakePerRating + reward + (bool result,) = payable(rater).call{value: minimumStakePerRating + reward}(""); if (!result) revert FailedToReward(); userStats[rater].ethRewarded += reward; @@ -174,13 +174,13 @@ contract FactchainCommunity is Ownable, IFactchainCommunity { creator: _creator, rater: rater, reward: reward, - stake: MINIMUM_STAKE_PER_RATING + stake: minimumStakePerRating }); } else if (delta > 2) { uint96 slash = delta - 2; // This will revert if contract current balance - // (address(this).balance) < MINIMUM_STAKE_PER_RATING - slash - (bool result,) = payable(rater).call{value: MINIMUM_STAKE_PER_RATING - slash}(""); + // (address(this).balance) < minimumStakePerRating - slash + (bool result,) = payable(rater).call{value: minimumStakePerRating - slash}(""); if (!result) revert FailedToSlash(); userStats[rater].ethSlashed += slash; @@ -189,7 +189,7 @@ contract FactchainCommunity is Ownable, IFactchainCommunity { creator: _creator, rater: rater, slash: slash, - stake: MINIMUM_STAKE_PER_RATING + stake: minimumStakePerRating }); } } @@ -201,7 +201,7 @@ contract FactchainCommunity is Ownable, IFactchainCommunity { /// @notice Create a new note function createNote(string memory _postUrl, string memory _content) external payable { - if (msg.value != MINIMUM_STAKE_PER_NOTE) revert InsufficientStake(); + if (msg.value != minimumStakePerNote) revert InsufficientStake(); if (!isPostUrlValid(bytes(_postUrl))) revert PostUrlInvalid(); if (!isContentValid(bytes(_content))) revert ContentInvalid(); if (noteExists(_postUrl, msg.sender)) revert NoteAlreadyExists(); @@ -210,12 +210,12 @@ contract FactchainCommunity is Ownable, IFactchainCommunity { Note({postUrl: _postUrl, content: _content, creator: msg.sender, finalRating: 0}); userStats[msg.sender].numberNotes += 1; - emit NoteCreated({postUrl: _postUrl, creator: msg.sender, stake: MINIMUM_STAKE_PER_NOTE}); + emit NoteCreated({postUrl: _postUrl, creator: msg.sender, stake: minimumStakePerNote}); } /// @notice Rate an existing note function rateNote(string memory _postUrl, address _creator, uint8 _rating) external payable { - if (msg.value != MINIMUM_STAKE_PER_RATING) revert InsufficientStake(); + if (msg.value != minimumStakePerRating) revert InsufficientStake(); if (!isRatingValid(_rating)) revert RatingInvalid(); if (_creator == msg.sender) revert CantRateOwnNote(); if (!noteExists(_postUrl, _creator)) revert NoteDoesNotExist(); @@ -231,7 +231,7 @@ contract FactchainCommunity is Ownable, IFactchainCommunity { creator: _creator, rater: msg.sender, rating: _rating, - stake: MINIMUM_STAKE_PER_RATING + stake: minimumStakePerRating }); } @@ -255,4 +255,14 @@ contract FactchainCommunity is Ownable, IFactchainCommunity { rewardOrSlashRaters(_postUrl, _creator); emit NoteFinalised({postUrl: _postUrl, creator: _creator, finalRating: _finalRating}); } + + /// @notice Set minimum staking for note creation + function setMinimumStakePerNote(uint64 _miniumStakePerNote) external onlyOwner { + minimumStakePerNote = _miniumStakePerNote; + } + + /// @notice Set minimum staking for note rating + function setMinimumStakePerRating(uint64 _minimumStakePerRating) external onlyOwner { + minimumStakePerRating = _minimumStakePerRating; + } } diff --git a/fc-community-contracts/src/FactchainSFT.sol b/fc-community-contracts/src/FactchainSFT.sol index 4fbfc717..bd2c4d29 100644 --- a/fc-community-contracts/src/FactchainSFT.sol +++ b/fc-community-contracts/src/FactchainSFT.sol @@ -26,9 +26,9 @@ interface IFactchainSFT is IFactchainSFTEvents { contract FactchainSFT is Ownable, ERC1155URIStorage, IFactchainSFT { address public FACTCHAIN_NFT_CONTRACT; uint256 public constant FACTCHAINERS_MINT_SUPPLY = 42; - uint256 public constant MINT_PRICE = 1_000_000; + uint256 public mintPrice = 1_000_000_000_000_000; - mapping(uint256 => uint256) private _supply; + mapping(uint256 => uint256) public supply; /// @notice Mapping of creators's addresses to NFT mapping(uint256 => address) private _creatorsNFT; @@ -44,11 +44,15 @@ contract FactchainSFT is Ownable, ERC1155URIStorage, IFactchainSFT { emit FactchainNFTContractUpdated(_factchainNFTContract); } + function setMintPrice(uint256 _mintPrice) public onlyOwner { + mintPrice = _mintPrice; + } + function initialMint(address creator, address[] memory raters, string memory ipfsHash, uint256 id) public returns (uint256) { if (msg.sender != FACTCHAIN_NFT_CONTRACT) { revert ReservedToFactchain(); } - _supply[id] = FACTCHAINERS_MINT_SUPPLY; + supply[id] = FACTCHAINERS_MINT_SUPPLY; _creatorsNFT[id] = creator; _setURI(id, ipfsHash); for (uint256 i = 0; i < raters.length; ++i) { @@ -58,12 +62,12 @@ contract FactchainSFT is Ownable, ERC1155URIStorage, IFactchainSFT { } function mint(uint256 id, uint256 value) external payable { - if (msg.value != MINT_PRICE * value) revert BadMintPrice(); + if (msg.value != mintPrice * value) revert BadMintPrice(); if (value <= 0) revert ValueError(); - if (value > _supply[id]) { + if (value > supply[id]) { revert SupplyExhausted(); } - _supply[id] -= value; + supply[id] -= value; address creator = _creatorsNFT[id]; uint256 reward = msg.value / 2; (bool result,) = payable(creator).call{value: reward}(""); diff --git a/fc-community-contracts/src/XCommunityNotes.sol b/fc-community-contracts/src/XCommunityNotes.sol index f0d12504..492b1fe0 100644 --- a/fc-community-contracts/src/XCommunityNotes.sol +++ b/fc-community-contracts/src/XCommunityNotes.sol @@ -35,8 +35,8 @@ interface IXCommunityNotes is IXCommunityNotesEvents { /// @dev contract XCommunityNotes is Ownable, ERC1155, IXCommunityNotes { uint256 public constant MAX_TOKEN_SUPPLY = 42; - uint256 public constant MINT_PRICE = 1_000_000; uint256 public constant SUPPLY_EXHAUSTED = MAX_TOKEN_SUPPLY + 1; + uint256 public mintPrice = 1_000_000_000_000_000; address public backend; mapping(uint256 id => uint256) public supply; @@ -58,6 +58,10 @@ contract XCommunityNotes is Ownable, ERC1155, IXCommunityNotes { emit NewBackend(backend); } + function setMintPrice(uint256 _mintPrice) public onlyOwner { + mintPrice = _mintPrice; + } + function getTokenID(string memory url) public view returns (uint256) { // the token ID is a 9 to 10 digits number calculated from // the url hash by converting to decimal its first 8 hex characters. @@ -71,14 +75,14 @@ contract XCommunityNotes is Ownable, ERC1155, IXCommunityNotes { function mint(uint256 id, uint256 value) public payable { if (value <= 0) revert ValueError(); - if (msg.value != MINT_PRICE * value) revert BadMintPrice(); + if (msg.value != mintPrice * value) revert BadMintPrice(); if (supply[id] == SUPPLY_EXHAUSTED) revert SupplyExhausted(); if (supply[id] == 0) revert UnknownToken(); if (value > supply[id]) { uint256 supplyCache = supply[id]; supply[id] = SUPPLY_EXHAUSTED; _mint(msg.sender, id, supplyCache, ""); - uint256 amount = (value - supplyCache) * MINT_PRICE; + uint256 amount = (value - supplyCache) * mintPrice; // use call rather than transfer // to support Smart Contract Wallets. (bool result,) = payable(msg.sender).call{value: amount}(""); diff --git a/fc-community-contracts/test/FactchainCommunity.t.sol b/fc-community-contracts/test/FactchainCommunity.t.sol index b32b73be..65b8e7f2 100644 --- a/fc-community-contracts/test/FactchainCommunity.t.sol +++ b/fc-community-contracts/test/FactchainCommunity.t.sol @@ -7,8 +7,6 @@ import "../src/FactchainCommunity.sol"; contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { FactchainCommunity public tmCommunity; uint160 lastUintAddress = 0; - uint32 internal constant MINIMUM_STAKE_PER_NOTE = 100_000; - uint16 internal constant MINIMUM_STAKE_PER_RATING = 10_000; function nextAddress() public returns (address) { lastUintAddress += 1; @@ -35,43 +33,47 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_createNote_RevertIf_notEnoughEth() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); hoax(player1); vm.expectRevert(IFactchainCommunity.InsufficientStake.selector); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE - 1}({ + tmCommunity.createNote{value: minimumStakePerNote - 1}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); } function test_createNote_RevertIf_postUrlInvalid() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); hoax(player1); vm.expectRevert(IFactchainCommunity.PostUrlInvalid.selector); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({_postUrl: "", _content: "Something something something"}); + tmCommunity.createNote{value: minimumStakePerNote}({_postUrl: "", _content: "Something something something"}); vm.expectRevert(IFactchainCommunity.PostUrlInvalid.selector); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something-something-something-something-something-something-something-something-something-something-something-something-something-something-something", _content: "Something something something" }); } function test_createNote_RevertIf_contentInvalid() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); hoax(player1); vm.expectRevert(IFactchainCommunity.ContentInvalid.selector); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({_postUrl: "https://twitter.com/something", _content: ""}); + tmCommunity.createNote{value: minimumStakePerNote}({_postUrl: "https://twitter.com/something", _content: ""}); vm.expectRevert(IFactchainCommunity.ContentInvalid.selector); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something something" }); } function test_createNote() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); (uint32 originalNumberNotes,,,) = tmCommunity.userStats(player1); hoax(player1); vm.expectEmit(); - emit NoteCreated("https://twitter.com/something", player1, MINIMUM_STAKE_PER_NOTE); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + emit NoteCreated("https://twitter.com/something", player1, minimumStakePerNote); + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); @@ -80,35 +82,38 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_createNote_RevertIf_alreadyExists() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); vm.prank(player1); vm.expectRevert(IFactchainCommunity.NoteAlreadyExists.selector); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); } function test_rateNote_RevertIf_ratingInvalid() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); + uint256 minimumStakePerRating = tmCommunity.minimumStakePerRating(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); hoax(rater1); vm.expectRevert(IFactchainCommunity.RatingInvalid.selector); - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 0 }); vm.expectRevert(IFactchainCommunity.RatingInvalid.selector); - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 6 @@ -116,14 +121,16 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_rateNot_RevertIf_notEnoughStake() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); + uint256 minimumStakePerRating = tmCommunity.minimumStakePerRating(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); hoax(player2); vm.expectRevert(IFactchainCommunity.InsufficientStake.selector); - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING - 1}({ + tmCommunity.rateNote{value: minimumStakePerRating - 1}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 4 @@ -131,15 +138,17 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_rateNote_RevertIf_creatorIsRater() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); + uint256 minimumStakePerRating = tmCommunity.minimumStakePerRating(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); vm.prank(player1); vm.expectRevert(IFactchainCommunity.CantRateOwnNote.selector); - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 1 @@ -147,9 +156,10 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_rateNote_RevertIf_noteDoesNotExist() public { + uint256 minimumStakePerRating = tmCommunity.minimumStakePerRating(); hoax(player2); vm.expectRevert(IFactchainCommunity.NoteDoesNotExist.selector); - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 1 @@ -157,8 +167,10 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_rateNote_RevertIf_noteAlreadyFinalised() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); + uint256 minimumStakePerRating = tmCommunity.minimumStakePerRating(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); @@ -168,7 +180,7 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { hoax(rater1); vm.expectRevert(IFactchainCommunity.NoteAlreadyFinalised.selector); - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 2 @@ -176,17 +188,19 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_rateNote() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); + uint256 minimumStakePerRating = tmCommunity.minimumStakePerRating(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); (, uint32 originalNumberRatings,,) = tmCommunity.userStats(rater1); vm.expectEmit(); - emit NoteRated("https://twitter.com/something", player1, rater1, 1, MINIMUM_STAKE_PER_RATING); + emit NoteRated("https://twitter.com/something", player1, rater1, 1, minimumStakePerRating); hoax(rater1); - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 1 @@ -196,14 +210,16 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_rateNote_RevertIf_ratingAlreadyExist() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); + uint256 minimumStakePerRating = tmCommunity.minimumStakePerRating(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); hoax(rater1); - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 1 @@ -211,7 +227,7 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { hoax(rater1); vm.expectRevert(IFactchainCommunity.RatingAlreadyExists.selector); - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 2 @@ -219,8 +235,9 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_finaliseNote_RevertIf_notOwner() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); @@ -231,8 +248,9 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_finaliseNote_RevertIf_ratingInvalid() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); @@ -251,8 +269,9 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_finaliseNote() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); @@ -264,8 +283,9 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_finaliseNote_RevertIf_noteAlreadyFinalised() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); @@ -278,11 +298,13 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_rewardAndSlashRaters() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); + uint256 minimumStakePerRating = tmCommunity.minimumStakePerRating(); (,, uint96 rater1OldRewards,) = tmCommunity.userStats(rater1); (,,, uint96 rater2OldSlash) = tmCommunity.userStats(rater2); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); @@ -291,7 +313,7 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { uint256 rater1OriginalBalance = rater1.balance; // _rating perfectly matches final rating (1) // rater1 should be reward of 2 WEI - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 1 @@ -301,16 +323,16 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { uint256 rater2OriginalBalance = rater2.balance; // wrong rating with maximal delta // rater2 should be slashed of 2 WEI - tmCommunity.rateNote{value: MINIMUM_STAKE_PER_RATING}({ + tmCommunity.rateNote{value: minimumStakePerRating}({ _postUrl: "https://twitter.com/something", _creator: player1, _rating: 5 }); vm.expectEmit(); - emit RaterRewarded("https://twitter.com/something", player1, rater1, 2, MINIMUM_STAKE_PER_RATING); + emit RaterRewarded("https://twitter.com/something", player1, rater1, 2, minimumStakePerRating); vm.expectEmit(); - emit RaterSlashed("https://twitter.com/something", player1, rater2, 2, MINIMUM_STAKE_PER_RATING); + emit RaterSlashed("https://twitter.com/something", player1, rater2, 2, minimumStakePerRating); vm.prank(owner); tmCommunity.finaliseNote({_postUrl: "https://twitter.com/something", _creator: player1, _finalRating: 1}); @@ -323,17 +345,18 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_rewardCreator() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); (,, uint96 oldRewards,) = tmCommunity.userStats(player1); hoax(player1); uint256 player1OriginalBalance = player1.balance; - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); vm.expectEmit(); - emit CreatorRewarded("https://twitter.com/something", player1, 30, MINIMUM_STAKE_PER_NOTE); + emit CreatorRewarded("https://twitter.com/something", player1, 30, minimumStakePerNote); vm.prank(owner); tmCommunity.finaliseNote({_postUrl: "https://twitter.com/something", _creator: player1, _finalRating: 5}); @@ -343,16 +366,17 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_slashCreator() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); (,,, uint96 oldSlash) = tmCommunity.userStats(player1); hoax(player1); uint256 player1OriginalBalance = player1.balance; - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: "https://twitter.com/something", _content: "Something something something" }); vm.expectEmit(); - emit CreatorSlashed("https://twitter.com/something", player1, 10, MINIMUM_STAKE_PER_NOTE); + emit CreatorSlashed("https://twitter.com/something", player1, 10, minimumStakePerNote); vm.prank(owner); tmCommunity.finaliseNote({_postUrl: "https://twitter.com/something", _creator: player1, _finalRating: 1}); @@ -362,11 +386,12 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { } function test_RevertIf_insuficientFundForReward() public { + uint256 minimumStakePerNote = tmCommunity.minimumStakePerNote(); uint256 index = 0; while (address(tmCommunity).balance > 30) { string memory postUrl1 = string.concat("https://twitter.com/something", vm.toString(index)); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: postUrl1, _content: "Something something something" }); @@ -378,7 +403,7 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { string memory postUrl2 = string.concat("https://twitter.com/something", vm.toString(index)); hoax(player1); - tmCommunity.createNote{value: MINIMUM_STAKE_PER_NOTE}({ + tmCommunity.createNote{value: minimumStakePerNote}({ _postUrl: postUrl2, _content: "Something something something" }); @@ -387,4 +412,28 @@ contract FactchainCommunityTest is Test, IFactchainCommunity, IOwnable { vm.prank(owner); tmCommunity.finaliseNote({_postUrl: postUrl2, _creator: player1, _finalRating: 5}); } + + function test_setMinimumStakePerNote() public { + vm.prank(owner); + tmCommunity.setMinimumStakePerNote(123); + assert(tmCommunity.minimumStakePerNote() == 123); + } + + function test_setMinimumStakePerNote_RevertIf_notOwner() public { + vm.expectRevert(IOwnable.NotOwner.selector); + vm.prank(player1); + tmCommunity.setMinimumStakePerNote(123); + } + + function test_setMinimumStakePerRating() public { + vm.prank(owner); + tmCommunity.setMinimumStakePerRating(123); + assert(tmCommunity.minimumStakePerRating() == 123); + } + + function test_setMinimumStakePerRating_RevertIf_notOwner() public { + vm.expectRevert(IOwnable.NotOwner.selector); + vm.prank(player1); + tmCommunity.setMinimumStakePerRating(123); + } } diff --git a/fc-community-contracts/test/FactchainSFT.t.sol b/fc-community-contracts/test/FactchainSFT.t.sol index b7d160b9..e1542028 100644 --- a/fc-community-contracts/test/FactchainSFT.t.sol +++ b/fc-community-contracts/test/FactchainSFT.t.sol @@ -2,14 +2,12 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; - +import {IOwnable} from "../src/utils/Ownable.sol"; import {FactchainSFT, IFactchainSFT} from "../src/FactchainSFT.sol"; -contract FactchainSFTTest is Test, IFactchainSFT { +contract FactchainSFTTest is Test, IFactchainSFT, IOwnable { FactchainSFT collection; - uint256 public constant MINT_PRICE = 1_000_000; - address public owner = address(1); address public factchainNFT = address(2); address public rater1 = address(3); @@ -39,13 +37,14 @@ contract FactchainSFTTest is Test, IFactchainSFT { } function testOpenMint() public { + uint256 mintPrice = collection.mintPrice(); vm.startPrank(owner); collection.setFactchainNFTContract(factchainNFT); vm.startPrank(nftBuyer); vm.expectRevert(IFactchainSFT.BadMintPrice.selector); - collection.mint{value: MINT_PRICE + 1}(42, 1); + collection.mint{value: mintPrice + 1}(42, 1); vm.expectRevert(IFactchainSFT.SupplyExhausted.selector); - collection.mint{value: MINT_PRICE * 43}(42, 43); + collection.mint{value: mintPrice * 43}(42, 43); // first valid mint should be initiated by the factchain main contract // to rewards all raters of the note, raise SupplyExhaused otherwise vm.startPrank(factchainNFT); @@ -53,22 +52,34 @@ contract FactchainSFTTest is Test, IFactchainSFT { uint256 creatorBalanceBefore = address(creator).balance; uint256 factchainSFTBalanceBefore = address(collection).balance; vm.expectEmit(); - emit FactchainBuildersRewarded(MINT_PRICE / 2); - emit CreatorRewarded(creator, MINT_PRICE / 2); - collection.mint{value: MINT_PRICE}(42, 1); + emit FactchainBuildersRewarded(mintPrice / 2); + emit CreatorRewarded(creator, mintPrice / 2); + collection.mint{value: mintPrice}(42, 1); assertEq( address(creator).balance, - creatorBalanceBefore + MINT_PRICE / 2, + creatorBalanceBefore + mintPrice / 2, "Contract Balance should have been increased by half MintPrice" ); assertEq( address(collection).balance, - factchainSFTBalanceBefore + MINT_PRICE / 2, + factchainSFTBalanceBefore + mintPrice / 2, "Factchain builders should have been rewarded too by half MintPrice" ); // should exhaust the supply - collection.mint{value: MINT_PRICE * 41}(42, 41); + collection.mint{value: mintPrice * 41}(42, 41); vm.expectRevert(IFactchainSFT.SupplyExhausted.selector); - collection.mint{value: MINT_PRICE}(42, 1); + collection.mint{value: mintPrice}(42, 1); + } + + function test_setMintPrice() public { + vm.prank(owner); + collection.setMintPrice(123); + assert(collection.mintPrice() == 123); + } + + function test_setMintPrice_RevertIf_notOwner() public { + vm.expectRevert(IOwnable.NotOwner.selector); + vm.prank(nftBuyer); + collection.setMintPrice(123); } } diff --git a/fc-community-contracts/test/XCommunityNotes.t.sol b/fc-community-contracts/test/XCommunityNotes.t.sol index 8a42d5fd..d3a01d13 100644 --- a/fc-community-contracts/test/XCommunityNotes.t.sol +++ b/fc-community-contracts/test/XCommunityNotes.t.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; import {XCommunityNotes, IXCommunityNotes} from "../src/XCommunityNotes.sol"; +import {IOwnable} from "../src/utils/Ownable.sol"; -contract XCommunityNotesTest is Test, IXCommunityNotes { +contract XCommunityNotesTest is Test, IXCommunityNotes, IOwnable { XCommunityNotes collection; uint256 public constant MAX_TOKEN_SUPPLY = 42; - uint256 public constant MINT_PRICE = 1_000_000; uint256 public constant SUPPLY_EXHAUSTED = MAX_TOKEN_SUPPLY + 1; address public owner = address(1); address public recipient = address(2); @@ -26,23 +26,25 @@ contract XCommunityNotesTest is Test, IXCommunityNotes { } function testMintUnkownToken() public { + uint256 mintPrice = collection.mintPrice(); // Should not be able to mint unknown tokens // To mint a token for the first time // provide a token ID signed by the backend vm.expectRevert(IXCommunityNotes.UnknownToken.selector); - collection.mint{value: MINT_PRICE}(123, 1); + collection.mint{value: mintPrice}(123, 1); } function testGetTokenID() public { + uint256 mintPrice = collection.mintPrice(); vm.prank(recipient); - collection.mint{value: MINT_PRICE * 3}( + collection.mint{value: mintPrice * 3}( 424273286, 3, hex"84fac280d097e9c99d8522dd6adb8fcb46d9c1d0798d309b3abfd511d24e43b8", hex"4d27fe7200c3628938070a426865303178724b6165088e1de049e4c6a94ba0f73b73ea6636c85494ae5b516c0f1094f51d751738ab0d383ba254c5ade08a99fb1b" ); vm.prank(recipient); - collection.mint{value: MINT_PRICE * 3}( + collection.mint{value: mintPrice * 3}( 2742163345, 3, hex"a683622ff02d5db7693fc088d7382b73f03bf1656a4cc771f2410e30d521bb87", // keccak(bytes("2742163345", "utf-8")).hex() @@ -58,10 +60,11 @@ contract XCommunityNotesTest is Test, IXCommunityNotes { } function testMintBalanceUpdate() public { + uint256 mintPrice = collection.mintPrice(); // Mint 3 tokens of ID 123 for the first time // provide signature of "123" vm.prank(recipient); - collection.mint{value: MINT_PRICE * 3}( + collection.mint{value: mintPrice * 3}( 123, 3, // keccak(bytes("123", "utf-8")).hex() @@ -74,7 +77,7 @@ contract XCommunityNotesTest is Test, IXCommunityNotes { assertEq(balanceAfterMint, 3, "Recipient should have 3 tokens after minting"); // Check the contract balance assertEq( - address(collection).balance, MINT_PRICE * 3, "Contract Balance should have been increased by MintPrice" + address(collection).balance, mintPrice * 3, "Contract Balance should have been increased by MintPrice" ); uint256 balanceBefore = address(recipient).balance; @@ -84,16 +87,17 @@ contract XCommunityNotesTest is Test, IXCommunityNotes { // see worstRandEver contract function. // in this test: we've already minted 3 quantiy of token 123 in the above mint call // remaining supply *should be* = 23 - emit Refunded(recipient, (MAX_TOKEN_SUPPLY - 23) * MINT_PRICE); - collection.mint{value: MINT_PRICE * MAX_TOKEN_SUPPLY}(123, MAX_TOKEN_SUPPLY); - assertEq(address(recipient).balance, balanceBefore - (23 * MINT_PRICE)); + emit Refunded(recipient, (MAX_TOKEN_SUPPLY - 23) * mintPrice); + collection.mint{value: mintPrice * MAX_TOKEN_SUPPLY}(123, MAX_TOKEN_SUPPLY); + assertEq(address(recipient).balance, balanceBefore - (23 * mintPrice)); } function testMintSupplyDecrease() public { + uint256 mintPrice = collection.mintPrice(); // First Mint of token 123 // needs token hash and backend signature vm.prank(recipient); - collection.mint{value: MINT_PRICE * 3}( + collection.mint{value: mintPrice * 3}( 123, 3, // keccak(bytes("123", "utf-8")).hex() @@ -106,17 +110,18 @@ contract XCommunityNotesTest is Test, IXCommunityNotes { vm.startPrank(other); // second mint, token already created // mint only requires token id and quantity - collection.mint{value: MINT_PRICE * 3}(123, 3); + collection.mint{value: mintPrice * 3}(123, 3); uint256 supplyBeforeMint = collection.supply(123); - collection.mint{value: MINT_PRICE}(123, 1); + collection.mint{value: mintPrice}(123, 1); assertEq(collection.supply(123), supplyBeforeMint - 1, "Supply should have been decreased!"); } function testMintSupplyExhausted() public { + uint256 mintPrice = collection.mintPrice(); // First Mint of token 123 // needs token hash and backend signature vm.prank(recipient); - collection.mint{value: MINT_PRICE * MAX_TOKEN_SUPPLY}( + collection.mint{value: mintPrice * MAX_TOKEN_SUPPLY}( 123, MAX_TOKEN_SUPPLY, // keccak(bytes("123", "utf-8")).hex() @@ -126,10 +131,11 @@ contract XCommunityNotesTest is Test, IXCommunityNotes { ); vm.expectRevert(IXCommunityNotes.SupplyExhausted.selector); - collection.mint{value: MINT_PRICE}(123, 1); + collection.mint{value: mintPrice}(123, 1); } function testMintNotAllowed() public { + uint256 mintPrice = collection.mintPrice(); vm.prank(owner); // set new backend // following signatures should be wrong! @@ -138,7 +144,7 @@ contract XCommunityNotesTest is Test, IXCommunityNotes { // needs token hash and backend signature vm.prank(recipient); vm.expectRevert(IXCommunityNotes.NotAllowed.selector); - collection.mint{value: MINT_PRICE}( + collection.mint{value: mintPrice}( 123, 1, // keccak(bytes("123", "utf-8")).hex() @@ -171,44 +177,15 @@ contract XCommunityNotesTest is Test, IXCommunityNotes { collection.setBackend(backend); } - // function testMintWithAdjustedValue() public { - // vm.startPrank(owner); - // collection.setTokenSupply(42, 24); - // vm.startPrank(other); - // vm.expectEmit(); - // emit MintWithAdjustedValue(42, 24); - // // value should be adjusted to 24 - // collection.mint{value: MINT_PRICE * 26}( - // 42, - // 26, - // // keccak(bytes("123", "utf-8")).hex() - // hex"64e604787cbf194841e7b68d7cd28786f6c9a0a3ab9f8b0a0e87cb4387ab0107", - // // sign(bytes(b'\x19Ethereum Signed Message:\n32' + keccak(bytes("123","utf-8")).hex()) - // // SIGNED WITH BACKEND PRIVATE KEY - // hex"fccfdfb298d4f8ec3e534fe76a074f6aa30c0237a20f6375cf1315f370ab816b5133193ecd9ecc89b3359d8c5cc45660fcb2c32e05712a3a4a00c6c012556a961b" - // ); - // } - - // function testMintWithProvidedValue() public { - // vm.startPrank(owner); - // collection.setTokenSupply(42, 24); - // vm.startPrank(other); - // vm.expectEmit(); - - // emit MintWithProvidedValue(42, 12); - // collection.mint{value: MINT_PRICE * 12}( - // 42, - // 12, - // // keccak(bytes("123", "utf-8")).hex() - // hex"64e604787cbf194841e7b68d7cd28786f6c9a0a3ab9f8b0a0e87cb4387ab0107", - // // sign(bytes(b'\x19Ethereum Signed Message:\n32' + keccak(bytes("123","utf-8")).hex()) - // // SIGNED WITH BACKEND PRIVATE KEY - // hex"fccfdfb298d4f8ec3e534fe76a074f6aa30c0237a20f6375cf1315f370ab816b5133193ecd9ecc89b3359d8c5cc45660fcb2c32e05712a3a4a00c6c012556a961b" - // ); - // assertEq(collection.supply(42), 12, "Supply should have been decreased by 12!"); - // vm.expectEmit(); - // emit MintWithProvidedValue(42, 12); - // collection.mint{value: MINT_PRICE * 12}(42, 12); - // assertEq(collection.supply(42), SUPPLY_EXHAUSTED, "Supply should be exhausted!"); - // } + function test_setMintPrice() public { + vm.prank(owner); + collection.setMintPrice(123); + assert(collection.mintPrice() == 123); + } + + function test_setMintPrice_RevertIf_notOwner() public { + vm.expectRevert(IOwnable.NotOwner.selector); + vm.prank(recipient); + collection.setMintPrice(123); + } }