40,000 USDC
View results
Submission Details
Severity: low

Low risk

Summary for LOW findings

Number Details Instances
[L-01] Some tokens may revert when zero value transfers are made 5
[L-02] Consider implementing two-step procedure for updating protocol addresses 1
[L-03] Functions calling contracts/addresses with transfer hooks are missing reentrancy guards 5
[L-04] Contracts are not using their OZ Upgradeable counterparts 7
[L-05] Constructors contains no validation 1
[L-06] Emitting storage values instead of the memory one 4

[L‑01] Some tokens may revert when zero value transfers are made

In spite of the fact that EIP-20 states that zero-valued transfers must be accepted, some tokens, such as LEND will revert if this is attempted, which may cause transactions that involve other tokens (such as batch operations) to fully revert. Consider skipping the transfer if the amount is zero, which will also save gas.

file: main/src/Escrow.sol
98 i_tokenContract.safeTransfer(i_seller, i_tokenContract.balanceOf(address(this)));
120 i_tokenContract.safeTransfer(i_buyer, buyerAward);
123 i_tokenContract.safeTransfer(i_arbiter, i_arbiterFee);
127 i_tokenContract.safeTransfer(i_seller, tokenBalance);

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/Escrow.sol#L98

file: main/src/EscrowFactory.sol
39 tokenContract.safeTransferFrom(msg.sender, computedAddress, price);

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/EscrowFactory.sol#L39

[L‑02] Consider implementing two-step procedure for updating protocol addresses

A copy-paste error or a typo may end up bricking protocol functionality, or sending tokens to an address with no known private key. Consider implementing a two-step procedure for updating protocol addresses, where the recipient is set as pending, and must 'accept' the assignment by making an affirmative call. A straight forward way of doing this would be to have the target contracts implement EIP-165, and to have the 'set' functions ensure that the recipient is of the right interface type.

file: main/src/Escrow.sol
94 function confirmReceipt() external onlyBuyer inState(State.Created) {
s_state = State.Confirmed;
emit Confirmed(i_seller);
i_tokenContract.safeTransfer(i_seller, i_tokenContract.balanceOf(address(this)));
}

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/Escrow.sol#L94-L99

[L‑03] Functions calling contracts/addresses with transfer hooks are missing reentrancy guards

Even if the function follows the best practice of check-effects-interaction, not using a reentrancy guard when there may be transfer hooks will open the users of this protocol up to read-only reentrancies with no way to protect against it, except by block-listing the whole protocol.

file: main/src/Escrow.sol
98 i_tokenContract.safeTransfer(i_seller, i_tokenContract.balanceOf(address(this)));
120 i_tokenContract.safeTransfer(i_buyer, buyerAward);
123 i_tokenContract.safeTransfer(i_arbiter, i_arbiterFee);
127 i_tokenContract.safeTransfer(i_seller, tokenBalance);

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/Escrow.sol#L98

file: main/src/EscrowFactory.sol
39 tokenContract.safeTransferFrom(msg.sender, computedAddress, price);

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/EscrowFactory.sol#L39

[L-04] Contracts are not using their OZ Upgradeable counterparts

The non-upgradeable standard version of OpenZeppelin’s library are inherited / used by the contracts. It would be safer to use the upgradeable versions of the library contracts to avoid unexpected behaviour.

Where applicable, use the contracts from @openzeppelin/contracts-upgradeable instead of @openzeppelin/contracts. See this for list of available upgradeable contracts

file: main/src/Escrow.sol
7 import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8 import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9 import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/Escrow.sol#L7

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/Escrow.sol#L8

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/Escrow.sol#L9

file: main/src/EscrowFactory.sol
7 import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8 import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/EscrowFactory.sol#L7

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/EscrowFactory.sol#L8

file: main/src/IEscrow.sol
4 import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/IEscrow.sol#L4

file: main/src/IEscrowFactory.sol
5 import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/IEscrowFactory.sol#L5

[L-05] Constructors contains no validation

In Solidity, when values are being assigned in constructors to unsigned or integer variables, it's crucial to ensure the provided values adhere to the protocol's specific operational boundaries as laid out in the project specifications and documentation. If the constructors lack appropriate validation checks, there's a risk of setting state variables with values that could cause unexpected and potentially detrimental behavior within the contract's operations, violating the intended logic of the protocol. This can compromise the contract's security and impact the maintainability and reliability of the system. In order to avoid such issues, it is recommended to incorporate rigorous validation checks in constructors. These checks should align with the project's defined rules and constraints, making use of Solidity's built-in require function to enforce these conditions. If the validation checks fail, the require function will cause the transaction to revert, ensuring the integrity and adherence to the protocol's expected behavior.

file: main/src/Escrow.sol
32 constructor(
uint256 price,
IERC20 tokenContract,
address buyer,
address seller,
address arbiter,
uint256 arbiterFee
) {
if (address(tokenContract) == address(0)) revert Escrow__TokenZeroAddress();
if (buyer == address(0)) revert Escrow__BuyerZeroAddress();
if (seller == address(0)) revert Escrow__SellerZeroAddress();
if (arbiterFee >= price) revert Escrow__FeeExceedsPrice(price, arbiterFee);
if (tokenContract.balanceOf(address(this)) < price) revert Escrow__MustDeployWithTokenBalance();
i_price = price;
i_tokenContract = tokenContract;
i_buyer = buyer;
i_seller = seller;
i_arbiter = arbiter;
i_arbiterFee = arbiterFee;
}

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/Escrow.sol#L32-L51

[L-06] Emitting storage values instead of the memory one

Emitted values should not be read from storage again. Instead, the existing values from memory should be used.

file: main/src/Escrow.sol
96 emit Confirmed(i_seller);
105 emit Disputed(msg.sender);
117 emit Resolved(i_buyer, i_seller);

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/Escrow.sol#L96

file: main/src/EscrowFactory.sol
51 emit EscrowCreated(address(escrow), msg.sender, seller, arbiter);

https://github.com/Cyfrin/2023-07-escrow/blob/main/src/EscrowFactory.sol#L51

Support

FAQs

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