Skip to content

Commit fdd80c9

Browse files
authored
Merge pull request #2 from kobigurk/fix/nullifier-exploit
merge kobigurk/fix: don't allow double-spending with a large nullifier
2 parents 4002962 + f6f5802 commit fdd80c9

File tree

2 files changed

+45
-2
lines changed

2 files changed

+45
-2
lines changed

contracts/PrivateAirdrop.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ contract PrivateAirdrop is Ownable {
2020

2121
mapping(bytes32 => bool) public nullifierSpent;
2222

23+
uint256 constant SNARK_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
24+
2325
constructor(
2426
IERC20 _airdropToken,
2527
uint _amountPerRedemption,
@@ -34,6 +36,7 @@ contract PrivateAirdrop is Ownable {
3436

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

3942
uint[] memory pubSignals = new uint[](3);
@@ -51,4 +54,4 @@ contract PrivateAirdrop is Ownable {
5154
function updateRoot(bytes32 newRoot) public onlyOwner {
5255
root = newRoot;
5356
}
54-
}
57+
}

test/PrivateAirdropIntegration.spec.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,53 @@ describe("PrivateAirdrop", async () => {
4747

4848
// Collect
4949
let keyHash = toHex(pedersenHash(key))
50+
5051
let execute = await (
5152
await airdrop.connect(redeemer).collectAirdrop(callData, keyHash)).wait()
5253
expect(execute.status).to.be.eq(1)
5354
let contractBalanceUpdated: BigNumber = await erc20.balanceOf(airdrop.address);
5455
expect(contractBalanceUpdated.toNumber()).to.be.eq(contractBalanceInit.toNumber() - NUM_ERC20_PER_REDEMPTION)
5556
let redeemerBalance: BigNumber = await erc20.balanceOf(redeemer.address);
5657
expect(redeemerBalance.toNumber()).to.be.eq(NUM_ERC20_PER_REDEMPTION)
58+
59+
})
60+
61+
it("cannot exploit using public inputs larger than the scalar field", async () => {
62+
// Deploy contracts
63+
let hexRoot = toHex(merkleTreeAndSource.merkleTree.root.val)
64+
let [universalOwnerSigner, erc20SupplyHolder, redeemer] = await ethers.getSigners();
65+
let {erc20, verifier, airdrop} =
66+
await deployContracts(
67+
universalOwnerSigner,
68+
erc20SupplyHolder.address,
69+
hexRoot);
70+
71+
// Transfer airdroppable tokens to contract
72+
await erc20.connect(erc20SupplyHolder).transfer(airdrop.address, NUM_ERC20_TO_DISTRIBUTE);
73+
let contractBalanceInit: BigNumber = await erc20.balanceOf(airdrop.address);
74+
expect(contractBalanceInit.toNumber()).to.be.eq(NUM_ERC20_TO_DISTRIBUTE);
75+
76+
let merkleTree = merkleTreeAndSource.merkleTree;
77+
78+
// Generate proof
79+
let leafIndex = 7;
80+
let key = merkleTreeAndSource.leafNullifiers[leafIndex];
81+
let secret = merkleTreeAndSource.leafSecrets[leafIndex];
82+
let callData = await generateProofCallData(merkleTree, key, secret, redeemer.address, WASM_BUFF, ZKEY_BUFF);
83+
84+
// Collect
85+
let keyHash = toHex(pedersenHash(key))
86+
let keyHashTwo = toHex(BigInt(keyHash) + BigInt('21888242871839275222246405745257275088548364400416034343698204186575808495617'))
87+
88+
let execute = await (
89+
await airdrop.connect(redeemer).collectAirdrop(callData, keyHash)).wait()
90+
expect(execute.status).to.be.eq(1)
91+
let contractBalanceUpdated: BigNumber = await erc20.balanceOf(airdrop.address);
92+
expect(contractBalanceUpdated.toNumber()).to.be.eq(contractBalanceInit.toNumber() - NUM_ERC20_PER_REDEMPTION)
93+
let redeemerBalance: BigNumber = await erc20.balanceOf(redeemer.address);
94+
expect(redeemerBalance.toNumber()).to.be.eq(NUM_ERC20_PER_REDEMPTION)
95+
await expect(airdrop.connect(redeemer).collectAirdrop(callData, keyHashTwo)).to.be.revertedWith("Nullifier is not within the field")
96+
5797
})
5898

5999
it("cannot be front-run by another party", async () => {
@@ -176,4 +216,4 @@ async function deployContracts(
176216
root
177217
)) as PrivateAirdrop
178218
return {erc20, verifier, airdrop}
179-
}
219+
}

0 commit comments

Comments
 (0)