Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: don't allow double-spending with a large nullifier #2

Merged
merged 1 commit into from
Jan 26, 2022
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
5 changes: 4 additions & 1 deletion contracts/PrivateAirdrop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ contract PrivateAirdrop is Ownable {

mapping(bytes32 => bool) public nullifierSpent;

uint256 constant SNARK_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;

constructor(
IERC20 _airdropToken,
uint _amountPerRedemption,
Expand All @@ -34,6 +36,7 @@ contract PrivateAirdrop is Ownable {

/// @notice verifies the proof, collects the airdrop if valid, and prevents this proof from working again.
function collectAirdrop(bytes calldata proof, bytes32 nullifierHash) public {
require(uint256(nullifierHash) < SNARK_FIELD ,"Nullifier is not within the field");
require(!nullifierSpent[nullifierHash], "Airdrop already redeemed");

uint[] memory pubSignals = new uint[](3);
Expand All @@ -51,4 +54,4 @@ contract PrivateAirdrop is Ownable {
function updateRoot(bytes32 newRoot) public onlyOwner {
root = newRoot;
}
}
}
42 changes: 41 additions & 1 deletion test/PrivateAirdropIntegration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,53 @@ describe("PrivateAirdrop", async () => {

// Collect
let keyHash = toHex(pedersenHash(key))

let execute = await (
await airdrop.connect(redeemer).collectAirdrop(callData, keyHash)).wait()
expect(execute.status).to.be.eq(1)
let contractBalanceUpdated: BigNumber = await erc20.balanceOf(airdrop.address);
expect(contractBalanceUpdated.toNumber()).to.be.eq(contractBalanceInit.toNumber() - NUM_ERC20_PER_REDEMPTION)
let redeemerBalance: BigNumber = await erc20.balanceOf(redeemer.address);
expect(redeemerBalance.toNumber()).to.be.eq(NUM_ERC20_PER_REDEMPTION)

})

it("cannot exploit using public inputs larger than the scalar field", async () => {
// Deploy contracts
let hexRoot = toHex(merkleTreeAndSource.merkleTree.root.val)
let [universalOwnerSigner, erc20SupplyHolder, redeemer] = await ethers.getSigners();
let {erc20, verifier, airdrop} =
await deployContracts(
universalOwnerSigner,
erc20SupplyHolder.address,
hexRoot);

// Transfer airdroppable tokens to contract
await erc20.connect(erc20SupplyHolder).transfer(airdrop.address, NUM_ERC20_TO_DISTRIBUTE);
let contractBalanceInit: BigNumber = await erc20.balanceOf(airdrop.address);
expect(contractBalanceInit.toNumber()).to.be.eq(NUM_ERC20_TO_DISTRIBUTE);

let merkleTree = merkleTreeAndSource.merkleTree;

// Generate proof
let leafIndex = 7;
let key = merkleTreeAndSource.leafNullifiers[leafIndex];
let secret = merkleTreeAndSource.leafSecrets[leafIndex];
let callData = await generateProofCallData(merkleTree, key, secret, redeemer.address, WASM_BUFF, ZKEY_BUFF);

// Collect
let keyHash = toHex(pedersenHash(key))
let keyHashTwo = toHex(BigInt(keyHash) + BigInt('21888242871839275222246405745257275088548364400416034343698204186575808495617'))

let execute = await (
await airdrop.connect(redeemer).collectAirdrop(callData, keyHash)).wait()
expect(execute.status).to.be.eq(1)
let contractBalanceUpdated: BigNumber = await erc20.balanceOf(airdrop.address);
expect(contractBalanceUpdated.toNumber()).to.be.eq(contractBalanceInit.toNumber() - NUM_ERC20_PER_REDEMPTION)
let redeemerBalance: BigNumber = await erc20.balanceOf(redeemer.address);
expect(redeemerBalance.toNumber()).to.be.eq(NUM_ERC20_PER_REDEMPTION)
await expect(airdrop.connect(redeemer).collectAirdrop(callData, keyHashTwo)).to.be.revertedWith("Nullifier is not within the field")

})

it("cannot be front-run by another party", async () => {
Expand Down Expand Up @@ -176,4 +216,4 @@ async function deployContracts(
root
)) as PrivateAirdrop
return {erc20, verifier, airdrop}
}
}