Skip to content
Open
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
20 changes: 20 additions & 0 deletions script/MembershipManager.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

import {Script} from "forge-std/Script.sol";
import {AccessToken} from "../src/AccessToken.sol";
import {MembershipNFT} from "../src/MembershipNFT.sol";
import {MembershipManager} from "../src/MembershipManager.sol";

contract deployAccessToken is Script {
function run() external returns(AccessToken, MembershipNFT, MembershipManager) {
vm.startBroadcast();
AccessToken accessToken = new AccessToken();
MembershipNFT membershipNFT = new MembershipNFT();
MembershipManager membershipManager = new MembershipManager(address(accessToken), address(membershipNFT), address(accessToken.getOwner()));
vm.stopBroadcast();

return (accessToken, membershipNFT, membershipManager);
}
}
26 changes: 26 additions & 0 deletions src/AccessToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract AccessToken is ERC20{

address private owner;

modifier onlyOwner {
require(msg.sender == owner, "Unauthorized User");
_;
}

constructor() ERC20("AccessToken", "ACS"){
owner = msg.sender;
}

function mint(address to, uint256 _amount) onlyOwner external {
_mint(to, _amount);
}

function getOwner() external view returns(address) {
return owner;
}
}
56 changes: 56 additions & 0 deletions src/MembershipManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
// This is a Token-Gated Membership System where users gain access to exclusive features by holding a minimum amount of ERC20 tokens and are awarded ERC721 membership NFTs for elite participation.

pragma solidity ^0.8.15;

// Import contracts for Token and NFT
import {MembershipNFT} from "src/MembershipNFT.sol";
import {AccessToken} from "src/AccessToken.sol";

contract MembershipManager {
AccessToken public _accessToken;
MembershipNFT public _membershipNFT;

// State variables
address public owner;
uint256 public MAX_AMOUNT = 50;
uint256 public minAmount;

// Custom errors
error AmountTooHigh();
error InsufficientBalance();
error UnauthorizedAccess();
error UnauthorizedClaim();

constructor(address _deployedAccessToken, address _deployedNFT, address _owner) {
_accessToken = AccessToken(_deployedAccessToken);
_membershipNFT = MembershipNFT(_deployedNFT);
owner = _owner;
}

// Mapping for balances of Tokens and if a user has claimed NFT prior
mapping (address => uint256) public balances;
mapping (address => bool) public hasClaimed;

function setMinimumThreshold(uint256 _newMinAmount) external {
if (msg.sender != _accessToken.getOwner()) { revert UnauthorizedAccess();}
minAmount = _newMinAmount;
}

function getAccessToken(uint256 amount) external {
if (amount > MAX_AMOUNT) {revert AmountTooHigh();}

_accessToken.transfer(msg.sender, amount);
balances[msg.sender] += amount;
}

function claimMembershipNFT() external returns(uint256) {
if (_accessToken.balanceOf(msg.sender) < MAX_AMOUNT) {revert InsufficientBalance();}
if (hasClaimed[msg.sender] == true) { revert UnauthorizedClaim();}
hasClaimed[msg.sender] = true;

uint256 tokenId = _membershipNFT.mint(msg.sender);
return tokenId;
}
}

21 changes: 21 additions & 0 deletions src/MembershipNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

import "../../node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract MembershipNFT is ERC721{
uint256 public tokenID;

mapping (uint256 => address) public NFTowners;
constructor() ERC721("AccessNFT", "ANT"){}

function mint(address recipient) external returns(uint256){
tokenID++;
uint256 tokenId = tokenID;

NFTowners[tokenId] = msg.sender;
_mint(recipient, tokenId);
return tokenId;
}
}
125 changes: 125 additions & 0 deletions test/MembershipManager.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

import {Test, console} from "forge-std/Test.sol";
import {MembershipManager} from "../src/MembershipManager.sol";
import {AccessToken} from "../src/AccessToken.sol";
import {MembershipNFT} from "../src/MembershipNFT.sol";

