Sparkn

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

Automated Findings

Summary

Medium Risk Issues

Issue Instances
[M‑01] The owner is a single point of failure and a centralization risk 3
[M‑02] abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256() 1

Total: 4 instances over 2 issues

Low Risk Issues

Issue Instances
[L‑01] Use Ownable2Step rather than Ownable 1
[L‑02] Missing checks for address(0x0) in the constructor 1
[L‑03] Execution at deadlines should be allowed 5
[L‑04] External calls in an un-bounded loop may result in a DOS 1
[L‑05] Loss of precision 1
[L‑06] Unsafe downcast 1
[L‑07] Avoid double casting 3

Total: 13 instances over 7 issues

Non-critical Issues

Issue Instances
[N‑01] Variable names that consist of all capital letters should be reserved for constant/immutable variables 4
[N‑02] Constants in comparisons should appear on the left side 7
[N‑03] Consider disabling renounceOwnership() 1
[N‑04] public functions not called by the contract should be declared external instead 6
[N‑05] Function ordering does not follow the Solidity style guide 1
[N‑06] Large multiples of ten should use scientific notation (e.g. 1e6) rather than decimal literals (e.g. 1000000), for readability 2
[N‑07] Contract does not follow the Solidity style guide's suggested layout ordering 1
[N‑08] NatSpec @param is missing 4
[N‑09] NatSpec @return argument is missing 2
[N‑10] Lines are too long 2
[N‑11] Typos 3
[N‑12] Non-external/public variable names should begin with an underscore 5
[N‑13] Constants should be defined rather than using magic numbers 2
[N‑14] Functions not used internally could be marked external 6
[N‑15] Consider using named mappings 2

Total: 48 instances over 15 issues

Gas Optimizations

Gas totals use lower bounds of ranges and count two iterations of each for-loop. All values above are runtime, not deployment, values; deployment values are listed in the individual issue descriptions. The table above as well as its gas numbers do not include any of the excluded findings.

Issue Instances Total Gas Saved
[G‑01] Reduce gas usage by moving to Solidity 0.8.19 or later 3 -
[G‑02] Use assembly to check for address(0) 7 -
[G‑03] Use assembly to emit events, in order to save gass 3 114
[G‑04] Using bools for storage incurs overhead 1 1100
[G‑05] Cache array length outside of loop 1 -
[G‑06] The result of function calls should be cached rather than re-calling the function 1 -
[G‑07] internal functions only called once can be inlined to save gas 3 60
[G‑08] <array>.length should not be looked up in every loop of a for-loop 1 3
[G‑09] Not using the named return variables anywhere in the function is confusing 5 -
[G‑10] Constructors can be marked payable 3 63
[G‑11] Functions guaranteed to revert when called by normal users can be marked payable 3 63
[G‑12] <x> += <y> costs more gas than <x> = <x> + <y> for state variables 1 113
[G‑13] ++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too) 1 5
[G‑14] Using private rather than public for constants, saves gas 2 -
[G‑15] Use a more recent version of solidity 3 -

Total: 38 instances over 15 issues with 1521 gas saved

Medium Risk Issues

[M‑01] The owner is a single point of failure and a centralization risk

Having a single EOA as the only owner of contracts is a large centralization risk and a single point of failure. A single private key may be taken in a hack, or the sole holder of the key may become unable to retrieve the key when necessary. Consider changing to a multi-signature setup, or having a role-based authorization model.

There are 3 instances of this issue:

File: src/ProxyFactory.sol
105 function setContest(address organizer, bytes32 contestId, uint256 closeTime, address implementation)
106 public
107 onlyOwner
108 {
179 function deployProxyAndDistributeByOwner(
180 address organizer,
181 bytes32 contestId,
182 address implementation,
183 bytes calldata data
184 ) public onlyOwner returns (address) {
205 function distributeByOwner(
206 address proxy,
207 address organizer,
208 bytes32 contestId,
209 address implementation,
210 bytes calldata data
211 ) public onlyOwner {

GitHub: [105, 179, 205]

[M‑02] abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()

Use abi.encode() instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. abi.encodePacked(0x123,0x456) => 0x123456 => abi.encodePacked(0x1,0x23456), but abi.encode(0x123,0x456) => 0x0...1230...456). “Unless there is a compelling reason, abi.encode should be preferred”. If there is only one argument to abi.encodePacked() it can often be cast to bytes() or bytes32() instead.

There is one instance of this issue:

File: src/ProxyFactory.sol
227 bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(code)));

