Skip to content

Commit c6b58e5

Browse files
committed
Added Factory functionality
1 parent bbf3bfd commit c6b58e5

File tree

4 files changed

+162
-7
lines changed

4 files changed

+162
-7
lines changed

contracts/RaffleFactory.sol

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,68 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.28;
33

4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
45
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
56
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
6-
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
7+
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
8+
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
9+
import "./RaffleNFT.sol";
10+
11+
contract RaffleFactory is Initializable, UUPSUpgradeable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
12+
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
13+
address[] public raffles;
14+
15+
event RaffleCreated(address indexed raffleAddress, address indexed creator);
716

8-
contract RaffleFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable {
917
/// @custom:oz-upgrades-unsafe-allow constructor
1018
constructor() {
1119
_disableInitializers();
1220
}
1321

1422
function initialize() public initializer {
15-
__Ownable_init(msg.sender);
1623
__UUPSUpgradeable_init();
24+
__AccessControl_init();
25+
__ReentrancyGuard_init();
26+
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
27+
}
28+
29+
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}
30+
31+
modifier onlyManager() {
32+
require(hasRole(MANAGER_ROLE, msg.sender), "Not manager");
33+
_;
34+
}
35+
36+
modifier onlyAdmin() {
37+
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not admin");
38+
_;
1739
}
1840

19-
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
41+
// Reentrancy guarded via `nonReentrant` modifier
42+
// slither-disable-next-line reentrancy-benign
43+
function createRaffle(
44+
string memory name_,
45+
string memory symbol_,
46+
string memory tokenURI_,
47+
address prizeToken,
48+
uint256 amount
49+
) external onlyManager nonReentrant returns (address) {
50+
// Transfer tokens from user to factory
51+
require(
52+
IERC20(prizeToken).transferFrom(msg.sender, address(this), amount),
53+
"Token transfer to factory failed"
54+
);
55+
// Deploy RaffleNFT
56+
RaffleNFT raffle = new RaffleNFT(name_, symbol_, tokenURI_);
57+
raffles.push(address(raffle));
58+
// Approve RaffleNFT to spend tokens
59+
require(
60+
IERC20(prizeToken).approve(address(raffle), amount),
61+
"Approve to raffle failed"
62+
);
63+
// Start the raffle (transfer tokens to the contract)
64+
raffle.startRaffle(prizeToken, amount);
65+
emit RaffleCreated(address(raffle), msg.sender);
66+
return address(raffle);
67+
}
2068
}

contracts/RaffleNFT.sol

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,40 @@
22
pragma solidity ^0.8.28;
33

44
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
5+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
57

6-
contract RaffleNFT is ERC721 {
7-
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {}
8+
contract RaffleNFT is ERC721, ReentrancyGuard {
9+
address public prizeToken;
10+
uint256 public amount;
11+
bool public started;
12+
string private baseTokenURI;
13+
14+
constructor(
15+
string memory name_,
16+
string memory symbol_,
17+
string memory tokenURI_
18+
) ERC721(name_, symbol_) {
19+
baseTokenURI = tokenURI_;
20+
started = false;
21+
}
22+
23+
function startRaffle(
24+
address prizeToken_,
25+
uint256 amount_
26+
) external nonReentrant {
27+
require(prizeToken_ != address(0), "Zero address");
28+
require(!started, "Already started");
29+
started = true;
30+
prizeToken = prizeToken_;
31+
amount = amount_;
32+
require(
33+
IERC20(prizeToken).transferFrom(msg.sender, address(this), amount),
34+
"ERC20 transfer failed"
35+
);
36+
}
37+
38+
function tokenURI(uint256) public view override returns (string memory) {
39+
return baseTokenURI;
40+
}
841
}

contracts/test/ERC20.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+
contract ERC20TestToken is ERC20 {
7+
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}
8+
9+
function mint(address to, uint256 amount) public {
10+
_mint(to, amount);
11+
}
12+
}

test/Factory.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,70 @@ describe("Factory", function () {
1212

1313
// Deploy RaffleNFT (not upgradeable)
1414
const RaffleNFT = await ethers.getContractFactory("RaffleNFT");
15-
const nft = await RaffleNFT.deploy("TestNFT", "TNFT");
15+
const nft = await RaffleNFT.deploy("TestNFT", "TNFT", "https://example.com/metadata.json");
1616
await nft.waitForDeployment();
1717
expect(nft.target).to.properAddress;
1818
});
1919
});
20+
21+
describe("RaffleFactory full flow", function () {
22+
it("Should deploy ERC20, RaffleFactory, and create Raffle via factory with correct checks", async function () {
23+
const [owner, manager] = await ethers.getSigners();
24+
25+
// Deploy test ERC20 token
26+
const ERC20TestToken = await ethers.getContractFactory("ERC20TestToken", manager);
27+
28+
const erc20 = await ERC20TestToken.deploy("TestToken", "TTK");
29+
await erc20.waitForDeployment();
30+
expect(erc20.target).to.properAddress;
31+
32+
// Mint tokens to manager
33+
await erc20.mint(manager.address, 1000);
34+
expect(await erc20.balanceOf(manager.address)).to.equal(1000);
35+
36+
// Deploy RaffleFactory as UUPS proxy (owner)
37+
const RaffleFactory = await ethers.getContractFactory("RaffleFactory", owner);
38+
const factory = await upgrades.deployProxy(RaffleFactory, [], { kind: "uups" });
39+
await factory.waitForDeployment();
40+
expect(factory.target).to.properAddress;
41+
42+
// Grant MANAGER_ROLE to manager
43+
const MANAGER_ROLE = await factory.MANAGER_ROLE();
44+
await factory.grantRole(MANAGER_ROLE, manager.address);
45+
expect(await factory.hasRole(MANAGER_ROLE, manager.address)).to.be.true;
46+
47+
// Manager approves factory to spend tokens
48+
await erc20.connect(manager).approve(factory.target, 500);
49+
expect(await erc20.allowance(manager.address, factory.target)).to.equal(500);
50+
51+
// Manager creates a new Raffle
52+
const tokenURI = "https://example.com/metadata.json";
53+
const tx = await factory.connect(manager).createRaffle(
54+
"TestRaffle",
55+
"TRFL",
56+
tokenURI,
57+
erc20.target,
58+
500
59+
);
60+
const receipt = await tx.wait();
61+
62+
// Check event
63+
const event = receipt.logs.find(l => l.fragment && l.fragment.name === "RaffleCreated");
64+
expect(event).to.exist;
65+
const raffleAddress = event.args.raffleAddress;
66+
expect(raffleAddress).to.properAddress;
67+
68+
// Check balances
69+
expect(await erc20.balanceOf(manager.address)).to.equal(500);
70+
expect(await erc20.balanceOf(factory.target)).to.equal(0);
71+
expect(await erc20.balanceOf(raffleAddress)).to.equal(500);
72+
73+
// Check RaffleNFT state
74+
const RaffleNFT = await ethers.getContractFactory("RaffleNFT");
75+
const raffle = await RaffleNFT.attach(raffleAddress);
76+
expect(await raffle.prizeToken()).to.equal(erc20.target);
77+
expect(await raffle.amount()).to.equal(500);
78+
expect(await raffle.started()).to.be.true;
79+
expect(await raffle.tokenURI(0)).to.equal(tokenURI);
80+
});
81+
});

0 commit comments

Comments
 (0)