contract TokenGateTest is Test {
uint256 MINIMUM_BALANCE = 10;
address meso = makeAddr("meso");
MembershipManager public membershipManager;
AccessToken public accessToken;
MembershipNFT public membershipNFT;

address account1 = address(0x345);
address account2 = address(0x678);

// Custom errors
error AmountTooHigh();
error InsufficientBalance();
error UnauthorizedAccess();
error UnauthorizedClaim();

function setUp() public {
accessToken = new AccessToken();
membershipNFT = new MembershipNFT();

membershipManager = new MembershipManager(address(accessToken), address(membershipNFT), address(membershipManager));
accessToken.mint(address(membershipManager), 1000);
}

function test_Deployment() public view {
assertEq(accessToken.balanceOf(address(membershipManager)), 1000);
assertEq(accessToken.name(), "AccessToken");
assertEq(accessToken.symbol(), "ACS");
assertEq(accessToken.getOwner(), address(this));
assertEq(membershipNFT.name(), "AccessNFT");
assertEq(membershipNFT.symbol(), "ANT");
}

function test_Revert_If_Unauthorized_Minter() public {
vm.prank(account1);
vm.expectRevert();
accessToken.mint(account2, 1000);
}

function test_OnlyOwner_Can_Call_MinimumThreshold() public {
vm.prank(account1);
vm.expectRevert(UnauthorizedAccess.selector);
membershipManager.setMinimumThreshold(200);
}

function test_Successfully_SetMinimumThreshold () public {
vm.prank(address(membershipManager));
assertEq(membershipManager.minAmount(), 0);
membershipManager.setMinimumThreshold(100);
assertEq(membershipManager.minAmount(), 100);
}

function test_Maximum_AccessToken_Amount() public {
assertEq(accessToken.balanceOf(account1), 0);
vm.startPrank(account1);
vm.expectRevert(AmountTooHigh.selector);
membershipManager.getAccessToken(100);
vm.stopPrank();
}

function test_AccessToken_Transfer_Successfully () public {
assertEq(accessToken.balanceOf(account1), 0);

vm.startPrank(account1);
membershipManager.getAccessToken(50);
uint256 currentBalance = accessToken.balanceOf(account1);
assertEq(currentBalance, 50);
console.log(currentBalance);
assertEq(membershipManager.balances(account1), 50);
vm.stopPrank();
}

function test_Revert_If_CallerBalance_Is_Insufficient_To_MintNFT() public {
vm.startPrank(account1);
membershipManager.getAccessToken(30);
uint256 currentBalance = accessToken.balanceOf(account1);
assertEq(currentBalance, 30);
assertEq(membershipManager.balances(account1), 30);

vm.expectRevert(InsufficientBalance.selector);
membershipManager.claimMembershipNFT();
vm.stopPrank();
}

function test_If_Caller_Has_Claimed_NFT_Before() public {
vm.startPrank(account1);
membershipManager.getAccessToken(50);
uint256 currentBalance = accessToken.balanceOf(account1);
assertEq(currentBalance, 50);
assertEq(membershipManager.balances(account1), 50);

assertEq(membershipManager.hasClaimed(account1), false);
membershipManager.claimMembershipNFT();

vm.expectRevert(UnauthorizedClaim.selector);
membershipManager.claimMembershipNFT();

vm.stopPrank();
}

function test_If_User_Claimed_NFT_Successfully() public {
vm.startPrank(account1);
membershipManager.getAccessToken(50);
uint256 currentBalance = accessToken.balanceOf(account1);
assertEq(currentBalance, 50);
assertEq(membershipManager.balances(account1), 50);

assertEq(membershipManager.hasClaimed(account1), false);

uint256 mintedTokenId = membershipManager.claimMembershipNFT();

assertEq(mintedTokenId, 1);
assertEq(membershipNFT.ownerOf(mintedTokenId), account1);
vm.stopPrank();
}
}