GitHub: [227]

Low Risk Issues

[L‑01] Use Ownable2Step rather than Ownable

Ownable2Step and Ownable2StepUpgradeable prevent the contract ownership from mistakenly being transferred to an address that cannot handle it (e.g. due to a typo in the address), by requiring that the recipient of the owner permissions actively accept via a contract call of its own.

There is one instance of this issue:

File: src/ProxyFactory.sol
37 contract ProxyFactory is Ownable, EIP712 {

GitHub: [37]

[L‑02] Missing checks for address(0x0) in the constructor

There is one instance of this issue:

see instances
File: src/Proxy.sol
45 _implementation = implementation;

GitHub: [45]

[L‑03] Execution at deadlines should be allowed

The condition may be wrong in these cases, as when block.timestamp is equal to the compared > or < variable these blocks will not be executed.

There are 5 instances of this issue:

File: src/ProxyFactory.sol
110 if (closeTime > block.timestamp + MAX_CONTEST_PERIOD || closeTime < block.timestamp) {
111 revert ProxyFactory__CloseTimeNotInRange();
112 }
134 if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
163 if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
187 if (saltToCloseTime[salt] + EXPIRATION_TIME > block.timestamp) revert ProxyFactory__ContestIsNotExpired();
216 if (saltToCloseTime[salt] + EXPIRATION_TIME > block.timestamp) revert ProxyFactory__ContestIsNotExpired();

GitHub: [110, 134, 163, 187, 216]

[L‑04] External calls in an un-bounded loop may result in a DOS

Consider limiting the number of iterations in for-loops that make external calls

There is one instance of this issue:

File: src/Distributor.sol
147 erc20.safeTransfer(winners[i], amount);

GitHub: [147]

[L‑05] Loss of precision

Division by large numbers may result in the result being zero, due to solidity not supporting fractions. Consider requiring a minimum amount for the numerator to ensure that it is always larger than the denominator

There is one instance of this issue:

see instances
File: src/Distributor.sol
146 uint256 amount = totalAmount * percentages[i] / BASIS_POINTS;

GitHub: [146]

[L‑06] Unsafe downcast

When a type is downcast to a smaller type, the higher order bits are truncated, effectively applying a modulo to the original value. Without any other checks, this wrapping will lead to unexpected behavior and bugs

There is one instance of this issue:

File: src/ProxyFactory.sol
228 proxy = address(uint160(uint256(hash)));

GitHub: [228]

[L‑07] Avoid double casting

Consider refactoring the following code, as double casting may introduce unexpected truncations and/or rounding issues.
Furthermore, double type casting can make the code less readable and harder to maintain, increasing the likelihood of errors and misunderstandings during development and debugging.

There are 3 instances of this issue:

see instances
File: src/ProxyFactory.sol
226 bytes memory code = abi.encodePacked(type(Proxy).creationCode, uint256(uint160(implementation)));
228 proxy = address(uint160(uint256(hash)));

GitHub: [226, 228, 228]

Non-critical Issues

[N‑01] Variable names that consist of all capital letters should be reserved for constant/immutable variables

If the variable needs to be different based on which class it comes from, a view/pure function should be used instead (e.g. like this).

There are 4 instances of this issue:

File: src/Distributor.sol
186 returns (address _FACTORY_ADDRESS, address _STADIUM_ADDRESS, uint256 _COMMISSION_FEE, uint8 _VERSION)

GitHub: [186, 186, 186, 186]

[N‑02] Constants in comparisons should appear on the left side

Doing so will prevent typo bugs

There are 7 instances of this issue:

see instances
File: src/Distributor.sol
125 if (winners.length == 0 || winners.length != percentages.length) revert Distributor__MismatchedArrays();
142 if (totalAmount == 0) revert Distributor__NoTokenToDistribute();

GitHub: [125, 142]

File: src/ProxyFactory.sol
82 if (_whitelistedTokens.length == 0) revert ProxyFactory__NoEmptyArray();
132 if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
162 if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
186 if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
214 if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();

GitHub: [82, 132, 162, 186, 214]

[N‑03] Consider disabling renounceOwnership()

If the plan for your project does not include eventually giving up all ownership control, consider overwriting OpenZeppelin's Ownable's renounceOwnership() function in order to disable it.

There is one instance of this issue:

File: src/ProxyFactory.sol
37 contract ProxyFactory is Ownable, EIP712 {

GitHub: [37]

[N‑04] public functions not called by the contract should be declared external instead

Contracts are allowed to override their parents' functions and change the visibility from external to public.

There are 6 instances of this issue:

File: src/ProxyFactory.sol
105 function setContest(address organizer, bytes32 contestId, uint256 closeTime, address implementation)
106 public
107 onlyOwner
108 {
127 function deployProxyAndDistribute(bytes32 contestId, address implementation, bytes calldata data)
128 public
129 returns (address)
130 {
152 function deployProxyAndDistributeBySignature(
153 address organizer,
154 bytes32 contestId,
155 address implementation,
156 bytes calldata signature,
157 bytes calldata data
158 ) public returns (address) {
179 function deployProxyAndDistributeByOwner(
180 address organizer,
181 bytes32 contestId,
182 address implementation,
183 bytes calldata data
184 ) public onlyOwner returns (address) {
205 function distributeByOwner(
206 address proxy,
207 address organizer,
208 bytes32 contestId,
209 address implementation,
210 bytes calldata data
211 ) public onlyOwner {
225 function getProxyAddress(bytes32 salt, address implementation) public view returns (address proxy) {

GitHub: [105, 127, 152, 179, 205, 225]

[N‑05] Function ordering does not follow the Solidity style guide

According to the Solidity style guide, functions should be laid out in the following order :constructor(), receive(), fallback(), external, public, internal, private, but the cases below do not follow this pattern.

There is one instance of this issue:

see instances
File: src/Distributor.sol
/// @custom:audit Correct order: getConstants -> _isWhiteListed
38 contract Distributor {

GitHub: [38]

[N‑06] Large multiples of ten should use scientific notation (e.g. 1e6) rather than decimal literals (e.g. 1000000), for readability

There are 2 instances of this issue:

see instances
File: src/Distributor.sol
61 uint256 private constant BASIS_POINTS = 10000;
135 if (totalPercentage != (10000 - COMMISSION_FEE)) {

GitHub: [61, 135]

[N‑07] Contract does not follow the Solidity style guide's suggested layout ordering

The style guide says that, within a contract, the ordering should be 1) Type declarations, 2) State variables, 3) Events, 4) Modifiers, and 5) Functions, but the contract(s) below do not follow this ordering

There is one instance of this issue:

see instances
File: src/ProxyFactory.sol
/// @custom:audit Correct order: EXPIRATION_TIME -> Distributed
37 contract ProxyFactory is Ownable, EIP712 {

GitHub: [37]

[N‑08] NatSpec @param is missing

There are 4 instances of this issue:

see instances
File: src/Distributor.sol
/// @custom:audit Missing '@param factoryAddress'
/// @custom:audit Missing '@param stadiumAddress'
69 /// @dev initiate the contract with factory address and other key addresses, fee rate
70 constructor(
/// @custom:audit Missing '@param data'
85 /**
86 * @notice Distribute token to winners according to the percentages
87 * @dev Only factory contract can call this function
88 * @param token The token address to distribute
89 * @param winners The addresses array of winners
90 * @param percentages The percentages array of winners
91 */
92 function distribute(address token, address[] memory winners, uint256[] memory percentages, bytes memory data)

GitHub: [69, 69, 85]

File: src/Proxy.sol
/// @custom:audit Missing '@param implementation'
42 /// @notice constructor
43 /// @dev set implementation address
44 constructor(address implementation) {

GitHub: [42]

[N‑09] NatSpec @return argument is missing

There are 2 instances of this issue:

see instances
File: src/ProxyFactory.sol
/// @custom:audit Missing '@return'
234 /// @dev Deploy proxy and return the proxy address
235 /// @dev This is an internal function
236 /// @param organizer The contest organizer
237 /// @param contestId The contest id
238 /// @param implementation The implementation address
239 function _deployProxy(address organizer, bytes32 contestId, address implementation) internal returns (address) {
/// @custom:audit Missing '@return'
255 /// @dev Calculate salt using contest organizer address and contestId, implementation address
256 /// @dev This is an internal function
257 /// @param organizer The contest organizer
258 /// @param contestId The contest id
259 /// @param implementation The implementation address
260 function _calculateSalt(address organizer, bytes32 contestId, address implementation)
261 internal
262 pure
263 returns (bytes32)
264 {

GitHub: [234, 255]

[N‑10] Lines are too long

Usually lines in source code are limited to 80 characters. Today's screens are much larger so it's reasonable to stretch this in some cases. The solidity style guide recommends a maximum line length of 120 characters, so the lines below should be split when they reach that length.

There are 2 instances of this issue:

see instances
File: src/Distributor.sol
33 * @notice This contract is used as the implementation of proxy contracts to distribute ERC20 token(e.g. JPYC) to winners
114 * @param data The data to be logged. It is supposed to be used for showing the realation bbetween winners and proposals.

GitHub: [33, 114]

[N‑11] Typos

There are 3 instances of this issue:

File: src/Distributor.sol
/// @custom:audit suppsoed
30 /**
31 * @title Distributor contract
32 * @notice General ERC20 stable coin tokens, e.g. JPYC, USDC, USDT, DAI, etc, are suppsoed to be used in SPARKN.
33 * @notice This contract is used as the implementation of proxy contracts to distribute ERC20 token(e.g. JPYC) to winners
34 * @dev The main logic of prize token distribution sits in this contract waiting to be called by factory contract
35 * @dev Although the contract is immutable after deployment, If we want to upgrade the implementation contract
36 * we can deploy a new one and change the implementation address of proxy contract.
37 */
/// @custom:audit realation, bbetween
104 /**
105 * @notice An internal function to distribute JPYC to winners
106 * @dev Main logic of distribution is implemented here. The length of winners and percentages must be the same
107 * The token address must be one of the whitelisted tokens
108 * The winners and percentages array are supposed not to be so long, so the loop can stay unbounded
109 * The total percentage must be correct. It must be (100 - COMMITION_FEE).
110 * Finally send the remained token(fee) to STADIUM_ADDRESS with no dust in the contract
111 * @param token The token address
112 * @param winners The addresses of winners
113 * @param percentages The percentages of winners
114 * @param data The data to be logged. It is supposed to be used for showing the realation bbetween winners and proposals.
115 */

GitHub: [30, 104]

File: src/ProxyFactory.sol
/// @custom:audit uess
140 /**
141 * @notice deploy proxy contract and distribute prize on behalf of organizer
142 * @dev the caller can only control his own contest
143 * @dev It uess EIP712 to verify the signature to avoid replay attacks
144 * @dev front run is allowed because it will only help the tx sender
145 * @param organizer The organizer of the contest
146 * @param contestId The contest id
147 * @param implementation The implementation address
148 * @param signature The signature from organizer
149 * @param data The prize distribution data
150 * @return proxy The proxy address
151 */

GitHub: [140]

[N‑12] Non-external/public variable names should begin with an underscore

According to the Solidity Style Guide, Non-external/public variable names should begin with an underscore

There are 5 instances of this issue:

see instances
File: src/Distributor.sol
56 uint8 private constant VERSION = 1; // version is 1 for now
57 address private immutable FACTORY_ADDRESS;
58 address private immutable STADIUM_ADDRESS;
59 uint256 private constant COMMISSION_FEE = 500; // this can be changed in the future
61 uint256 private constant BASIS_POINTS = 10000;

GitHub: [56, 57, 58, 59, 61]

[N‑13] Constants should be defined rather than using magic numbers

Even assembly can benefit from using readable constants instead of hex/numeric literals

There are 2 instances of this issue:

see instances
File: src/Distributor.sol
/// @custom:audit 10000
135 if (totalPercentage != (10000 - COMMISSION_FEE)) {

GitHub: [135]

File: src/ProxyFactory.sol
/// @custom:audit 0xff
227 bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(code)));

GitHub: [227]

[N‑14] Functions not used internally could be marked external

There are 6 instances of this issue:

File: src/ProxyFactory.sol
105 function setContest(address organizer, bytes32 contestId, uint256 closeTime, address implementation)
106 public
107 onlyOwner
108 {
109 if (organizer == address(0) || implementation == address(0)) revert ProxyFactory__NoZeroAddress();
110 if (closeTime > block.timestamp + MAX_CONTEST_PERIOD || closeTime < block.timestamp) {
111 revert ProxyFactory__CloseTimeNotInRange();
112 }
113 bytes32 salt = _calculateSalt(organizer, contestId, implementation);
114 if (saltToCloseTime[salt] != 0) revert ProxyFactory__ContestIsAlreadyRegistered();
115 saltToCloseTime[salt] = closeTime;
116 emit SetContest(organizer, contestId, closeTime, implementation);
117 }
127 function deployProxyAndDistribute(bytes32 contestId, address implementation, bytes calldata data)
128 public
129 returns (address)
130 {
131 bytes32 salt = _calculateSalt(msg.sender, contestId, implementation);
132 if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
133 // can set close time to current time and end it immediately if organizer wish
134 if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
135 address proxy = _deployProxy(msg.sender, contestId, implementation);
136 _distribute(proxy, data);
137 return proxy;
138 }
152 function deployProxyAndDistributeBySignature(
153 address organizer,
154 bytes32 contestId,
155 address implementation,
156 bytes calldata signature,
157 bytes calldata data
158 ) public returns (address) {
159 bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, data)));
160 if (ECDSA.recover(digest, signature) != organizer) revert ProxyFactory__InvalidSignature();
161 bytes32 salt = _calculateSalt(organizer, contestId, implementation);
162 if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
163 if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
164 address proxy = _deployProxy(organizer, contestId, implementation);
165 _distribute(proxy, data);
166 return proxy;
167 }
179 function deployProxyAndDistributeByOwner(
180 address organizer,
181 bytes32 contestId,
182 address implementation,
183 bytes calldata data
184 ) public onlyOwner returns (address) {
185 bytes32 salt = _calculateSalt(organizer, contestId, implementation);
186 if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
187 if (saltToCloseTime[salt] + EXPIRATION_TIME > block.timestamp) revert ProxyFactory__ContestIsNotExpired();
188 // require(saltToCloseTime[salt] == 0, "Contest is not registered");
189 // require(saltToCloseTime[salt] < block.timestamp + EXPIRATION_TIME, "Contest is not expired");
190 address proxy = _deployProxy(organizer, contestId, implementation);
191 _distribute(proxy, data);
192 return proxy;
193 }
205 function distributeByOwner(
206 address proxy,
207 address organizer,
208 bytes32 contestId,
209 address implementation,
210 bytes calldata data
211 ) public onlyOwner {
212 if (proxy == address(0)) revert ProxyFactory__ProxyAddressCannotBeZero();
213 bytes32 salt = _calculateSalt(organizer, contestId, implementation);
214 if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
215 // distribute only when it exists and expired
216 if (saltToCloseTime[salt] + EXPIRATION_TIME > block.timestamp) revert ProxyFactory__ContestIsNotExpired();
217 _distribute(proxy, data);
218 }
225 function getProxyAddress(bytes32 salt, address implementation) public view returns (address proxy) {
226 bytes memory code = abi.encodePacked(type(Proxy).creationCode, uint256(uint160(implementation)));
227 bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(code)));
228 proxy = address(uint160(uint256(hash)));
229 }

GitHub: [105, 127, 152, 179, 205, 225]

[N‑15] Consider using named mappings

Consider moving to solidity version 0.8.18 or later, and using named mappings to make it easier to understand the purpose of each mapping

There are 2 instances of this issue:

see instances
File: src/ProxyFactory.sol
69 mapping(bytes32 => uint256) public saltToCloseTime;
71 mapping(address => bool) public whitelistedTokens;

GitHub: [69, 71]

Gas Optimizations

Gas totals use lower bounds of ranges and count two iterations of each for-loop. All values above are runtime, not deployment, values; deployment values are listed in the individual issue descriptions. The table above as well as its gas numbers do not include any of the excluded findings.

[G‑01] Reduce gas usage by moving to Solidity 0.8.19 or later

See this link for the full details

There are 3 instances of this issue:

see instances
File: src/Distributor.sol
24 pragma solidity 0.8.18;

GitHub: [24]

File: src/Proxy.sol
24 pragma solidity 0.8.18;

GitHub: [24]

File: src/ProxyFactory.sol
24 pragma solidity 0.8.18;

GitHub: [24]

[G‑02] Use assembly to check for address(0)

Saves 6 gas per instance

There are 7 instances of this issue:

File: src/Distributor.sol
77 if (factoryAddress == address(0) || stadiumAddress == address(0)) revert Distributor__NoZeroAddress();
120 if (token == address(0)) revert Distributor__NoZeroAddress();

GitHub: [77, 77, 120]

File: src/ProxyFactory.sol
84 if (_whitelistedTokens[i] == address(0)) revert ProxyFactory__NoZeroAddress();
109 if (organizer == address(0) || implementation == address(0)) revert ProxyFactory__NoZeroAddress();
212 if (proxy == address(0)) revert ProxyFactory__ProxyAddressCannotBeZero();

GitHub: [84, 109, 109, 212]

[G‑03] Use assembly to emit events, in order to save gass

Using the scratch space for event arguments (two words or fewer) will save gas over needing Solidity's full abi memory expansion used for emitting normally.

There are 3 instances of this issue:

File: src/Distributor.sol
155 emit Distributed(token, winners, percentages, data);

GitHub: [155]

File: src/ProxyFactory.sol
116 emit SetContest(organizer, contestId, closeTime, implementation);
252 emit Distributed(proxy, data);

GitHub: [116, 252]

[G‑04] Using bools for storage incurs overhead

// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing from false to true, after having been true in the past

There is one instance of this issue:

File: src/ProxyFactory.sol
71 mapping(address => bool) public whitelistedTokens;

GitHub: [71]

[G‑05] Cache array length outside of loop

If not cached, the solidity compiler will always read the length of the array during each iteration. That is, if it is a storage array, this is an extra sload operation (100 additional extra gas for each iteration except for the first) and if it is a memory array, this is an extra mload operation (3 additional gas for each iteration except for the first).

There is one instance of this issue:

File: src/ProxyFactory.sol
83 for (uint256 i; i < _whitelistedTokens.length;) {

GitHub: [83]

[G‑06] The result of function calls should be cached rather than re-calling the function

The instances below point to the second+ call of the function within a single function

There is one instance of this issue:

File: src/ProxyFactory.sol
227 bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(code)));

GitHub: [227]

[G‑07] internal functions only called once can be inlined to save gas

Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.

There are 3 instances of this issue:

File: src/Distributor.sol
116 function _distribute(address token, address[] memory winners, uint256[] memory percentages, bytes memory data)
163 function _commissionTransfer(IERC20 token) internal {
172 function _isWhiteListed(address token) internal view returns (bool) {

GitHub: [116, 163, 172]

[G‑08] <array>.length should not be looked up in every loop of a for-loop

The overheads outlined below are PER LOOP, excluding the first loop

  • storage arrays incur a Gwarmaccess (100 gas)

  • memory arrays use MLOAD (3 gas)

  • calldata arrays use CALLDATALOAD (3 gas)

There is one instance of this issue:

File: src/ProxyFactory.sol
83 for (uint256 i; i < _whitelistedTokens.length;) {

GitHub: [83]

[G‑09] Not using the named return variables anywhere in the function is confusing

Consider changing the variable to be an unnamed one, since the variable is never assigned, nor is it returned by name. If the optimizer is not turned on, leaving the code as it is will also waste gas for the stack variable.

There are 5 instances of this issue:

File: src/Distributor.sol
/// @custom:audit _FACTORY_ADDRESS
/// @custom:audit _STADIUM_ADDRESS
/// @custom:audit _COMMISSION_FEE
/// @custom:audit _VERSION
187 {
188 /* solhint-disable */
189 _FACTORY_ADDRESS = FACTORY_ADDRESS;
190 _STADIUM_ADDRESS = STADIUM_ADDRESS;
191 _COMMISSION_FEE = COMMISSION_FEE;
192 _VERSION = VERSION;
193 /* solhint-enable */
194 }

GitHub: [187, 187, 187, 187]

File: src/ProxyFactory.sol
/// @custom:audit proxy
225 function getProxyAddress(bytes32 salt, address implementation) public view returns (address proxy) {
226 bytes memory code = abi.encodePacked(type(Proxy).creationCode, uint256(uint160(implementation)));
227 bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(code)));
228 proxy = address(uint160(uint256(hash)));
229 }

GitHub: [225]

[G‑10] Constructors can be marked payable

Payable functions cost less gas to execute, since the compiler does not have to add extra checks to ensure that a payment wasn't provided. A constructor can safely be marked as payable, since only the deployer would be able to pass funds, and the project itself would not pass any funds.

There are 3 instances of this issue:

see instances
File: src/Distributor.sol
70 constructor(
71 // uint256 version, // for future use
72 address factoryAddress,
73 address stadiumAddress
74 )
75 /* solhint-enable */
76 {

GitHub: [70]

File: src/Proxy.sol
44 constructor(address implementation) {

GitHub: [44]

File: src/ProxyFactory.sol
81 constructor(address[] memory _whitelistedTokens) EIP712("ProxyFactory", "1") Ownable() {

GitHub: [81]

[G‑11] Functions guaranteed to revert when called by normal users can be marked payable

If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2),DUP1(3),ISZERO(3),PUSH2(3),JUMPI(10),PUSH1(3),DUP1(3),REVERT(0),JUMPDEST(1),POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost

There are 3 instances of this issue:

see instances
File: src/ProxyFactory.sol
105 function setContest(address organizer, bytes32 contestId, uint256 closeTime, address implementation)
106 public
107 onlyOwner
108 {
179 function deployProxyAndDistributeByOwner(
180 address organizer,
181 bytes32 contestId,
182 address implementation,
183 bytes calldata data
184 ) public onlyOwner returns (address) {
205 function distributeByOwner(
206 address proxy,
207 address organizer,
208 bytes32 contestId,
209 address implementation,
210 bytes calldata data
211 ) public onlyOwner {

GitHub: [105, 179, 205]

[G‑12] <x> += <y> costs more gas than <x> = <x> + <y> for state variables

Using the addition operator instead of plus-equals saves 113 gas

There is one instance of this issue:

File: src/Distributor.sol
129 totalPercentage += percentages[i];

GitHub: [129]

[G‑13] ++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too)

Saves 5 gas per loop

There is one instance of this issue:

File: src/ProxyFactory.sol
87 i++;

GitHub: [87]

[G‑14] Using private rather than public for constants, saves gas

If needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that returns a tuple of the values of all currently-public constants. Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table

There are 2 instances of this issue:

File: src/ProxyFactory.sol
64 uint256 public constant EXPIRATION_TIME = 7 days;
65 uint256 public constant MAX_CONTEST_PERIOD = 28 days;

GitHub: [64, 65]

[G‑15] Use a more recent version of solidity

Use a solidity version of at least 0.8.2 to get simple compiler automatic inlining
Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads
Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings
Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value

There are 3 instances of this issue:

see instances
File: src/Distributor.sol
24 pragma solidity 0.8.18;

GitHub: [24]

File: src/Proxy.sol
24 pragma solidity 0.8.18;

GitHub: [24]

File: src/ProxyFactory.sol
24 pragma solidity 0.8.18;

GitHub: [24]

Support

FAQs

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