Skip to content

Commit

Permalink
fix: restrict NFT minting only to voter (#184)
Browse files Browse the repository at this point in the history
* fix: restrict NFT minting only to voter

* fix: add missing `await`
  • Loading branch information
wa0x6e authored Oct 5, 2023
1 parent 1fad1df commit c169783
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 4 deletions.
22 changes: 22 additions & 0 deletions src/helpers/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ const VOTES_QUERY = gql`
}
`;

const VOTE_QUERY = gql`
query Votes($voter: String!, $proposalId: String!) {
votes(first: 1, where: { voter: $voter, proposal: $proposalId }) {
id
}
}
`;

const SPACE_QUERY = gql`
query Space($id: String) {
space(id: $id) {
Expand Down Expand Up @@ -143,6 +151,20 @@ export async function fetchVotes(
return votes;
}

export async function fetchVote(voter: string, proposalId: string) {
const {
data: { votes }
}: { data: { votes: Vote[] } } = await client.query({
query: VOTE_QUERY,
variables: {
voter,
proposalId
}
});

return votes[0];
}

export async function fetchSpace(id: string) {
const {
data: { space }
Expand Down
9 changes: 7 additions & 2 deletions src/lib/nftClaimer/mint.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { splitSignature } from '@ethersproject/bytes';
import { fetchProposal, Space } from '../../helpers/snapshot';
import { fetchProposal, Space, Proposal } from '../../helpers/snapshot';
import {
validateProposal,
getProposalContract,
signer,
numberizeProposalId,
validateMintInput,
mintingAllowed
mintingAllowed,
hasVoted
} from './utils';
import abi from './spaceCollectionImplementationAbi.json';
import { FormatTypes, Interface } from '@ethersproject/abi';
Expand Down Expand Up @@ -39,6 +40,10 @@ export default async function payload(input: {
throw new Error('Space has closed minting');
}

if (!(await hasVoted(params.recipient, proposal as Proposal))) {
throw new Error('Minting is open only for voters');
}

const message = {
proposer: params.proposalAuthor,
recipient: params.recipient,
Expand Down
7 changes: 6 additions & 1 deletion src/lib/nftClaimer/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Contract } from '@ethersproject/contracts';
import { getAddress, isAddress } from '@ethersproject/address';
import { BigNumber } from '@ethersproject/bignumber';
import { capture } from '@snapshot-labs/snapshot-sentry';
import type { Proposal, Space } from '../../helpers/snapshot';
import { fetchVote, type Proposal, type Space } from '../../helpers/snapshot';
import { fetchWithKeepAlive } from '../../helpers/utils';

const requiredEnvKeys = [
Expand Down Expand Up @@ -43,6 +43,11 @@ export async function mintingAllowed(space: Space) {
return (await getSpaceCollection(space.id)).enabled;
}

export async function hasVoted(address: string, proposal: Proposal) {
const vote = await fetchVote(address, proposal.id);
return vote !== undefined;
}

export async function validateSpace(address: string, space: Space | null) {
if (!space) {
throw new Error('RECORD_NOT_FOUND');
Expand Down
22 changes: 22 additions & 0 deletions test/integration/lib/nftClaimer/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Proposal } from '../../../../src/helpers/snapshot';
import { hasVoted } from '../../../../src/lib/nftClaimer/utils';

describe('nftClaimer/utils', () => {
describe('hasVoted()', () => {
it('returns true when the address has voted on the given proposal', () => {
expect(
hasVoted('0x96176C25803Ce4cF046aa74895646D8514Ea1611', {
id: 'QmPvbwguLfcVryzBRrbY4Pb9bCtxURagdv1XjhtFLf3wHj'
} as Proposal)
).resolves.toBe(true);
});

it('returns false when the address has not voted on the given proposal', () => {
expect(
hasVoted('0x96176C25803Ce4cF046aa74895646D8514Ea1611', {
id: '0xcf201ad7a32dcd399654c476093f079554dae429a13063f50d839e5621cd2e6e'
} as Proposal)
).resolves.toBe(false);
});
});
});
14 changes: 13 additions & 1 deletion test/unit/lib/nftClaimer/mint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const mockValidateProposal = jest.fn((proposal: any): void => {
const mockMintingAllowed = jest.fn((space: any): boolean => {
return true;
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const mockHasVoted = jest.fn((address: string, proposal: string): boolean => {
return true;
});
jest.mock('../../../../src/lib/nftClaimer/utils', () => {
// Require the original module to not be mocked...
const originalModule = jest.requireActual('../../../../src/lib/nftClaimer/utils');
Expand All @@ -41,7 +45,8 @@ jest.mock('../../../../src/lib/nftClaimer/utils', () => {
...originalModule,
getProposalContract: (id: string) => mockGetProposalContract(id),
validateProposal: (id: any) => mockValidateProposal(id),
mintingAllowed: (space: any) => mockMintingAllowed(space)
mintingAllowed: (space: any) => mockMintingAllowed(space),
hasVoted: (address: string, proposal: string) => mockHasVoted(address, proposal)
};
});

Expand Down Expand Up @@ -110,6 +115,13 @@ describe('nftClaimer', () => {
});
});

describe('when address has not voted on the proposal', () => {
it('throws an error', () => {
mockHasVoted.mockReturnValueOnce(false);
return expect(async () => await payload(input)).rejects.toThrow();
});
});

describe('when maxSupply has been reached', () => {
it.todo('throws an error');
});
Expand Down

0 comments on commit c169783

Please sign in to comment.