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
50 changes: 41 additions & 9 deletions contracts/RepositoryRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ pragma solidity ^0.8.28;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract RepositoryRegistry is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;

struct Repository {
string name;
Expand All @@ -19,13 +23,25 @@ contract RepositoryRegistry is Ownable, ReentrancyGuard {

mapping(uint256 => Repository) private repositories;
uint256 private repoCounter;

// Event placeholders
event RepositorySubmitted(uint256 indexed id, address indexed maintainer);

// === Fee System ===
IERC20 public immutable token;
address public feeWallet;
uint256 public submissionFee;

// Events
event RepositorySubmitted(uint256 indexed id, address indexed maintainer, uint256 feePaid);
event RepositoryUpdated(uint256 indexed id, address indexed maintainer);
event RepositoryDeactivated(uint256 indexed id);

constructor(address initialOwner) Ownable(initialOwner) {
event SubmissionFeeUpdated(uint256 oldFee, uint256 newFee);
event FeeWalletUpdated(address oldWallet, address newWallet);

constructor(address initialOwner, address _token, address _feeWallet, uint256 _submissionFee) Ownable(initialOwner) {
require(_token != address(0), "Token address cannot be zero");
require(_feeWallet != address(0), "Fee wallet cannot be zero");
token = IERC20(_token);
feeWallet = _feeWallet;
submissionFee = _submissionFee;
repoCounter = 0;
}

Expand Down Expand Up @@ -61,10 +77,14 @@ contract RepositoryRegistry is Ownable, ReentrancyGuard {
require(bytes(name).length > 0, "Repository name cannot be empty");
require(bytes(url).length > 0, "Repository URL cannot be empty");
require(tags.length > 0, "Tags are required");


// Require fee payment
require(submissionFee > 0, "Submission fee not set");
token.safeTransferFrom(msg.sender, feeWallet, submissionFee);

Comment on lines +81 to +84
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Allow zero fee to disable charging; move external transfer after state updates (CEI)

  • Requiring submissionFee > 0 prevents disabling fees, contradicting “Backward compatibility maintained.”
  • Perform the ERC20 transfer after storage writes to follow checks‑effects‑interactions. nonReentrant remains a good guard; a revert still rolls back state.
-        // Require fee payment
-        require(submissionFee > 0, "Submission fee not set");
-        token.safeTransferFrom(msg.sender, feeWallet, submissionFee);
+        // Effects first, interactions later (fee may be zero to disable charging)
@@
-        // Emit event
-        emit RepositorySubmitted(repoCounter, msg.sender, submissionFee);
+        // Interactions (charge fee if enabled)
+        uint256 fee = submissionFee;
+        if (fee > 0) {
+            token.safeTransferFrom(msg.sender, feeWallet, fee);
+        }
+        // Emit event
+        emit RepositorySubmitted(repoCounter, msg.sender, fee);

Also applies to: 100-103

🤖 Prompt for AI Agents
In contracts/RepositoryRegistry.sol around lines 81-84 (and similarly lines
100-103), the code currently requires submissionFee > 0 and performs the ERC20
transfer before updating storage; remove the require so a zero submissionFee is
allowed (disabling charging), and move the token.safeTransferFrom call to after
the state changes to follow Checks-Effects-Interactions (you can keep
nonReentrant); ensure you still call safeTransferFrom only when submissionFee >
0 to avoid unnecessary external calls when fee is zero.

// Increment counter and create new repository
repoCounter += 1;

// Store repository in mapping
repositories[repoCounter] = Repository({
name: name,
Expand All @@ -76,9 +96,21 @@ contract RepositoryRegistry is Ownable, ReentrancyGuard {
submissionTime: block.timestamp,
tags: tags
});

// Emit event
emit RepositorySubmitted(repoCounter, msg.sender);
emit RepositorySubmitted(repoCounter, msg.sender, submissionFee);
}

// === Admin Functions ===
function setSubmissionFee(uint256 newFee) external onlyOwner {
emit SubmissionFeeUpdated(submissionFee, newFee);
submissionFee = newFee;
}

function setFeeWallet(address newWallet) external onlyOwner {
require(newWallet != address(0), "Fee wallet cannot be zero");
emit FeeWalletUpdated(feeWallet, newWallet);
feeWallet = newWallet;
}

/**
Expand Down
72 changes: 68 additions & 4 deletions test/RepositoryRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,63 @@ import "@nomicfoundation/hardhat-viem/types";

describe("RepositoryRegistry", function () {
async function deployRepositoryRegistryFixture() {
const [owner, maintainer1, maintainer2, otherAccount] =
const [owner, maintainer1, maintainer2, otherAccount, feeWallet] =
await hre.viem.getWalletClients();

// Deploy MockDEVToken
const mockDEVToken = await hre.viem.deployContract("MockDEVToken", [
getAddress(owner.account.address),
"Mock DEV Token",
"mDEV",
]);

// Mint tokens to maintainers
const mintAmount = 1000n * 10n ** 18n;
await mockDEVToken.write.mintTo([
getAddress(maintainer1.account.address),
mintAmount,
]);
await mockDEVToken.write.mintTo([
getAddress(maintainer2.account.address),
mintAmount,
]);

// Set fee and fee wallet
const submissionFee = 10n * 10n ** 18n; // 10 tokens
const feeWalletAddr = getAddress(feeWallet.account.address);

// Deploy RepositoryRegistry with token, fee wallet, and fee
const repositoryRegistry = await hre.viem.deployContract(
"RepositoryRegistry",
[getAddress(owner.account.address)]
[
getAddress(owner.account.address),
mockDEVToken.address,
feeWalletAddr,
submissionFee,
]
);

// Approve registry to spend tokens for maintainers
await mockDEVToken.write.approve([
repositoryRegistry.address,
mintAmount,
], { account: maintainer1.account });
await mockDEVToken.write.approve([
repositoryRegistry.address,
mintAmount,
], { account: maintainer2.account });

const publicClient = await hre.viem.getPublicClient();

return {
repositoryRegistry,
mockDEVToken,
owner,
maintainer1,
maintainer2,
otherAccount,
feeWallet,
submissionFee,
publicClient,
};
}
Expand Down Expand Up @@ -159,8 +200,8 @@ describe("RepositoryRegistry", function () {
});

describe("Repository Submission", function () {
it("Should successfully submit a repository with valid inputs", async function () {
const { repositoryRegistry, maintainer1 } = await loadFixture(
it("Should successfully submit a repository with valid inputs and pay fee", async function () {
const { repositoryRegistry, maintainer1, feeWallet, mockDEVToken, submissionFee } = await loadFixture(
deployRepositoryRegistryFixture
);

Expand All @@ -169,12 +210,15 @@ describe("RepositoryRegistry", function () {
const url = "https://github.com/test/repository";
const tags = ["javascript", "blockchain", "testing"];

const feeWalletBefore = await mockDEVToken.read.balanceOf([feeWallet.account.address]);

await repositoryRegistry.write.submitRepository(
[name, description, url, tags],
{ account: maintainer1.account }
);

const repo = await repositoryRegistry.read.getRepositoryDetails([1n]);
const feeWalletAfter = await mockDEVToken.read.balanceOf([feeWallet.account.address]);

expect(repo.name).to.equal(name);
expect(repo.description).to.equal(description);
Expand All @@ -186,6 +230,26 @@ describe("RepositoryRegistry", function () {
expect(repo.isActive).to.be.true;
expect(Number(repo.submissionTime)).to.be.greaterThan(0);
expect(repo.tags).to.deep.equal(tags);
expect(feeWalletAfter - feeWalletBefore).to.equal(submissionFee);
});

it("Should fail if not enough allowance for fee", async function () {
const { repositoryRegistry, maintainer1, mockDEVToken } = await loadFixture(
deployRepositoryRegistryFixture
);

// Remove approval
await mockDEVToken.write.approve([
repositoryRegistry.address,
0n,
], { account: maintainer1.account });

await expect(
repositoryRegistry.write.submitRepository(
["Name", "Desc", "https://github.com/test", ["tag"]],
{ account: maintainer1.account }
)
).to.be.rejected;
});

it("Should emit RepositorySubmitted event with correct parameters", async function () {
Expand Down