diff --git a/contracts/RewardLogic.sol b/contracts/RewardLogic.sol index d125251..b9d1e89 100644 --- a/contracts/RewardLogic.sol +++ b/contracts/RewardLogic.sol @@ -63,6 +63,9 @@ contract RewardLogic is Ownable, IRewards { bool authorized ); + event ContractAuthorized(address indexed contractAddress, uint256 timestamp); + event ContractAuthorizationRevoked(address indexed contractAddress, uint256 timestamp); + /** * @dev Constructor sets the DCU token and NFT collection addresses * @param _dcuToken Address of the DCU token contract @@ -71,6 +74,31 @@ contract RewardLogic is Ownable, IRewards { constructor(address _dcuToken, address _nftCollection) Ownable(msg.sender) { dcuToken = IDCUToken(_dcuToken); nftCollection = INFTCollection(_nftCollection); + + // Authorize the NFT collection by default + if (_nftCollection != address(0)) { + authorizedContracts[_nftCollection] = true; + emit ContractAuthorized(_nftCollection, block.timestamp); + } + } + + /** + * @dev Authorize a contract to call distributeDCU (only owner) + * @param contractAddress Address of the contract to authorize + */ + function authorizeContract(address contractAddress) external onlyOwner { + require(contractAddress != address(0), "Cannot authorize zero address"); + authorizedContracts[contractAddress] = true; + emit ContractAuthorized(contractAddress, block.timestamp); + } + + /** + * @dev Revoke authorization for a contract (only owner) + * @param contractAddress Address of the contract to revoke + */ + function revokeContractAuthorization(address contractAddress) external onlyOwner { + authorizedContracts[contractAddress] = false; + emit ContractAuthorizationRevoked(contractAddress, block.timestamp); } /** @@ -161,22 +189,23 @@ contract RewardLogic is Ownable, IRewards { * @dev Distribute DCU tokens to a user (implementation of IRewards interface) * @param user Address of the user to distribute to * @param amount Amount of DCU to distribute + * @return success Whether the distribution was successful */ - function distributeDCU(address user, uint256 amount) external override { + function distributeDCU(address user, uint256 amount) external override returns (bool) { // Only authorized contracts can call this function - require( - msg.sender == address(nftCollection) || authorizedContracts[msg.sender], - "Only authorized contracts can call" - ); + require(authorizedContracts[msg.sender] || msg.sender == address(nftCollection), + "Only authorized contracts can call"); - // Verify user through the reward manager if available + // Initialize reward manager DCURewardManager rewardManager = DCURewardManager(address(0)); - // Try to get the reward manager from the NFT contract - try INFTCollection(nftCollection).rewardsContract() returns (address rewardsContractAddr) { - rewardManager = DCURewardManager(rewardsContractAddr); - } catch { - // Continue even if we can't get the reward manager + // Only try to get the reward manager if we have an NFT collection + if (address(nftCollection) != address(0)) { + try INFTCollection(nftCollection).rewardsContract() returns (address rewardsContractAddr) { + rewardManager = DCURewardManager(rewardsContractAddr); + } catch { + // Continue even if we can't get the reward manager + } } // If we have a reward manager and the caller is the NFT contract, check eligibility @@ -193,7 +222,8 @@ contract RewardLogic is Ownable, IRewards { } // Mint tokens to the user - require(dcuToken.mint(user, amount), "DCU distribution failed"); + bool success = dcuToken.mint(user, amount); + require(success, "DCU distribution failed"); emit DCUDistributed( user, @@ -201,5 +231,7 @@ contract RewardLogic is Ownable, IRewards { block.timestamp, "Reward from authorized contract" ); + + return success; } } \ No newline at end of file diff --git a/contracts/Submission.sol b/contracts/Submission.sol new file mode 100644 index 0000000..f3511a1 --- /dev/null +++ b/contracts/Submission.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IDCUToken.sol"; +import "./interfaces/IRewards.sol"; + +/** + * @title Submission + * @dev Contract for handling form submissions from the DeCleanup dapp + */ +contract Submission is Ownable, ReentrancyGuard, AccessControl { + // Custom Errors + error SUBMISSION__InvalidAddress(); + error SUBMISSION__InvalidSubmissionData(); + error SUBMISSION__SubmissionNotFound(uint256 submissionId); + error SUBMISSION__Unauthorized(address user); + error SUBMISSION__AlreadyApproved(uint256 submissionId); + error SUBMISSION__AlreadyRejected(uint256 submissionId); + error SUBMISSION__NoRewardsAvailable(); + + // Role definitions for access control + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + + // Submission status enum + enum SubmissionStatus { Pending, Approved, Rejected } + + // Submission structure + struct Submission { + uint256 id; + address submitter; + string dataURI; // IPFS URI or other storage reference to submission data + uint256 timestamp; + SubmissionStatus status; + address approver; // Admin who processed the submission + uint256 processedTimestamp; + bool rewarded; // Whether a reward has been issued for this submission + } + + // Reference to the DCU token contract for rewards + IDCUToken public dcuToken; + + // Reference to the RewardLogic contract + IRewards public rewardLogic; + + // Mapping from submission ID to Submission data + mapping(uint256 => Submission) public submissions; + + // Mapping from user address to their submission IDs + mapping(address => uint256[]) public userSubmissions; + + // Mapping from user address to claimable rewards amount + mapping(address => uint256) public claimableRewards; + + // Total number of submissions + uint256 public submissionCount; + + // Default reward amount for approved submissions (in wei, 18 decimals) + uint256 public defaultRewardAmount; + + // Events + event SubmissionCreated( + uint256 indexed submissionId, + address indexed submitter, + string dataURI, + uint256 timestamp + ); + + event SubmissionApproved( + uint256 indexed submissionId, + address indexed approver, + uint256 timestamp + ); + + event SubmissionRejected( + uint256 indexed submissionId, + address indexed approver, + uint256 timestamp + ); + + event DefaultRewardUpdated(uint256 oldAmount, uint256 newAmount); + + event RewardClaimed( + address indexed user, + uint256 amount, + uint256 timestamp + ); + + event RewardAvailable( + address indexed user, + uint256 amount, + uint256 submissionId, + uint256 timestamp + ); + + /** + * @dev Constructor sets up the contract with DCU token, RewardLogic, and roles + * @param _dcuToken Address of the DCU token contract + * @param _rewardLogic Address of the RewardLogic contract + * @param _defaultRewardAmount Default reward amount for approved submissions + */ + constructor(address _dcuToken, address _rewardLogic, uint256 _defaultRewardAmount) Ownable(msg.sender) { + if (_dcuToken == address(0)) revert SUBMISSION__InvalidAddress(); + if (_rewardLogic == address(0)) revert SUBMISSION__InvalidAddress(); + + dcuToken = IDCUToken(_dcuToken); + rewardLogic = IRewards(_rewardLogic); + defaultRewardAmount = _defaultRewardAmount; + + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + _grantRole(ADMIN_ROLE, msg.sender); + } + + /** + * @dev Create a new submission + * @param dataURI IPFS URI or other storage reference to submission data + * @return submissionId The ID of the created submission + */ + function createSubmission(string calldata dataURI) external nonReentrant returns (uint256) { + if (bytes(dataURI).length == 0) revert SUBMISSION__InvalidSubmissionData(); + + uint256 submissionId = submissionCount; + + submissions[submissionId] = Submission({ + id: submissionId, + submitter: msg.sender, + dataURI: dataURI, + timestamp: block.timestamp, + status: SubmissionStatus.Pending, + approver: address(0), + processedTimestamp: 0, + rewarded: false + }); + + userSubmissions[msg.sender].push(submissionId); + submissionCount++; + + emit SubmissionCreated(submissionId, msg.sender, dataURI, block.timestamp); + + return submissionId; + } + + /** + * @dev Approve a submission (only for admins) + * @param submissionId The ID of the submission to approve + */ + function approveSubmission( + uint256 submissionId + ) external nonReentrant onlyRole(ADMIN_ROLE) { + if (submissionId >= submissionCount) revert SUBMISSION__SubmissionNotFound(submissionId); + + Submission storage submission = submissions[submissionId]; + + if (submission.status == SubmissionStatus.Approved) + revert SUBMISSION__AlreadyApproved(submissionId); + + submission.status = SubmissionStatus.Approved; + submission.approver = msg.sender; + submission.processedTimestamp = block.timestamp; + + emit SubmissionApproved( + submissionId, + msg.sender, + block.timestamp + ); + + // Instead of immediately distributing rewards, add to claimable rewards + if (!submission.rewarded) { + submission.rewarded = true; + claimableRewards[submission.submitter] += defaultRewardAmount; + + emit RewardAvailable( + submission.submitter, + defaultRewardAmount, + submissionId, + block.timestamp + ); + } + } + + /** + * @dev Claim available rewards + */ + function claimRewards() external nonReentrant { + uint256 amount = claimableRewards[msg.sender]; + if (amount == 0) revert SUBMISSION__NoRewardsAvailable(); + + // Reset claimable rewards before external calls + claimableRewards[msg.sender] = 0; + + // Distribute the rewards through RewardLogic and check the return value + bool success = rewardLogic.distributeDCU(msg.sender, amount); + require(success, "Reward distribution failed"); + + emit RewardClaimed( + msg.sender, + amount, + block.timestamp + ); + } + + /** + * @dev Get available claimable rewards for a user + * @param user The user address to check + * @return amount The amount of claimable rewards + */ + function getClaimableRewards(address user) external view returns (uint256) { + return claimableRewards[user]; + } + + /** + * @dev Reject a submission (only for admins) + * @param submissionId The ID of the submission to reject + */ + function rejectSubmission( + uint256 submissionId + ) external nonReentrant onlyRole(ADMIN_ROLE) { + if (submissionId >= submissionCount) revert SUBMISSION__SubmissionNotFound(submissionId); + + Submission storage submission = submissions[submissionId]; + + if (submission.status == SubmissionStatus.Rejected) + revert SUBMISSION__AlreadyRejected(submissionId); + + submission.status = SubmissionStatus.Rejected; + submission.approver = msg.sender; + submission.processedTimestamp = block.timestamp; + + emit SubmissionRejected( + submissionId, + msg.sender, + block.timestamp + ); + } + + /** + * @dev Update the default reward amount (only for admins) + * @param newRewardAmount The new default reward amount + */ + function updateDefaultReward(uint256 newRewardAmount) external onlyRole(ADMIN_ROLE) { + uint256 oldAmount = defaultRewardAmount; + defaultRewardAmount = newRewardAmount; + + emit DefaultRewardUpdated(oldAmount, newRewardAmount); + } + + /** + * @dev Update the RewardLogic contract address (only for owner) + * @param _newRewardLogic The new RewardLogic contract address + */ + function updateRewardLogic(address _newRewardLogic) external onlyOwner { + if (_newRewardLogic == address(0)) revert SUBMISSION__InvalidAddress(); + rewardLogic = IRewards(_newRewardLogic); + } + + /** + * @dev Get all submissions for a user + * @param user The address of the user + * @return An array of submission IDs + */ + function getSubmissionsByUser(address user) external view returns (uint256[] memory) { + return userSubmissions[user]; + } + + /** + * @dev Get the details of a submission + * @param submissionId The ID of the submission + * @return The submission details + */ + function getSubmissionDetails(uint256 submissionId) external view returns (Submission memory) { + if (submissionId >= submissionCount) revert SUBMISSION__SubmissionNotFound(submissionId); + return submissions[submissionId]; + } + + /** + * @dev Get a batch of submissions for pagination + * @param startIndex The starting index + * @param batchSize The number of submissions to return + * @return Submission[] An array of submissions + */ + function getSubmissionBatch(uint256 startIndex, uint256 batchSize) + external + view + returns (Submission[] memory) + { + uint256 endIndex = startIndex + batchSize; + + // Ensure we don't go past the end of the submissions + if (endIndex > submissionCount) { + endIndex = submissionCount; + } + + // Calculate actual batch size + uint256 resultSize = endIndex - startIndex; + Submission[] memory result = new Submission[](resultSize); + + for (uint256 i = 0; i < resultSize; i++) { + result[i] = submissions[startIndex + i]; + } + + return result; + } +} \ No newline at end of file diff --git a/contracts/interfaces/IRewards.sol b/contracts/interfaces/IRewards.sol index 87f2164..cb6ced7 100644 --- a/contracts/interfaces/IRewards.sol +++ b/contracts/interfaces/IRewards.sol @@ -3,13 +3,14 @@ pragma solidity ^0.8.28; /** * @title IRewards - * @dev Interface for the rewards distribution contract + * @dev Interface for reward distribution contract */ interface IRewards { /** - * @dev Distribute DCU tokens to a user - * @param user The address of the user to distribute to - * @param amount The amount to distribute + * @dev Distributes DCU tokens to a user + * @param user Address of the user to distribute tokens to + * @param amount Amount of DCU tokens to distribute + * @return success Whether the distribution was successful */ - function distributeDCU(address user, uint256 amount) external; + function distributeDCU(address user, uint256 amount) external returns (bool); } \ No newline at end of file diff --git a/docs/ERROR_CODES.md b/docs/ERROR_CODES.md index 8d61ddc..fa2a65c 100644 --- a/docs/ERROR_CODES.md +++ b/docs/ERROR_CODES.md @@ -41,6 +41,18 @@ Where: | `NFT__UserHasNoNFT` | User has not minted an NFT | `user`: The user's address | | `NFT__TransferNotAuthorized` | Transfer is not authorized for the token | `tokenId`: The token ID | +### Submission Contract Errors + +| Error Code | Description | Parameters | +|------------|-------------|------------| +| `SUBMISSION__InvalidAddress` | Invalid address (typically zero address) | None | +| `SUBMISSION__InvalidSubmissionData` | The submission data is invalid or empty | None | +| `SUBMISSION__SubmissionNotFound` | The specified submission ID does not exist | `submissionId`: The queried submission ID | +| `SUBMISSION__Unauthorized` | User is not authorized to perform this action | `user`: The user's address | +| `SUBMISSION__AlreadyApproved` | Submission has already been approved | `submissionId`: The submission ID | +| `SUBMISSION__AlreadyRejected` | Submission has already been rejected | `submissionId`: The submission ID | +| `SUBMISSION__NoRewardsAvailable` | No rewards are available for the user to claim | None | + ### DCURewardManager Contract Errors | Error Code | Description | Parameters | diff --git a/test/Submission.test.ts b/test/Submission.test.ts new file mode 100644 index 0000000..31b39b2 --- /dev/null +++ b/test/Submission.test.ts @@ -0,0 +1,395 @@ +import { expect } from "chai"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { ethers } from "hardhat"; + +describe("Submission", function () { + // Define a fixture for deploying the test environment + async function deploySubmissionFixture() { + const [owner, user, admin] = await ethers.getSigners(); + + // Deploy DCU token with owner as initial reward logic + const DCUToken = await ethers.getContractFactory("DCUToken"); + const dcuToken = await DCUToken.deploy(owner.address); + + // Deploy the real reward logic with zero address for NFT collection + const RewardLogic = await ethers.getContractFactory("RewardLogic"); + const rewardLogic = await RewardLogic.deploy( + dcuToken.address, + ethers.constants.AddressZero + ); + + // Update reward logic in token + await dcuToken.updateRewardLogicContract(rewardLogic.address); + + // Deploy submission contract + const defaultRewardAmount = ethers.utils.parseEther("10"); // 10 DCU tokens + const Submission = await ethers.getContractFactory("Submission"); + const submission = await Submission.deploy( + dcuToken.address, + rewardLogic.address, + defaultRewardAmount + ); + + // Add the submission contract to authorized contracts in RewardLogic + const rewardLogicWithAuth = new ethers.Contract( + rewardLogic.address, + [ + "function authorizeContract(address contractAddress) external", + "function distributeDCU(address user, uint256 amount) external returns (bool)", + "function authorizedContracts(address) external view returns (bool)", + ], + owner + ); + await rewardLogicWithAuth.authorizeContract(submission.address); + + // Verify the submission contract is authorized + const isAuthorized = await rewardLogicWithAuth.authorizedContracts( + submission.address + ); + if (!isAuthorized) { + throw new Error("Failed to authorize submission contract"); + } + + // Grant admin role to the admin account + const ADMIN_ROLE = await submission.ADMIN_ROLE(); + await submission.grantRole(ADMIN_ROLE, admin.address); + + return { + submission, + dcuToken, + rewardLogic: rewardLogicWithAuth, + owner, + user, + admin, + }; + } + + describe("Submission Creation", function () { + it("Should create a submission with the correct data", async function () { + const { submission, user } = await loadFixture(deploySubmissionFixture); + + // Create a submission + const dataURI = "ipfs://QmTest123"; + const tx = await submission.connect(user).createSubmission(dataURI); + await tx.wait(); + + // Get the submission ID from events (should be 0 for the first submission) + const submissionId = 0; + + // Check submission count + const count = await submission.submissionCount(); + expect(count.toNumber()).to.equal(1); + + // Verify the user's submission was recorded + const userSubmissions = await submission.getSubmissionsByUser( + user.address + ); + expect(userSubmissions.length).to.equal(1); + expect(userSubmissions[0].toNumber()).to.equal(submissionId); + }); + + it("Should reject submissions with empty dataURI", async function () { + const { submission, user } = await loadFixture(deploySubmissionFixture); + + // Try to create a submission with empty data URI + try { + await submission.connect(user).createSubmission(""); + expect.fail("Should have reverted"); + } catch (error: any) { + expect(error.message).to.include("SUBMISSION__InvalidSubmissionData"); + } + }); + }); + + describe("Submission Approval", function () { + it("Should allow admin to approve a submission and make rewards claimable", async function () { + const { submission, user, admin, dcuToken } = await loadFixture( + deploySubmissionFixture + ); + + // Create a submission + const dataURI = "ipfs://QmTest123"; + await submission.connect(user).createSubmission(dataURI); + const submissionId = 0; + + // Check initial claimable rewards + const initialClaimable = await submission.getClaimableRewards( + user.address + ); + console.log("Initial claimable rewards:", initialClaimable.toString()); + expect(initialClaimable.toNumber()).to.equal(0); + + // Check user's initial DCU balance + const initialBalance = await dcuToken.balanceOf(user.address); + console.log("Initial DCU balance:", initialBalance.toString()); + expect(initialBalance.toNumber()).to.equal(0); + + // Admin approves the submission + const tx = await submission + .connect(admin) + .approveSubmission(submissionId); + await tx.wait(); + + // Check that rewards are now claimable + const claimableRewards = await submission.getClaimableRewards( + user.address + ); + console.log( + "Claimable rewards after approval:", + claimableRewards.toString() + ); + const expectedReward = ethers.utils.parseEther("10"); + console.log("Expected reward amount:", expectedReward.toString()); + + // Using strict equality can fail with BigNumber, use equals method instead + expect(claimableRewards.toString()).to.equal(expectedReward.toString()); + + // User's DCU balance should still be 0 until they claim the rewards + const balanceAfterApproval = await dcuToken.balanceOf(user.address); + console.log( + "DCU balance after approval:", + balanceAfterApproval.toString() + ); + expect(balanceAfterApproval.toNumber()).to.equal(0); + }); + + it("Should prevent non-admin from approving submissions", async function () { + const { submission, user } = await loadFixture(deploySubmissionFixture); + + // Create a submission + await submission.connect(user).createSubmission("ipfs://QmTest123"); + const submissionId = 0; + + // Try to approve the submission as non-admin user + try { + await submission.connect(user).approveSubmission(submissionId); + expect.fail("Should have reverted"); + } catch (error: any) { + // Check for AccessControl error + expect(error.message).to.include("AccessControl"); + } + }); + + it("Should prevent approving a non-existent submission", async function () { + const { submission, admin } = await loadFixture(deploySubmissionFixture); + + // Try to approve a non-existent submission + try { + await submission.connect(admin).approveSubmission(999); + expect.fail("Should have reverted"); + } catch (error: any) { + expect(error.message).to.include("SUBMISSION__SubmissionNotFound"); + } + }); + + it("Should prevent approving an already approved submission", async function () { + const { submission, user, admin } = await loadFixture( + deploySubmissionFixture + ); + + // Create and approve a submission + await submission.connect(user).createSubmission("ipfs://QmTest123"); + await submission.connect(admin).approveSubmission(0); + + // Try to approve it again + try { + await submission.connect(admin).approveSubmission(0); + expect.fail("Should have reverted"); + } catch (error: any) { + expect(error.message).to.include("SUBMISSION__AlreadyApproved"); + } + }); + }); + + describe("Reward Claiming", function () { + it("Should allow users to claim rewards from approved submissions", async function () { + const { submission, user, admin, dcuToken } = await loadFixture( + deploySubmissionFixture + ); + + // Create and approve a submission + await submission.connect(user).createSubmission("ipfs://QmTest123"); + await submission.connect(admin).approveSubmission(0); + + // Check claimable rewards before claiming + const claimableBefore = await submission.getClaimableRewards( + user.address + ); + console.log("Claimable before claiming:", claimableBefore.toString()); + const expectedReward = ethers.utils.parseEther("10"); + console.log("Expected reward amount:", expectedReward.toString()); + + // Using string comparison for BigNumber + expect(claimableBefore.toString()).to.equal(expectedReward.toString()); + + // Claim rewards + await submission.connect(user).claimRewards(); + + // Check claimable rewards after claiming (should be 0) + const claimableAfter = await submission.getClaimableRewards(user.address); + console.log("Claimable after claiming:", claimableAfter.toString()); + expect(claimableAfter.toNumber()).to.equal(0); + + // Check user's DCU balance after claiming + const balanceAfterClaim = await dcuToken.balanceOf(user.address); + console.log("DCU balance after claim:", balanceAfterClaim.toString()); + console.log("Expected DCU after claim:", expectedReward.toString()); + + // Using string comparison for BigNumber + expect(balanceAfterClaim.toString()).to.equal(expectedReward.toString()); + }); + + it("Should prevent claiming when no rewards are available", async function () { + const { submission, user } = await loadFixture(deploySubmissionFixture); + + // Try to claim rewards when none are available + try { + await submission.connect(user).claimRewards(); + expect.fail("Should have reverted"); + } catch (error: any) { + expect(error.message).to.include("SUBMISSION__NoRewardsAvailable"); + } + }); + + it("Should accumulate rewards from multiple approved submissions", async function () { + const { submission, user, admin, dcuToken } = await loadFixture( + deploySubmissionFixture + ); + + // Create and approve first submission + await submission.connect(user).createSubmission("ipfs://QmTest1"); + await submission.connect(admin).approveSubmission(0); + + // Create and approve second submission + await submission.connect(user).createSubmission("ipfs://QmTest2"); + await submission.connect(admin).approveSubmission(1); + + // Check cumulative claimable rewards + const totalClaimable = await submission.getClaimableRewards(user.address); + console.log("Total claimable rewards:", totalClaimable.toString()); + const expectedTotal = ethers.utils.parseEther("20"); // 10 + 10 = 20 DCU + console.log("Expected total rewards:", expectedTotal.toString()); + + // Using string comparison for BigNumber + expect(totalClaimable.toString()).to.equal(expectedTotal.toString()); + + // Claim all rewards + await submission.connect(user).claimRewards(); + + // Verify user received all rewards + const finalBalance = await dcuToken.balanceOf(user.address); + console.log("Final DCU balance:", finalBalance.toString()); + + // Using string comparison for BigNumber + expect(finalBalance.toString()).to.equal(expectedTotal.toString()); + + // Claimable amount should be reset to 0 + const claimableAfter = await submission.getClaimableRewards(user.address); + console.log("Claimable after claiming all:", claimableAfter.toString()); + expect(claimableAfter.toNumber()).to.equal(0); + }); + }); + + describe("Submission Rejection", function () { + it("Should allow admin to reject a submission", async function () { + const { submission, user, admin } = await loadFixture( + deploySubmissionFixture + ); + + // Create a submission + await submission.connect(user).createSubmission("ipfs://QmTest123"); + const submissionId = 0; + + // Admin rejects the submission + await submission.connect(admin).rejectSubmission(submissionId); + + // Check submission count + const count = await submission.submissionCount(); + expect(count.toNumber()).to.equal(1); + + // Check that no rewards are claimable + const claimableRewards = await submission.getClaimableRewards( + user.address + ); + expect(claimableRewards.toNumber()).to.equal(0); + }); + + it("Should prevent rejecting an already rejected submission", async function () { + const { submission, user, admin } = await loadFixture( + deploySubmissionFixture + ); + + // Create and reject a submission + await submission.connect(user).createSubmission("ipfs://QmTest123"); + await submission.connect(admin).rejectSubmission(0); + + // Try to reject it again + try { + await submission.connect(admin).rejectSubmission(0); + expect.fail("Should have reverted"); + } catch (error: any) { + expect(error.message).to.include("SUBMISSION__AlreadyRejected"); + } + }); + }); + + describe("Configuration Management", function () { + it("Should allow admin to update the default reward amount", async function () { + const { submission, owner } = await loadFixture(deploySubmissionFixture); + + // Initial default reward is set to 10 DCU + const initialReward = await submission.defaultRewardAmount(); + expect(initialReward.toString()).to.equal( + ethers.utils.parseEther("10").toString() + ); + + // Update the default reward + const newReward = ethers.utils.parseEther("20"); + await submission.connect(owner).updateDefaultReward(newReward); + + // Verify update + const updatedReward = await submission.defaultRewardAmount(); + expect(updatedReward.toString()).to.equal(newReward.toString()); + }); + + it("Should allow owner to update the reward logic contract", async function () { + const { submission, rewardLogic, owner, dcuToken } = await loadFixture( + deploySubmissionFixture + ); + + // Check initial reward logic address + expect(await submission.rewardLogic()).to.equal(rewardLogic.address); + + // Deploy a new reward logic contract + const NewRewardLogic = await ethers.getContractFactory("RewardLogic"); + const newRewardLogicContract = await NewRewardLogic.deploy( + dcuToken.address, + ethers.constants.AddressZero + ); + + // Update the reward logic contract + await submission + .connect(owner) + .updateRewardLogic(newRewardLogicContract.address); + + // Verify update + expect(await submission.rewardLogic()).to.equal( + newRewardLogicContract.address + ); + }); + + it("Should reject invalid addresses for reward logic update", async function () { + const { submission, owner } = await loadFixture(deploySubmissionFixture); + + // Try to update with zero address + try { + await submission + .connect(owner) + .updateRewardLogic(ethers.constants.AddressZero); + expect.fail("Should have reverted"); + } catch (error: any) { + expect(error.message).to.include("SUBMISSION__InvalidAddress"); + } + }); + }); +});