Sparkn

CodeFox Inc.
DeFiFoundryProxy
15,000 USDC
View results
Submission Details
Severity: high
Valid

The same signature can be used in different `distribution` implementation causing that the caller who owns the signature, can distribute on unauthorized implementations

Summary

The same signature can be used in different distribute implementations causing that the caller who owns the signature, to distribute on unauthorized implementations.

Vulnerability Details

The ProxyFactory::setContest() function helps to configure a closeTime to specific organizer, contestId and implementation.

File: ProxyFactory.sol
105: function setContest(address organizer, bytes32 contestId, uint256 closeTime, address implementation)
106: public
107: onlyOwner
...
...
113: bytes32 salt = _calculateSalt(organizer, contestId, implementation);
114: if (saltToCloseTime[salt] != 0) revert ProxyFactory__ContestIsAlreadyRegistered();
115: saltToCloseTime[salt] = closeTime;

The caller who owns the signature, can distributes to winners using the deployProxyAndDistributeBySignature() function. The problem is that the hash in the code line (#159) does not consider the implementation parameter.

File: ProxyFactory.sol
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: }

For some reason, there could be a different distribution implementation to the same contestId. Then the caller who owns the signature can distribute even if the organizer does not authorize a signature to the new implementation.

I created a test where the caller who owns a signature can distribute to new distribute implementation using the same signature. Test steps:

  1. Owner setContest using the implementation address(distributor)

  2. Organizer creates a signature.

  3. Caller distributes prizes using the signature.

  4. For some reason there is a new distributor implementation. The Owner set the new distributor for the same contestId.

  5. The caller can distribute prizes using the same signature created in the step 2 in different distributor implementation.

// test/integration/ProxyFactoryTest.t.sol:ProxyFactoryTest
// $ forge test --match-test "testSignatureCanBeUsedToNewImplementation" -vvv
//
function testSignatureCanBeUsedToNewImplementation() public {
address organizer = TEST_SIGNER;
bytes32 contestId = keccak256(abi.encode("Jason", "001"));
//
// 1. Owner setContest using address(distributor)
vm.startPrank(factoryAdmin);
proxyFactory.setContest(organizer, contestId, block.timestamp + 8 days, address(distributor));
vm.stopPrank();
bytes32 salt = keccak256(abi.encode(organizer, contestId, address(distributor)));
address proxyAddress = proxyFactory.getProxyAddress(salt, address(distributor));
vm.startPrank(sponsor);
MockERC20(jpycv2Address).transfer(proxyAddress, 10000 ether);
vm.stopPrank();
assertEq(MockERC20(jpycv2Address).balanceOf(proxyAddress), 10000 ether);
// before
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 0 ether);
//
// 2. Organizer creates a signature
(bytes32 digest, bytes memory sendingData, bytes memory signature) = createSignatureByASigner(TEST_SIGNER_KEY);
assertEq(ECDSA.recover(digest, signature), TEST_SIGNER);
vm.warp(8.01 days);
//
// 3. Caller distributes prizes using the signature
proxyFactory.deployProxyAndDistributeBySignature(
TEST_SIGNER, contestId, address(distributor), signature, sendingData
);
// after
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 9500 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 500 ether);
//
// 4. For some reason there is a new distributor implementation.
// The Owner set the new distributor for the same contestId
Distributor new_distributor = new Distributor(address(proxyFactory), stadiumAddress);
vm.startPrank(factoryAdmin);
proxyFactory.setContest(organizer, contestId, block.timestamp + 8 days, address(new_distributor));
vm.stopPrank();
bytes32 newDistributorSalt = keccak256(abi.encode(organizer, contestId, address(new_distributor)));
address proxyNewDistributorAddress = proxyFactory.getProxyAddress(newDistributorSalt, address(new_distributor));
vm.startPrank(sponsor);
MockERC20(jpycv2Address).transfer(proxyNewDistributorAddress, 10000 ether);
vm.stopPrank();
//
// 5. The caller can distribute prizes using the same signature in different distributor implementation
vm.warp(20 days);
proxyFactory.deployProxyAndDistributeBySignature(
TEST_SIGNER, contestId, address(new_distributor), signature, sendingData
);
}

Impact

The caller who owns the signature, can distribute the prizes for a new distribution implementation using the same signature which was created for an old implementation.
The organizer must create a new signature if there is a new implementation for the same contestId. The authorized signature is for one distribution implementation not for the future distribution implementations.

Tools used

Manual review

Recommendations

Include the distribution implementation in the signature hash.

function deployProxyAndDistributeBySignature(
address organizer,
bytes32 contestId,
address implementation,
bytes calldata signature,
bytes calldata data
) public returns (address) {
-- bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, data)));
++ bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, implementation, data)));

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.