Sparkn

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

Rewards sent accidentally to the distributor contract will be stuck forever

Summary

Rewards sent accidentally to the distributor contract cannot be withdrawn and will be stuck forever.

Vulnerability Details

In ProxyFactory.sol, the factory owner can retrieve tokens from the proxy contract in case the sponsor or anyone accidentally deposits into the proxy contract after the rewards are distributed already. The test file also shows the owner successfully obtaining back the tokens from the proxy contract.

function distributeByOwner(
address proxy,
address organizer,
bytes32 contestId,
address implementation,
bytes calldata data
) public onlyOwner {
if (proxy == address(0)) revert ProxyFactory__ProxyAddressCannotBeZero();
bytes32 salt = _calculateSalt(organizer, contestId, implementation);
if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
// distribute only when it exists and expired
if (saltToCloseTime[salt] + EXPIRATION_TIME > block.timestamp) revert ProxyFactory__ContestIsNotExpired();
_distribute(proxy, data);
}
// sponsor send token to proxy by mistake
vm.startPrank(sponsor);
MockERC20(jpycv2Address).transfer(proxyAddress, 10000 ether);
vm.stopPrank();
bytes memory dataToSendToAdmin = createDataToSendToAdmin();
vm.startPrank(factoryAdmin);
proxyFactory.distributeByOwner(
calculatedProxyAddress, organizer, randomId_, address(distributor), dataToSendToAdmin
);
vm.stopPrank();

However, if the sponsor or anyone accidentally deposits into the Distributor contract instead, there is no way for the funds to be retrieved.

Attached is a test suite that demonstrates funds being stuck. A short summary of the test:

  1. Factory Admin computes the address of the proxy contract with setContest()

  2. Sponsor deposits 10,000 JPYC into the proxy address

  3. Sponsor also deposits 10,000 JPYC into the distributor address

  4. The contests started and ended, the contest owner is nowhere to be found so the admin calls deployProxyAndDistributeByOwner()

  5. There are two winners, user1 and user2. User1 wins 9,000 and user2 wins 500. The stadium address wins 500.

  6. 10,000 is stuck in the distributor address.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import {MockERC20} from "../mock/MockERC20.sol";
