Sparkn

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

EIP-712 signatures can be re-used

Summary

The protocol implements the EIP-712 standard to prevent signature replay attacks. However there are no checks to determine if the signature has been used before. This could be exploitable in future versions of the protocol if additional functionality is added to the implementation contract.

This only applies to signatures on the same network as EIP-712 includes the chainId in the _buildDomainSeparator function to prevent cross-network exploits.

Vulnerability Details

In the Distributor contract the deployProxyAndDistributeBySignature function takes a signature as input to create a new proxy and distribute the funds accordingly.

The protocol in its current form is actually safe as deployProxyAndDistributeBySignature creates a new proxy address. Reusing the same signature will attempt to create the proxy at the same address resulting in a revert, as the address already exists.

Impact

A signature can be replayed numerous times resulting in potential loss of funds to the protocol and users.

POC

For the purpose of demonstration the deployProxyAndDistributeBySignature increments the signature count

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();
signatureCount++;
}

Foundry test confirm the same signature is replayed

contract SignatureTest is StdCheats, HelperContract {
address proxyAddress;
bytes32 salt;
function setUp() public {
// set up balances of each token belongs to each user
if (block.chainid == 31337) {
///deal ether
vm.deal(factoryAdmin, STARTING_USER_BALANCE);
vm.deal(sponsor, SMALL_STARTING_USER_BALANCE);
vm.deal(organizer, SMALL_STARTING_USER_BALANCE);
vm.deal(user1, SMALL_STARTING_USER_BALANCE);
vm.startPrank(tokenMinter);
// mint erc20 token
MockERC20(jpycv2Address).mint(sponsor, 300_000 ether); // 300k JPYCv2
MockERC20(jpycv2Address).mint(organizer, 300_000 ether); // 300k JPYCv2
vm.stopPrank();
}
// labels
vm.label(organizer, "organizer");
vm.label(sponsor, "sponsor");
vm.label(user1, "user1");
}
function createData() public view returns (bytes memory data) {
address[] memory tokens_ = new address[](1);
tokens_[0] = jpycv2Address;
address[] memory winners = new address[](1);
winners[0] = user1;
uint256[] memory percentages_ = new uint256[](1);
percentages_[0] = 9500;
data = abi.encodeWithSelector(Distributor.withdrawTokens.selector, jpycv2Address, user1);
}
modifier setUpContestForJasonAndSentJpycv2Token(address _organizer) {
vm.startPrank(factoryAdmin);
bytes32 randomId = keccak256(abi.encode("Jason", "001"));
proxyFactory.setContest(_organizer, randomId, block.timestamp + 8 days, address(distributor));
vm.stopPrank();
bytes32 salt = keccak256(abi.encode(_organizer, randomId, 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);
_;
}
function createSignatureByASigner(uint256 privateK) 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)
)
);
bytes32 randomId_ = keccak256(abi.encode("Jason", "001"));
bytes memory sendingData = createData();
bytes32 data = keccak256(abi.encode(randomId_, 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);
}
function testAuditIfAllConditionsMetThenSucceeds() public setUpContestForJasonAndSentJpycv2Token(TEST_SIGNER) {
// before
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 0 ether);
(bytes32 digest, bytes memory sendingData, bytes memory signature) = createSignatureByASigner(TEST_SIGNER_KEY);
assertEq(ECDSA.recover(digest, signature), TEST_SIGNER);
bytes32 randomId = keccak256(abi.encode("Jason", "001"));
vm.warp(8.01 days);
// signature is used
proxyFactory.deployProxyAndDistributeBySignature(
TEST_SIGNER, randomId, address(distributor), signature, sendingData
);
assertEq(proxyFactory.signatureCount(), 1);
// the same signature is used again
proxyFactory.deployProxyAndDistributeBySignature(
TEST_SIGNER, randomId, address(distributor), signature, sendingData
);
assertEq(proxyFactory.signatureCount(), 2);
}
}
├─ [28477] ProxyFactory::deployProxyAndDistributeBySignature(0x70997970C51812dc3A010C7d01b50e0d17dc79C8, 0xad40762e8c031b7ef93b899573e7257f7221bc68688c4c73bbfb58ce9c2e102d, Distributor: [0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0], 0xb05154aca6718ce1d0b62e0e74510783edefa2414098745d2d5a5885f6069ba71e0f56959d9a09af4829435666c91c8471d1f065e4fb782043539ce6a7b7bb7e1c, 0xa522ad2500000000000000000000000090193c961a926261b756d1e5bb255e67ff9498a1000000000000000000000000000000000000000000000000000000000000000e)
│ ├─ [3000] PRECOMPILE::ecrecover(0x17fb9fa263c03fedcbdf788ade230cdf2b814e7c87fce20a95e1e84c9c2c9d81, 28, 79750760364234442951185986804986539181243427277563769123971780682324023614375 [7.975e76], 13596485747284942090199456907236871085947436427723157321481595415290317355902 [1.359e76]) [staticcall]
│ │ └─ ← 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
│ └─ ← 0x0000000000000000000000000000000000000000
├─ [361] ProxyFactory::signatureCount() [staticcall]
│ └─ ← 1
├─ [6577] ProxyFactory::deployProxyAndDistributeBySignature(0x70997970C51812dc3A010C7d01b50e0d17dc79C8, 0xad40762e8c031b7ef93b899573e7257f7221bc68688c4c73bbfb58ce9c2e102d, Distributor: [0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0], 0xb05154aca6718ce1d0b62e0e74510783edefa2414098745d2d5a5885f6069ba71e0f56959d9a09af4829435666c91c8471d1f065e4fb782043539ce6a7b7bb7e1c, 0xa522ad2500000000000000000000000090193c961a926261b756d1e5bb255e67ff9498a1000000000000000000000000000000000000000000000000000000000000000e)
│ ├─ [3000] PRECOMPILE::ecrecover(0x17fb9fa263c03fedcbdf788ade230cdf2b814e7c87fce20a95e1e84c9c2c9d81, 28, 79750760364234442951185986804986539181243427277563769123971780682324023614375 [7.975e76], 13596485747284942090199456907236871085947436427723157321481595415290317355902 [1.359e76]) [staticcall]
│ │ └─ ← 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
│ └─ ← 0x0000000000000000000000000000000000000000
├─ [361] ProxyFactory::signatureCount() [staticcall]
│ └─ ← 2
└─ ← ()

Tools Used

Manual review, Foundry tests.

Recommendations

For upgraded versions of the protocol implement a nonce feature when generating EIP-712 signatures to ensure that the signature hasn’t been previously used.

Support

FAQs

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