Skip to content

Commit 41c614c

Browse files
committed
fix: improve tests
1 parent 64ed605 commit 41c614c

1 file changed

Lines changed: 73 additions & 76 deletions

File tree

test/BatchFund.t.sol

Lines changed: 73 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ import {EscrowBase} from "../src/EscrowBase.sol";
2929
// - Obfuscated fund() selector 0x49364cd4 is present in deployed dispatcher
3030
// - Original selector 0xa65e2cfd is absent (obfuscation replaced all selectors)
3131
//
32+
// Cross-TX verification (successful vs failed):
33+
// WORKING TX 0x8bff4e21...: CREATE → 0x7720..., approve → 0x7720..., fund → 0x7720... (all same)
34+
// FAILING TX 0xedf03465...: CREATE → 0x7e97..., approve → 0xD69B..., fund → 0xD69B... (mismatch)
35+
// When the batch targets the correct address, it works. When wrong, silent no-op.
36+
//
3237
// Hypotheses tested:
3338
// H1 – Bytecode dispatch dead end → RULED OUT
3439
// H2 – msg.sender context in batch → PLAUSIBLE (general risk)
@@ -130,8 +135,7 @@ contract EscrowFactory {
130135
// Constructor auto-funds when both amounts > 0.
131136
// transferFrom inside fund() pulls from msg.sender of fund(), which is
132137
// address(this) during constructor execution.
133-
EscrowERC20 escrow =
134-
new EscrowERC20(tokenContract, recipient, expectedAmount, rewardAmount, paymentAmount);
138+
EscrowERC20 escrow = new EscrowERC20(tokenContract, recipient, expectedAmount, rewardAmount, paymentAmount);
135139
return address(escrow);
136140
}
137141

@@ -201,6 +205,15 @@ contract BatchFundTest is Test {
201205
uint256 constant PAYMENT = 500e18;
202206
uint256 constant EXPECTED_AMOUNT = 1000e18;
203207

208+
// Real values from the failing Tempo TX:
209+
// 0xedf034653df8ebd016a471bb4c19a1a71b01b39694720ffdde148790b4ac94ae
210+
uint256 constant TEMPO_AMOUNT = 100000000;
211+
uint256 constant TEMPO_REWARD = 723471;
212+
address constant TEMPO_TOKEN = 0x20C0000000000000000000000000000000000000;
213+
address constant TEMPO_ESCROW = 0x7e9798a62b42D97fb05b9e092a9A2117FA3fB995;
214+
address constant TEMPO_WRONG_TARGET = 0xD69B8fC5D21819A713fDE3e051C97e1Cb09BD2Aa;
215+
address constant TEMPO_SENDER = 0xA79045285379f02ad505D7338523843D3A73BBaD;
216+
204217
function setUp() public {
205218
deployer = makeAddr("deployer");
206219
executor = makeAddr("executor");
@@ -328,8 +341,7 @@ contract BatchFundTest is Test {
328341
vm.prank(address(factory));
329342
token.approve(futureEscrow, REWARD + PAYMENT);
330343

331-
address escrowAddr =
332-
factory.deployUnfundedThenFund(address(token), recipient, EXPECTED_AMOUNT, REWARD, PAYMENT);
344+
address escrowAddr = factory.deployUnfundedThenFund(address(token), recipient, EXPECTED_AMOUNT, REWARD, PAYMENT);
333345

334346
EscrowERC20 escrow = EscrowERC20(escrowAddr);
335347
assertTrue(escrow.funded(), "should be funded after factory deploy-then-fund");
@@ -342,8 +354,7 @@ contract BatchFundTest is Test {
342354
EscrowFactory factory = new EscrowFactory();
343355

344356
// Deploy unfunded (0,0) via factory
345-
address escrowAddr =
346-
factory.deployAndAutoFund(address(token), recipient, EXPECTED_AMOUNT, 0, 0);
357+
address escrowAddr = factory.deployAndAutoFund(address(token), recipient, EXPECTED_AMOUNT, 0, 0);
347358

348359
EscrowERC20 escrow = EscrowERC20(escrowAddr);
349360
assertFalse(escrow.funded(), "should start unfunded");
@@ -371,9 +382,7 @@ contract BatchFundTest is Test {
371382

372383
LenientBatcher.Call[] memory calls = new LenientBatcher.Call[](1);
373384
calls[0] = LenientBatcher.Call({
374-
target: address(escrow),
375-
data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT),
376-
value: 0
385+
target: address(escrow), data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT), value: 0
377386
});
378387

379388
// Batcher swallows the revert — does not propagate
@@ -398,9 +407,7 @@ contract BatchFundTest is Test {
398407

399408
LenientBatcher.Call[] memory calls = new LenientBatcher.Call[](1);
400409
calls[0] = LenientBatcher.Call({
401-
target: address(escrow),
402-
data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT),
403-
value: 0
410+
target: address(escrow), data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT), value: 0
404411
});
405412

406413
// Batcher is msg.sender, not deployer → OnlyDeployer revert → swallowed
@@ -427,9 +434,7 @@ contract BatchFundTest is Test {
427434
// Call 1: fund() — this would succeed if msg.sender matches
428435
// But msg.sender here is the batcher, not deployer → reverts with OnlyDeployer
429436
calls[0] = StrictBatcher.Call({
430-
target: address(escrow),
431-
data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT),
432-
value: 0
437+
target: address(escrow), data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT), value: 0
433438
});
434439
// Call 2: some other call that reverts
435440
calls[1] = StrictBatcher.Call({target: address(0), data: hex"", value: 0});
@@ -454,9 +459,7 @@ contract BatchFundTest is Test {
454459

455460
StrictBatcher.Call[] memory calls = new StrictBatcher.Call[](1);
456461
calls[0] = StrictBatcher.Call({
457-
target: address(escrow),
458-
data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT),
459-
value: 0
462+
target: address(escrow), data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT), value: 0
460463
});
461464

