Skip to content

Commit 9c503c3

Browse files
committed
feat: add priority token-gating (erc-721) (#41)
closes #41
1 parent 61f467a commit 9c503c3

File tree

6 files changed

+144
-14
lines changed

6 files changed

+144
-14
lines changed

contracts/protocol/Sector3DAO.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ contract Sector3DAO {
7070
token = token_;
7171
}
7272

73-
function deployPriority(string calldata title, address rewardToken, uint16 epochDurationInDays, uint256 epochBudget) public returns (Sector3DAOPriority) {
73+
function deployPriority(string calldata title, address rewardToken, uint16 epochDurationInDays, uint256 epochBudget, address gatingNFT) public returns (Sector3DAOPriority) {
7474
require(msg.sender == owner, "You aren't the owner");
75-
Sector3DAOPriority priority = new Sector3DAOPriority(address(this), title, rewardToken, epochDurationInDays, epochBudget);
75+
Sector3DAOPriority priority = new Sector3DAOPriority(address(this), title, rewardToken, epochDurationInDays, epochBudget, gatingNFT);
7676
priorities.push(priority);
7777
return priority;
7878
}

contracts/protocol/Sector3DAOPriority.sol

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.17;
33

44
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
5+
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
56
import "./IPriority.sol";
67
import "./Enums.sol";
78
import "./Structs.sol";
@@ -15,21 +16,24 @@ contract Sector3DAOPriority is IPriority {
1516
uint256 public immutable startTime;
1617
uint16 public immutable epochDuration;
1718
uint256 public immutable epochBudget;
19+
IERC721 public immutable gatingNFT;
1820
Contribution[] contributions;
1921

2022
event ContributionAdded(Contribution contribution);
2123
event RewardClaimed(uint16 epochIndex, address contributor, uint256 amount);
2224

2325
error EpochNotYetEnded();
2426
error NoRewardForEpoch();
27+
error NoGatingNFTOwnership();
2528

26-
constructor(address dao_, string memory title_, address rewardToken_, uint16 epochDurationInDays, uint256 epochBudget_) {
29+
constructor(address dao_, string memory title_, address rewardToken_, uint16 epochDurationInDays, uint256 epochBudget_, address gatingNFT_) {
2730
dao = dao_;
2831
title = title_;
2932
rewardToken = IERC20(rewardToken_);
3033
startTime = block.timestamp;
3134
epochDuration = epochDurationInDays;
3235
epochBudget = epochBudget_;
36+
gatingNFT = IERC721(gatingNFT_);
3337
}
3438

3539
/**
@@ -51,6 +55,11 @@ contract Sector3DAOPriority is IPriority {
5155
}
5256

5357
function addContribution2(string memory description, string memory proofURL, uint8 hoursSpent, Alignment alignment) public {
58+
if (address(gatingNFT) != address(0x0)) {
59+
if (gatingNFT.balanceOf(msg.sender) == 0) {
60+
revert NoGatingNFTOwnership();
61+
}
62+
}
5463
Contribution memory contribution = Contribution({
5564
timestamp: block.timestamp,
5665
epochIndex: getEpochIndex(),

contracts/token/Sector3Dove.sol

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.17;
3+
4+
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
5+
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
6+
import "@openzeppelin/contracts/access/Ownable.sol";
7+
import "@openzeppelin/contracts/utils/Counters.sol";
8+
9+
contract Sector3Dove is ERC721, ERC721Enumerable, Ownable {
10+
using Counters for Counters.Counter;
11+
12+
Counters.Counter private _tokenIdCounter;
13+
14+
constructor() ERC721(unicode"Sector#3 Dove 🕊️", "S3DOVE") {}
15+
16+
function safeMint(address to) public onlyOwner {
17+
uint256 tokenId = _tokenIdCounter.current();
18+
_tokenIdCounter.increment();
19+
_safeMint(to, tokenId);
20+
}
21+
22+
// The following functions are overrides required by Solidity.
23+
24+
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize)
25+
internal
26+
override(ERC721, ERC721Enumerable)
27+
{
28+
super._beforeTokenTransfer(from, to, tokenId, batchSize);
29+
}
30+
31+
function supportsInterface(bytes4 interfaceId)
32+
public
33+
view
34+
override(ERC721, ERC721Enumerable)
35+
returns (bool)
36+
{
37+
return super.supportsInterface(interfaceId);
38+
}
39+
}

scripts/deploy-nft.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ethers } from "hardhat";
2+
3+
async function main() {
4+
console.log('Deploying token/Sector3Dove.sol')
5+
6+
console.log('process.env.DEPLOYER_PRIVATE_KEY exists:', process.env.DEPLOYER_PRIVATE_KEY != undefined)
7+
console.log('process.env.ETHERSCAN_API_KEY exists:', process.env.ETHERSCAN_API_KEY != undefined)
8+
9+
const Sector3Dove = await ethers.getContractFactory("Sector3Dove");
10+
const sector3Dove = await Sector3Dove.deploy();
11+
12+
await sector3Dove.deployed();
13+
14+
console.log(`Sector3Dove deployed to ${sector3Dove.address}`);
15+
}
16+
17+
// We recommend this pattern to be able to use async/await everywhere
18+
// and properly handle errors.
19+
main().catch((error) => {
20+
console.error(error);
21+
process.exitCode = 1;
22+
});

test/Sector3DAO.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ describe("Sector3DAO", function () {
5959
console.log('priorities:', priorities);
6060
expect(priorities.length).to.equal(0);
6161

62-
await sector3DAO.deployPriority('Priority #1', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 7, (2.049 * 1e18).toString());
62+
await sector3DAO.deployPriority('Priority #1', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 7, (2.049 * 1e18).toString(), ethers.constants.AddressZero);
6363
priorities = await sector3DAO.getPriorities();
6464
console.log('priorities:', priorities);
6565
expect(priorities.length).to.equal(1);
6666

67-
await sector3DAO.deployPriority('Priority #2', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 14, (4.098 * 1e18).toString());
67+
await sector3DAO.deployPriority('Priority #2', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 14, (4.098 * 1e18).toString(), ethers.constants.AddressZero);
6868
priorities = await sector3DAO.getPriorities();
6969
console.log('priorities:', priorities);
7070
expect(priorities.length).to.equal(2);
@@ -73,7 +73,7 @@ describe("Sector3DAO", function () {
7373
it("remove priority - from array of 1", async function () {
7474
const { sector3DAO } = await loadFixture(deployOneYearLockFixture);
7575

76-
await sector3DAO.deployPriority('Priority #1', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 7, (2.049 * 1e18).toString());
76+
await sector3DAO.deployPriority('Priority #1', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 7, (2.049 * 1e18).toString(), ethers.constants.AddressZero);
7777
let priorities = await sector3DAO.getPriorities();
7878
console.log('priorities:', priorities);
7979
expect(priorities.length).to.equal(1);
@@ -87,8 +87,8 @@ describe("Sector3DAO", function () {
8787
it("remove priority - from array of 2", async function () {
8888
const { sector3DAO } = await loadFixture(deployOneYearLockFixture);
8989

90-
await sector3DAO.deployPriority('Priority #1', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 7, (2.049 * 1e18).toString());
91-
await sector3DAO.deployPriority('Priority #2', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 14, (4.098 * 1e18).toString());
90+
await sector3DAO.deployPriority('Priority #1', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 7, (2.049 * 1e18).toString(), ethers.constants.AddressZero);
91+
await sector3DAO.deployPriority('Priority #2', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 14, (4.098 * 1e18).toString(), ethers.constants.AddressZero);
9292
let priorities = await sector3DAO.getPriorities();
9393
console.log('priorities:', priorities);
9494
expect(priorities.length).to.equal(2);
@@ -103,8 +103,8 @@ describe("Sector3DAO", function () {
103103
it("remove priority, then deploy priority", async function () {
104104
const { sector3DAO } = await loadFixture(deployOneYearLockFixture);
105105

106-
await sector3DAO.deployPriority('Priority #1', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 7, (2.049 * 1e18).toString());
107-
await sector3DAO.deployPriority('Priority #2', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 14, (4.098 * 1e18).toString());
106+
await sector3DAO.deployPriority('Priority #1', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 7, (2.049 * 1e18).toString(), ethers.constants.AddressZero);
107+
await sector3DAO.deployPriority('Priority #2', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 14, (4.098 * 1e18).toString(), ethers.constants.AddressZero);
108108
let priorities = await sector3DAO.getPriorities();
109109
console.log('priorities:', priorities);
110110
expect(priorities.length).to.equal(2);
@@ -115,7 +115,7 @@ describe("Sector3DAO", function () {
115115
expect(prioritiesAfterRemoval.length).to.equal(1);
116116
expect(prioritiesAfterRemoval[0]).to.equal(priorities[1]);
117117

118-
await sector3DAO.deployPriority('Priority #3', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 21, (6.147 * 1e18).toString());
118+
await sector3DAO.deployPriority('Priority #3', '0x942d6e75465C3c248Eb8775472c853d2b56139fE', 21, (6.147 * 1e18).toString(), ethers.constants.AddressZero);
119119
priorities = await sector3DAO.getPriorities();
120120
console.log('priorities:', priorities);
121121
expect(priorities.length).to.equal(2);

test/Sector3Priority.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ describe("Sector3DAOPriority", function () {
2121
const rewardToken = await SECTOR3.deploy();
2222
const epochDurationInDays = 7; // Weekly
2323
const epochBudget = (2.049 * 1e18).toString(); // 2.049
24-
const sector3DAOPriority = await Sector3DAOPriority.deploy(dao, title, rewardToken.address, epochDurationInDays, epochBudget);
24+
const gatingNFT = ethers.constants.AddressZero;
25+
const sector3DAOPriority = await Sector3DAOPriority.deploy(dao, title, rewardToken.address, epochDurationInDays, epochBudget, gatingNFT);
2526

2627
return { sector3DAOPriority, owner, otherAccount, rewardToken };
2728
}
@@ -42,7 +43,8 @@ describe("Sector3DAOPriority", function () {
4243
const rewardToken = await SECTOR3.deploy();
4344
const epochDurationInDays = 14; // Biweekly
4445
const epochBudget = (2.049 * 1e18).toString(); // 2.049
45-
const sector3DAOPriority = await Sector3DAOPriority.deploy(dao, title, rewardToken.address, epochDurationInDays, epochBudget);
46+
const gatingNFT = ethers.constants.AddressZero;
47+
const sector3DAOPriority = await Sector3DAOPriority.deploy(dao, title, rewardToken.address, epochDurationInDays, epochBudget, gatingNFT);
4648

4749
return { sector3DAOPriority, owner, otherAccount, rewardToken };
4850
}
@@ -63,11 +65,38 @@ describe("Sector3DAOPriority", function () {
6365
const rewardToken = await SECTOR3.deploy();
6466
const epochDurationInDays = 28; // Monthly
6567
const epochBudget = (2.049 * 1e18).toString(); // 2.049
66-
const sector3DAOPriority = await Sector3DAOPriority.deploy(dao, title, rewardToken.address, epochDurationInDays, epochBudget);
68+
const gatingNFT = ethers.constants.AddressZero;
69+
const sector3DAOPriority = await Sector3DAOPriority.deploy(dao, title, rewardToken.address, epochDurationInDays, epochBudget, gatingNFT);
6770

6871
return { sector3DAOPriority, owner, otherAccount, rewardToken };
6972
}
7073

74+
75+
// We define a fixture to reuse the same setup in every test.
76+
// We use loadFixture to run this setup once, snapshot that state,
77+
// and reset Hardhat Network to that snapshot in every test.
78+
async function deployWeeklyFixtureWithNFTGating() {
79+
console.log('deployWeeklyFixtureWithNFTGating')
80+
81+
// Contracts are deployed using the first signer/account by default
82+
const [owner, otherAccount] = await ethers.getSigners();
83+
console.log('owner.address:', owner.address);
84+
console.log('otherAccount.address:', otherAccount.address);
85+
86+
const Sector3DAOPriority = await ethers.getContractFactory("Sector3DAOPriority");
87+
const dao = "0x96Bf89193E2A07720e42bA3AD736128a45537e63"; // Sector#3
88+
const title = "Priority Title";
89+
const SECTOR3 = await ethers.getContractFactory("SECTOR3");
90+
const rewardToken = await SECTOR3.deploy();
91+
const epochDurationInDays = 7; // Weekly
92+
const epochBudget = (2.049 * 1e18).toString(); // 2.049
93+
const Sector3Dove = await ethers.getContractFactory("Sector3Dove");
94+
const gatingNFT = await Sector3Dove.deploy();
95+
const sector3DAOPriority = await Sector3DAOPriority.deploy(dao, title, rewardToken.address, epochDurationInDays, epochBudget, gatingNFT.address);
96+
97+
return { sector3DAOPriority, owner, otherAccount, rewardToken, gatingNFT };
98+
}
99+
71100

72101
describe("Deployment", function() {
73102
it("Should set the right DAO address", async function() {
@@ -413,6 +442,37 @@ describe("Sector3DAOPriority", function () {
413442
});
414443
});
415444

445+
446+
describe("NFT gating - addContribution2", async function() {
447+
it("should fail if NFT gating", async function() {
448+
const { sector3DAOPriority, owner } = await loadFixture(deployWeeklyFixtureWithNFTGating);
449+
450+
await expect(sector3DAOPriority.addContribution2(
451+
"Description (test)",
452+
"https://github.com/sector-3",
453+
10,
454+
3 // Alignment.Mostly
455+
)).to.be.revertedWithCustomError(
456+
sector3DAOPriority,
457+
"NoGatingNFTOwnership"
458+
)
459+
});
460+
461+
it("should succeed if NFT gating and account is NFT owner", async function() {
462+
const { sector3DAOPriority, owner, gatingNFT } = await loadFixture(deployWeeklyFixtureWithNFTGating);
463+
464+
await gatingNFT.safeMint(owner.address);
465+
466+
await sector3DAOPriority.addContribution2(
467+
"Description (test)",
468+
"https://github.com/sector-3",
469+
10,
470+
3 // Alignment.Mostly
471+
);
472+
expect(await sector3DAOPriority.getContributionCount()).to.equal(1);
473+
});
474+
});
475+
416476

417477
describe("getAllocationPercentage", async function() {
418478
it("Should be 100% if one contributor", async function() {

0 commit comments

Comments
 (0)