Rewards sent accidentally to the distributor contract cannot be withdrawn and will be stuck forever.
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.
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:
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 {
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.deal(user2, SMALL_STARTING_USER_BALANCE);
vm.deal(user3, SMALL_STARTING_USER_BALANCE);
vm.deal(TEST_SIGNER, SMALL_STARTING_USER_BALANCE);
vm.startPrank(tokenMinter);
MockERC20(jpycv1Address).mint(sponsor, 100_000 ether);
MockERC20(jpycv2Address).mint(sponsor, 300_000 ether);
MockERC20(usdcAddress).mint(sponsor, 10_000 ether);
MockERC20(jpycv1Address).mint(organizer, 100_000 ether);
MockERC20(jpycv2Address).mint(organizer, 300_000 ether);
MockERC20(usdcAddress).mint(organizer, 10_000 ether);
MockERC20(jpycv1Address).mint(TEST_SIGNER, 100_000 ether);
MockERC20(jpycv2Address).mint(TEST_SIGNER, 300_000 ether);
MockERC20(usdcAddress).mint(TEST_SIGNER, 10_000 ether);
vm.stopPrank();
}
vm.label(organizer, "organizer");
vm.label(sponsor, "sponsor");
vm.label(supporter, "supporter");
vm.label(user1, "user1");
vm.label(user2, "user2");
vm.label(user3, "user3");
}
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)
{
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 0 ether);
bytes32 randomId_ = keccak256(abi.encode("Jason", "001"));
bytes32 salt_ = keccak256(
abi.encode(organizer, randomId_, address(distributor))
);
bytes memory data = createData();
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
);
}
}
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.
Funds will be stuck if sent to the wrong contract.
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.