462465
// deployer calls batcher, batcher calls escrow.fund()
@@ -672,9 +675,7 @@ contract BatchFundTest is Test {
672675

673676
StrictBatcher.Call[] memory calls = new StrictBatcher.Call[](1);
674677
calls[0] = StrictBatcher.Call({
675-
target: address(escrow),
676-
data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT),
677-
value: 0
678+
target: address(escrow), data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT), value: 0
678679
});
679680

680681
// Even though deployer is tx.origin, msg.sender in fund() is batcher
@@ -700,9 +701,7 @@ contract BatchFundTest is Test {
700701

701702
LenientBatcher.Call[] memory calls = new LenientBatcher.Call[](1);
702703
calls[0] = LenientBatcher.Call({
703-
target: address(escrow),
704-
data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT),
705-
value: 0
704+
target: address(escrow), data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT), value: 0
706705
});
707706

708707
// Deployer sends batch → batcher calls fund() → OnlyDeployer revert → SWALLOWED
@@ -721,23 +720,21 @@ contract BatchFundTest is Test {
721720
/// Simulate the exact Tempo token address (0x20c0...0000) as a precompile
722721
/// that has no code → external call returns empty data → ABI decode failure.
723722
function testTempoTokenAddressNoCode() public {
724-
address tempoToken = address(0x20C0000000000000000000000000000000000000);
725-
// Don't etch any code → address has no code
723+
// Don't etch any code → TEMPO_TOKEN has no code in test env
726724

727725
vm.startPrank(deployer);
728726
vm.expectRevert(); // call to address with no code → empty returndata → ABI decode fail
729-
new EscrowERC20(tempoToken, recipient, EXPECTED_AMOUNT, REWARD, PAYMENT);
727+
new EscrowERC20(TEMPO_TOKEN, recipient, TEMPO_AMOUNT, TEMPO_REWARD, TEMPO_AMOUNT);
730728
vm.stopPrank();
731729
}
732730

733731
/// Simulate Tempo token with NoOp behavior etched at the real address.
734732
function testTempoTokenAsNoOp() public {
735-
address tempoToken = address(0x20C0000000000000000000000000000000000000);
736733
NoOpToken noOp = new NoOpToken();
737-
vm.etch(tempoToken, address(noOp).code);
734+
vm.etch(TEMPO_TOKEN, address(noOp).code);
738735

739736
vm.startPrank(deployer);
740-
EscrowERC20 escrow = new EscrowERC20(tempoToken, recipient, EXPECTED_AMOUNT, REWARD, PAYMENT);
737+
EscrowERC20 escrow = new EscrowERC20(TEMPO_TOKEN, recipient, TEMPO_AMOUNT, TEMPO_REWARD, TEMPO_AMOUNT);
741738
vm.stopPrank();
742739

743740
// fund() "succeeded" — constructor auto-funded
@@ -758,111 +755,111 @@ contract BatchFundTest is Test {
758755

759756
/// EVM CALL to an address with no code succeeds with empty returndata.
760757
/// This is the fundamental EVM behavior that enables the silent failure.
758+
/// Uses the real wrong target from the failing TX.
761759
function testCallToEmptyAddressSucceeds() public {
762-
address empty = makeAddr("empty");
763-
assertEq(empty.code.length, 0, "should have no code");
760+
assertEq(TEMPO_WRONG_TARGET.code.length, 0, "wrong target should have no code");
764761

765-
// Any calldata, any selector — returns success with empty data
766-
bytes memory fundCalldata = abi.encodeWithSelector(0x49364cd4, uint256(723471), uint256(100000000));
767-
(bool ok, bytes memory ret) = empty.call(fundCalldata);
762+
// Exact calldata from the failing TX: obfuscated fund(723471, 100000000)
763+
bytes memory fundCalldata = abi.encodeWithSelector(0x49364cd4, TEMPO_REWARD, TEMPO_AMOUNT);
764+
(bool ok, bytes memory ret) = TEMPO_WRONG_TARGET.call(fundCalldata);
768765

769766
assertTrue(ok, "call to empty address should succeed");
770767
assertEq(ret.length, 0, "return data should be empty");
771768
}
772769

773-
/// Reproduce the exact Tempo batch failure:
774-
/// 1. Deploy escrow at address A
775-
/// 2. Approve tokens to address B (wrong, no code)
776-
/// 3. Call fund() on address B (wrong, no code) → silent success
777-
/// 4. Escrow at A remains unfunded
770+
/// Reproduce the exact Tempo batch failure with real on-chain values:
771+
/// TX: 0xedf034653df8ebd016a471bb4c19a1a71b01b39694720ffdde148790b4ac94ae
772+
/// Token: 0x20C0000000000000000000000000000000000000 (PathUSD)
773+
/// Escrow: 0x7e9798a62b42d97fb05b9e092a9a2117fa3fb995 (deployed correctly)
774+
/// Wrong: 0xd69b8fc5d21819a713fde3e051c97e1cb09bd2aa (no code, batch target)
775+
/// Amount: 100000000, Reward: 723471
776+
///
777+
/// Steps in the batch:
778+
/// 1. CREATE escrow → lands at 0x7e97... (correct)
779+
/// 2. approve(0xd69b..., amount) on PathUSD → succeeds (wrong spender)
780+
/// 3. fund(723471, 100000000) on 0xd69b... → silent no-op (no code)
781+
/// 4. Escrow at 0x7e97... remains unfunded
778782
function testExactTempoFailure_WrongAddressBatch() public {
779-
address tempoToken = address(0x20C0000000000000000000000000000000000000);
783+
// Etch a NoOp token at the real PathUSD address
780784
NoOpToken noOp = new NoOpToken();
781-
vm.etch(tempoToken, address(noOp).code);
785+
vm.etch(TEMPO_TOKEN, address(noOp).code);
782786

783787
// Step 1: Deploy escrow (unfunded — constructor gets 0,0)
784788
vm.startPrank(deployer);
785-
EscrowERC20 escrow = new EscrowERC20(tempoToken, recipient, EXPECTED_AMOUNT, 0, 0);
789+
EscrowERC20 escrow = new EscrowERC20(TEMPO_TOKEN, recipient, TEMPO_AMOUNT, 0, 0);
786790
vm.stopPrank();
787791

788-
address wrongTarget = makeAddr("wrongTarget"); // simulates 0xd69b...
789-
790792
assertFalse(escrow.funded(), "escrow should start unfunded");
791-
assertEq(wrongTarget.code.length, 0, "wrong target has no code");
793+
assertEq(TEMPO_WRONG_TARGET.code.length, 0, "0xd69b... has no code on-chain");
792794

793-
// Step 2: Approve tokens to WRONG address (not the escrow)
795+
// Step 2: Approve tokens to WRONG address (0xd69b... instead of escrow)
794796
vm.prank(deployer);
795-
(bool ok1,) = tempoToken.call(
796-
abi.encodeWithSignature("approve(address,uint256)", wrongTarget, REWARD + PAYMENT)
797+
(bool ok1,) = TEMPO_TOKEN.call(
798+
abi.encodeWithSignature("approve(address,uint256)", TEMPO_WRONG_TARGET, TEMPO_REWARD + TEMPO_AMOUNT)
797799
);
798800
assertTrue(ok1, "approve should succeed");
799801

800-
// Step 3: Call fund() on WRONG address (no code → silent success)
801-
bytes memory fundCall = abi.encodeWithSelector(0x49364cd4, REWARD, PAYMENT);
802+
// Step 3: Call obfuscated fund() on WRONG address (no code → silent success)
803+
bytes memory fundCall = abi.encodeWithSelector(0x49364cd4, TEMPO_REWARD, TEMPO_AMOUNT);
802804
vm.prank(deployer);
803-
(bool ok2, bytes memory ret) = wrongTarget.call(fundCall);
805+
(bool ok2, bytes memory ret) = TEMPO_WRONG_TARGET.call(fundCall);
804806

805-
assertTrue(ok2, "call to empty address succeeds (THIS IS THE BUG)");
807+
assertTrue(ok2, "call to 0xd69b... succeeds (THIS IS THE BUG)");
806808
assertEq(ret.length, 0, "empty return - no fund() logic executed");
807809

808810
// Step 4: Escrow is STILL unfunded — the batch "succeeded" but did nothing
809-
assertFalse(escrow.funded(), "CONFIRMED: escrow still unfunded after batch");
810-
811-
// Meanwhile, the correct call WOULD have worked (if sent to escrow):
812-
// The escrow has the fund() code, it would execute the transfer logic.
813-
// But the batch never called it.
811+
assertFalse(escrow.funded(), "CONFIRMED: escrow still unfunded after batch to wrong address");
814812
}
815813

816814
/// Prove the obfuscated fund() works when called at the correct address.
817-
/// Uses the actual escrow's runtime bytecode etched locally.
815+
/// Uses the real Tempo token and amounts.
818816
function testObfuscatedFundWorksAtCorrectAddress() public {
819-
address tempoToken = address(0x20C0000000000000000000000000000000000000);
820817
NoOpToken noOp = new NoOpToken();
821-
vm.etch(tempoToken, address(noOp).code);
818+
vm.etch(TEMPO_TOKEN, address(noOp).code);
822819

823820
// Deploy a real escrow (unfunded)
824821
vm.prank(deployer);
825-
EscrowERC20 escrow = new EscrowERC20(tempoToken, recipient, EXPECTED_AMOUNT, 0, 0);
822+
EscrowERC20 escrow = new EscrowERC20(TEMPO_TOKEN, recipient, TEMPO_AMOUNT, 0, 0);
826823

827824
// Call fund() with the ORIGINAL selector on the unobfuscated contract
828825
vm.prank(deployer);
829-
escrow.fund(REWARD, PAYMENT);
826+
escrow.fund(TEMPO_REWARD, TEMPO_AMOUNT);
830827

831-
// fund() works on the real escrow
832-
assertTrue(escrow.funded(), "fund() works when called at correct address");
828+
// fund() works when called at the correct address
829+
assertTrue(escrow.funded(), "fund() works at correct address with real amounts");
830+
assertEq(escrow.currentRewardAmount(), TEMPO_REWARD);
831+
assertEq(escrow.currentPaymentAmount(), TEMPO_AMOUNT);
833832
}
834833

835-
/// Demonstrate that address mismatch in a strict batch causes silent
836-
/// funding failure when the wrong address has no code.
834+
/// Demonstrate the exact failure in a strict batch context:
835+
/// approve + fund both target TEMPO_WRONG_TARGET instead of the escrow.
837836
function testAddressMismatchInStrictBatch() public {
838837
vm.startPrank(deployer);
839-
EscrowERC20 escrow = new EscrowERC20(address(token), recipient, EXPECTED_AMOUNT, 0, 0);
838+
EscrowERC20 escrow = new EscrowERC20(address(token), recipient, TEMPO_AMOUNT, 0, 0);
840839
vm.stopPrank();
841840

842-
address wrongAddr = makeAddr("computedWrongAddress");
843-
844841
StrictBatcher batcher = new StrictBatcher();
845842
StrictBatcher.Call[] memory calls = new StrictBatcher.Call[](2);
846843

847-
// Approve to wrong address
844+
// Approve to wrong address (0xd69b...)
848845
calls[0] = StrictBatcher.Call({
849846
target: address(token),
850-
data: abi.encodeWithSignature("approve(address,uint256)", wrongAddr, REWARD + PAYMENT),
847+
data: abi.encodeWithSignature("approve(address,uint256)", TEMPO_WRONG_TARGET, TEMPO_REWARD + TEMPO_AMOUNT),
851848
value: 0
852849
});
853850

854851
// fund() on wrong address (no code → returns success)
855852
calls[1] = StrictBatcher.Call({
856-
target: wrongAddr,
857-
data: abi.encodeWithSelector(EscrowERC20.fund.selector, REWARD, PAYMENT),
853+
target: TEMPO_WRONG_TARGET,
854+
data: abi.encodeWithSelector(EscrowERC20.fund.selector, TEMPO_REWARD, TEMPO_AMOUNT),
858855
value: 0
859856
});
860857

861-
// The batch SUCCEEDS because both calls return success
858+
// The batch SUCCEEDS because both calls return success (empty address)
862859
vm.prank(deployer);
863860
batcher.execute(calls);
864861

865862
// But the escrow is not funded
866-
assertFalse(escrow.funded(), "escrow unfunded - batch hit wrong address");
863+
assertFalse(escrow.funded(), "escrow unfunded - batch hit 0xd69b... instead of escrow");
867864
}
868865
}

0 commit comments

Comments
 (0)