import {ECDSA} from "openzeppelin/utils/cryptography/ECDSA.sol";
import {Test, console} from "forge-std/Test.sol";
import {StdCheats} from "forge-std/StdCheats.sol";
import {ProxyFactory} from "../../src/ProxyFactory.sol";
import {Proxy} from "../../src/Proxy.sol";
import {Distributor} from "../../src/Distributor.sol";
import {HelperContract} from "./HelperContract.t.sol";
contract Hack is StdCheats, HelperContract {
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.deal(user2, SMALL_STARTING_USER_BALANCE);
vm.deal(user3, SMALL_STARTING_USER_BALANCE);
vm.deal(TEST_SIGNER, SMALL_STARTING_USER_BALANCE);
// mint erc20 token
vm.startPrank(tokenMinter);
MockERC20(jpycv1Address).mint(sponsor, 100_000 ether); // 100k JPYCv1
MockERC20(jpycv2Address).mint(sponsor, 300_000 ether); // 300k JPYCv2
MockERC20(usdcAddress).mint(sponsor, 10_000 ether); // 10k USDC
MockERC20(jpycv1Address).mint(organizer, 100_000 ether); // 100k JPYCv1
MockERC20(jpycv2Address).mint(organizer, 300_000 ether); // 300k JPYCv2
MockERC20(usdcAddress).mint(organizer, 10_000 ether); // 10k USDC
MockERC20(jpycv1Address).mint(TEST_SIGNER, 100_000 ether); // 100k JPYCv1
MockERC20(jpycv2Address).mint(TEST_SIGNER, 300_000 ether); // 300k JPYCv2
MockERC20(usdcAddress).mint(TEST_SIGNER, 10_000 ether); // 10k USDC
vm.stopPrank();
}
// labels
vm.label(organizer, "organizer");
vm.label(sponsor, "sponsor");
vm.label(supporter, "supporter");
vm.label(user1, "user1");
vm.label(user2, "user2");
vm.label(user3, "user3");
}
////////////////
// test setup //
////////////////
///////////////////////
// Modifier for test //
///////////////////////
// Set contest for `Jason`, `001` and sent JPYC v2 token to the
// undeployed proxy contract address and then check the balance
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);
MockERC20(jpycv2Address).transfer(address(distributor), 10000 ether);
vm.stopPrank();
emit log_named_decimal_uint(
"Balance of Proxy address",
MockERC20(jpycv2Address).balanceOf(address(proxyAddress)),
18
);
emit log_named_decimal_uint(
"Balance of Distributor address",
MockERC20(jpycv2Address).balanceOf(address(distributor)),
18
);
assertEq(MockERC20(jpycv2Address).balanceOf(proxyAddress), 10000 ether);
_;
}
modifier setUpContestForTerryAndSentJpycv2Token(address _organizer) {
vm.startPrank(factoryAdmin);
bytes32 randomId = keccak256(abi.encode("Terry", "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);
MockERC20(jpycv2Address).transfer(address(distributor), 10000 ether);
vm.stopPrank();
emit log_named_decimal_uint(
"Balance of Proxy address",
MockERC20(jpycv2Address).balanceOf(address(proxyAddress)),
18
);
emit log_named_decimal_uint(
"Balance of Distributor address",
MockERC20(jpycv2Address).balanceOf(address(distributor)),
18
);
console.log("THE ORGANIZER ADDRESS IS:", address(_organizer));
console.log("THE DISTRIBUTOR ADDRESS IS:", address(distributor));
console.log("THE PROXY ADDRESS IS:", proxyAddress);
assertEq(MockERC20(jpycv2Address).balanceOf(proxyAddress), 10000 ether);
_;
}
function createData() public view returns (bytes memory data) {
address[] memory tokens_ = new address[](1);
tokens_[0] = jpycv2Address;
address[] memory winners = new address[](2);
winners[0] = user1;
winners[1] = user2;
uint256[] memory percentages_ = new uint256[](2);
percentages_[0] = 9000;
percentages_[1] = 500;
data = abi.encodeWithSelector(
Distributor.distribute.selector,
jpycv2Address,
winners,
percentages_,
""
);
}
function testDistributorAmountStuckWhenDistributeByOwner()
public
setUpContestForJasonAndSentJpycv2Token(organizer)
{
// before
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 0 ether);
// prepare for data
bytes32 randomId_ = keccak256(abi.encode("Jason", "001"));
bytes32 salt_ = keccak256(
abi.encode(organizer, randomId_, address(distributor))
);
bytes memory data = createData();
// calculate proxy address
address calculatedProxyAddress = proxyFactory.getProxyAddress(
salt_,
address(distributor)
);
vm.warp(16 days);
vm.startPrank(factoryAdmin);
address proxyAddress = proxyFactory.deployProxyAndDistributeByOwner(
organizer,
randomId_,
address(distributor),
data
);
vm.stopPrank();
emit log_named_decimal_uint(
"Balance of user1 address",
MockERC20(jpycv2Address).balanceOf(address(user1)),
18
);
emit log_named_decimal_uint(
"Balance of stadium address ",
MockERC20(jpycv2Address).balanceOf(address(stadiumAddress)),
18
);
emit log_named_decimal_uint(
"Balance of user2 address",
MockERC20(jpycv2Address).balanceOf(address(user2)),
18
);
assertEq(proxyAddress, calculatedProxyAddress);
emit log_named_decimal_uint(
"Balance of Proxy address",
MockERC20(jpycv2Address).balanceOf(address(proxyAddress)),
18
);
emit log_named_decimal_uint(
"Balance of Distributor address",
MockERC20(jpycv2Address).balanceOf(address(distributor)),
18
);
}
}
Balance of Proxy address: 10000.000000000000000000
Balance of Distributor address: 10000.000000000000000000
Balance of user1 address: 9000.000000000000000000
Balance of stadium address : 500.000000000000000000
Balance of user2 address: 500.000000000000000000
Balance of Proxy address: 0.000000000000000000
Balance of Distributor address: 10000.000000000000000000

It is possible that the sponsor sends tokens to the wrong address (aka the distributor address) because they might not know the difference between the proxy and distributor. The distributor contract should have a failsafe measure to return any random tokens stuck in the contract.

Impact

Funds will be stuck if sent to the wrong contract.

Tools Used

Foundry

Recommendations

Recommend having a failsafe function to withdraw any funds in the distributor contract and even the proxy factory contract, since only the proxy contract has it currently. It is simply a preventive measure and it wouldn't induce any sort of centralization issues.

Support

FAQs

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