Skip to content

Commit eee8a28

Browse files
committed
Refactor SimulateCall to delegatecall into a single relayer (no more salt)
1 parent 1ea0072 commit eee8a28

File tree

3 files changed

+104
-206
lines changed

3 files changed

+104
-206
lines changed

contracts/mocks/CallReceiverMock.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ contract CallReceiverMock {
8888
}
8989
}
9090

91-
function mockFunctionExtra() public payable {
91+
function mockFunctionExtra() public payable returns (address, uint256) {
9292
emit MockFunctionCalledExtra(msg.sender, msg.value);
93+
return (msg.sender, msg.value);
9394
}
9495
}
9596

contracts/utils/SimulateCall.sol

Lines changed: 47 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pragma solidity ^0.8.20;
1212
library SimulateCall {
1313
/// @dev Simulates a call to the target contract through a dynamically deployed simulator.
1414
function simulateCall(address target, bytes memory data) internal returns (bool success, bytes memory retData) {
15-
return simulateCall(target, 0, data, bytes32(0));
15+
return simulateCall(target, 0, data);
1616
}
1717

1818
/// @dev Same as {simulateCall-address-bytes} but with a value.
@@ -21,112 +21,77 @@ library SimulateCall {
2121
uint256 value,
2222
bytes memory data
2323
) internal returns (bool success, bytes memory retData) {
24-
return simulateCall(target, value, data, bytes32(0));
24+
(success, retData) = getSimulator().delegatecall(abi.encodePacked(target, value, data));
25+
success = !success;
2526
}
2627

27-
/// @dev Same as {simulateCall-address-bytes} but with a salt for deterministic simulator address.
28-
function simulateCall(
29-
address target,
30-
bytes memory data,
31-
bytes32 salt
32-
) internal returns (bool success, bytes memory retData) {
33-
return simulateCall(target, 0, data, salt);
34-
}
35-
36-
/// @dev Same as {simulateCall-address-bytes} but with a salt and a value.
37-
function simulateCall(
38-
address target,
39-
uint256 value,
40-
bytes memory data,
41-
bytes32 salt
42-
) internal returns (bool success, bytes memory retData) {
43-
// Calls to a simulator always revert. No need to check the success flag.
44-
(, retData) = getSimulator(salt).call{value: value}(abi.encodePacked(target, data));
45-
46-
assembly ("memory-safe") {
47-
// length of the returned buffer (result/reason + success byte)
48-
let length := mload(retData)
49-
50-
// extract the success byte (last byte of the returned buffer)
51-
success := and(mload(add(retData, length)), 0xff)
52-
53-
// shrink retData to exclude the success byte
54-
mstore(retData, sub(length, 1))
55-
}
56-
}
57-
58-
/// @dev Same as {getSimulator} but with a `bytes32(0)` default salt.
59-
function getSimulator() internal returns (address) {
60-
return getSimulator(bytes32(0));
61-
}
62-
63-
/// @dev Returns the simulator address for a given salt.
64-
function getSimulator(bytes32 salt) internal returns (address simulator) {
28+
/// @dev Returns the simulator address.
29+
function getSimulator() internal returns (address instance) {
6530
// [Simulator details]
66-
//
67-
// deployment prefix: 602e5f8160095f39f3
68-
// deployed bytecode: 60133611600a575f5ffd5b6014360360145f375f5f601436035f345f3560601c5af13d5f5f3e3d533d6001015ffd
31+
// deployment prefix: 60315f8160095f39f3
32+
// deployed bytecode: 60333611600a575f5ffd5b6034360360345f375f5f603436035f6014355f3560601c5af13d5f5f3e5f3d91602f57f35bfd
6933
//
7034
// offset | bytecode | opcode | stack
7135
// -------|-------------|----------------|--------
72-
// 0x0000 | 6013 | push1 0x13 | 0x13
73-
// 0x0002 | 36 | calldatasize | cds 0x13
74-
// 0x0003 | 11 | gt | (cds>0x13)
75-
// 0x0004 | 600a | push1 0x0a | 0x0a (cds>0x13)
36+
// 0x0000 | 6033 | push1 0x33 | 0x33
37+
// 0x0002 | 36 | calldatasize | cds 0x33
38+
// 0x0003 | 11 | gt | (cds>0x33)
39+
// 0x0004 | 600a | push1 0x0a | 0x0a (cds>0x33)
7640
// 0x0006 | 57 | jumpi |
7741
// 0x0007 | 5f | push0 | 0
7842
// 0x0008 | 5f | push0 | 0 0
7943
// 0x0009 | fd | revert |
8044
// 0x000a | 5b | jumpdest |
81-
// 0x000b | 6014 | push1 0x14 | 0x14
82-
// 0x000d | 36 | calldatasize | cds 0x14
83-
// 0x000e | 03 | sub | (cds-0x14)
84-
// 0x000f | 6014 | push1 0x14 | 0x14 (cds-0x14)
85-
// 0x0011 | 5f | push0 | 0 0x14 (cds-0x14)
45+
// 0x000b | 6034 | push1 0x34 | 0x34
46+
// 0x000d | 36 | calldatasize | cds 0x34
47+
// 0x000e | 03 | sub | (cds-0x34)
48+
// 0x000f | 6034 | push1 0x34 | 0x34 (cds-0x34)
49+
// 0x0011 | 5f | push0 | 0 0x34 (cds-0x34)
8650
// 0x0012 | 37 | calldatacopy |
8751
// 0x0013 | 5f | push0 | 0
8852
// 0x0014 | 5f | push0 | 0 0
89-
// 0x0015 | 6014 | push1 0x14 | 0x14 0 0
90-
// 0x0017 | 36 | calldatasize | cds 0x14 0 0
91-
// 0x0018 | 03 | sub | (cds-0x14) 0 0
92-
// 0x0019 | 5f | push0 | 0 (cds-0x14) 0 0
93-
// 0x001a | 34 | callvalue | value 0 (cds-0x14) 0 0
94-
// 0x001b | 5f | push0 | 0 value 0 (cds-0x14) 0 0
95-
// 0x001c | 35 | calldataload | cd[0] value 0 (cds-0x14) 0 0
96-
// 0x001d | 6060 | push1 0x60 | 0x60 cd[0] value 0 (cds-0x14) 0 0
97-
// 0x001f | 1c | shr | target value 0 (cds-0x14) 0 0
98-
// 0x0020 | 5a | gas | gas target value 0 (cds-0x14) 0 0
99-
// 0x0021 | f1 | call | suc
100-
// 0x0022 | 3d | returndatasize | rds suc
101-
// 0x0023 | 5f | push0 | 0 rds suc
102-
// 0x0024 | 5f | push0 | 0 0 rds suc
103-
// 0x0025 | 3e | returndatacopy | suc
104-
// 0x0026 | 3d | returndatasize | rds suc
105-
// 0x0027 | 53 | mstore8 |
106-
// 0x0028 | 3d | returndatasize | rds
107-
// 0x0029 | 6001 | push1 0x01 | 0x01 rds
108-
// 0x002b | 01 | add | (rds+0x01)
109-
// 0x002c | 5f | push0 | 0 (rds+0x01)
110-
// 0x002d | fd | revert |
111-
53+
// 0x0015 | 6034 | push1 0x34 | 0x34 0 0
54+
// 0x0017 | 36 | calldatasize | cds 0x34 0 0
55+
// 0x0018 | 03 | sub | (cds-0x34) 0 0
56+
// 0x0019 | 5f | push0 | 0 (cds-0x34) 0 0
57+
// 0x001a | 6014 | push1 0x14 | 0x14 0 (cds-0x34) 0 0
58+
// 0x001c | 35 | calldataload | cd[0x14] 0 (cds-0x34) 0 0
59+
// 0x001d | 5f | push0 | 0 cd[0x14] 0 (cds-0x34) 0 0
60+
// 0x001e | 35 | calldataload | cd[0] cd[0x14] 0 (cds-0x34) 0 0
61+
// 0x001f | 6060 | push1 0x60 | 0x60 cd[0] cd[0x14] 0 (cds-0x34) 0 0
62+
// 0x0021 | 1c | shr | target cd[0x14] 0 (cds-0x34) 0 0
63+
// 0x0022 | 5a | gas | gas target cd[0x14] 0 (cds-0x34) 0 0
64+
// 0x0023 | f1 | call | suc
65+
// 0x0024 | 3d | returndatasize | rds suc
66+
// 0x0025 | 5f | push0 | 0 rds suc
67+
// 0x0026 | 5f | push0 | 0 0 rds suc
68+
// 0x0027 | 3e | returndatacopy | suc
69+
// 0x0028 | 5f | push0 | 0 suc
70+
// 0x0029 | 3d | returndatasize | rds 0 suc
71+
// 0x002a | 91 | swap2 | suc 0 rds
72+
// 0x002b | 602f | push1 0x2f | 0x2f suc 0 rds
73+
// 0x002d | 57 | jumpi | 0 rds
74+
// 0x002e | f3 | return |
75+
// 0x002f | 5b | jumpdest | 0 rds
76+
// 0x0030 | fd | revert |
11277
assembly ("memory-safe") {
11378
// build initcode at scratch space
114-
mstore(0x20, 0x0360145f375f5f601436035f345f3560601c5af13d5f5f3e3d533d6001015ffd)
115-
mstore(0x00, 0x602e5f8160095f39f360133611600a575f5ffd5b601436)
116-
let initcodehash := keccak256(0x09, 0x37)
79+
mstore(0x20, 0x5f375f5f603436035f6014355f3560601c5af13d5f5f3e5f3d91602f57f35bfd)
80+
mstore(0x00, 0x60315f8160095f39f360333611600a575f5ffd5b603436036034)
81+
let initcodehash := keccak256(0x06, 0x3a)
11782

11883
let fmp := mload(0x40) // cache free memory pointer
11984

12085
// compute create2 address
12186
mstore(add(fmp, 0x40), initcodehash)
122-
mstore(add(fmp, 0x20), salt)
87+
mstore(add(fmp, 0x20), 0)
12388
mstore(add(fmp, 0x00), address())
12489
mstore8(add(fmp, 0x0b), 0xff)
125-
simulator := and(keccak256(add(fmp, 0x0b), 0x55), shr(96, not(0)))
90+
instance := and(keccak256(add(fmp, 0x0b), 0x55), shr(96, not(0)))
12691

12792
// if simulator not yet deployed, deploy it
128-
if iszero(extcodesize(simulator)) {
129-
if iszero(create2(0, 0x09, 0x37, salt)) {
93+
if iszero(extcodesize(instance)) {
94+
if iszero(create2(0, 0x06, 0x3a, 0)) {
13095
returndatacopy(fmp, 0x00, returndatasize())
13196
revert(fmp, returndatasize())
13297
}

test/utils/SimulatedCall.test.js

Lines changed: 55 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -6,151 +6,83 @@ async function fixture() {
66
const [receiver, other] = await ethers.getSigners();
77

88
const mock = await ethers.deployContract('$SimulateCall');
9-
const computeSimulatorAddress = (salt = ethers.ZeroHash) =>
10-
ethers.getCreate2Address(
11-
mock.target,
12-
salt,
13-
ethers.keccak256(
14-
ethers.concat([
15-
'0x602e5f8160095f39f3',
16-
'0x60133611600a575f5ffd5b6014360360145f375f5f601436035f345f3560601c5af13d5f5f3e3d533d6001015ffd',
17-
]),
18-
),
19-
);
9+
const simulator = ethers.getCreate2Address(
10+
mock.target,
11+
ethers.ZeroHash,
12+
ethers.keccak256(
13+
ethers.concat([
14+
'0x60315f8160095f39f3',
15+
'0x60333611600a575f5ffd5b6034360360345f375f5f603436035f6014355f3560601c5af13d5f5f3e5f3d91602f57f35bfd',
16+
]),
17+
),
18+
);
2019

2120
const target = await ethers.deployContract('$CallReceiverMock');
2221

23-
return { mock, target, receiver, other, computeSimulatorAddress };
22+
return { mock, target, receiver, other, simulator };
2423
}
2524

2625
describe('SimulateCall', function () {
2726
beforeEach(async function () {
2827
Object.assign(this, await loadFixture(fixture));
2928
});
3029

31-
describe('default (zero) salt', function () {
32-
beforeEach(async function () {
33-
this.simulator = await this.computeSimulatorAddress();
34-
});
35-
36-
it('automatic simulator deployment', async function () {
37-
await expect(ethers.provider.getCode(this.simulator)).to.eventually.equal('0x');
38-
39-
// First call performs deployment
40-
await expect(this.mock.$getSimulator()).to.emit(this.mock, 'return$getSimulator').withArgs(this.simulator);
30+
it('automatic simulator deployment', async function () {
31+
await expect(ethers.provider.getCode(this.simulator)).to.eventually.equal('0x');
4132

42-
await expect(ethers.provider.getCode(this.simulator)).to.eventually.not.equal('0x');
33+
// First call performs deployment
34+
await expect(this.mock.$getSimulator()).to.emit(this.mock, 'return$getSimulator').withArgs(this.simulator);
4335

44-
// Following calls use the same simulator
45-
await expect(this.mock.$getSimulator()).to.emit(this.mock, 'return$getSimulator').withArgs(this.simulator);
46-
});
36+
await expect(ethers.provider.getCode(this.simulator)).to.eventually.not.equal('0x');
4737

48-
describe('simulated call', function () {
49-
it('target success', async function () {
50-
const tx = this.mock.$simulateCall(
51-
ethers.Typed.address(this.target),
52-
ethers.Typed.bytes(this.target.interface.encodeFunctionData('mockFunctionWithArgsReturn', [10, 20])),
53-
);
54-
await expect(tx)
55-
.to.emit(this.mock, 'return$simulateCall_address_bytes')
56-
.withArgs(true, ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'uint256'], [10, 20]))
57-
.to.not.emit(this.target, 'MockFunctionCalledWithArgs');
58-
});
59-
60-
it('target success (with value)', async function () {
61-
const value = 42n;
62-
63-
// fund the mock
64-
await this.other.sendTransaction({ to: this.mock.target, value });
65-
66-
// perform simulated call
67-
const tx = this.mock.$simulateCall(
68-
ethers.Typed.address(this.receiver),
69-
ethers.Typed.uint256(value),
70-
ethers.Typed.bytes('0x'),
71-
);
72-
73-
await expect(tx).to.changeEtherBalances([this.mock, this.simulator, this.receiver], [0n, 0n, 0n]);
74-
await expect(tx).to.emit(this.mock, 'return$simulateCall_address_uint256_bytes').withArgs(true, '0x');
75-
});
76-
77-
it('target revert', async function () {
78-
const tx = this.mock.$simulateCall(
79-
ethers.Typed.address(this.target),
80-
ethers.Typed.bytes(this.target.interface.encodeFunctionData('mockFunctionRevertsReason', [])),
81-
);
82-
83-
await expect(tx)
84-
.to.emit(this.mock, 'return$simulateCall_address_bytes')
85-
.withArgs(false, this.target.interface.encodeErrorResult('Error', ['CallReceiverMock: reverting']));
86-
});
87-
});
38+
// Following calls use the same simulator
39+
await expect(this.mock.$getSimulator()).to.emit(this.mock, 'return$getSimulator').withArgs(this.simulator);
8840
});
8941

90-
describe('random salt', function () {
91-
beforeEach(async function () {
92-
this.salt = ethers.hexlify(ethers.randomBytes(32));
93-
this.simulator = await this.computeSimulatorAddress(this.salt);
42+
describe('simulated call', function () {
43+
it('target success', async function () {
44+
const txPromise = this.mock.$simulateCall(
45+
ethers.Typed.address(this.target),
46+
ethers.Typed.bytes(this.target.interface.encodeFunctionData('mockFunctionWithArgsReturn', [10, 20])),
47+
);
48+
49+
await expect(txPromise).to.changeEtherBalances([this.mock, this.simulator, this.target], [0n, 0n, 0n]);
50+
await expect(txPromise)
51+
.to.emit(this.mock, 'return$simulateCall_address_bytes')
52+
.withArgs(true, ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'uint256'], [10, 20]))
53+
.to.not.emit(this.target, 'MockFunctionCalledWithArgs');
9454
});
9555

96-
it('automatic simulator deployment', async function () {
97-
await expect(ethers.provider.getCode(this.simulator)).to.eventually.equal('0x');
56+
it('target success (with value)', async function () {
57+
const value = 42n;
9858

99-
// First call performs deployment
100-
await expect(this.mock.$getSimulator(ethers.Typed.bytes32(this.salt)))
101-
.to.emit(this.mock, 'return$getSimulator_bytes32')
102-
.withArgs(this.simulator);
59+
// fund the mock
60+
await this.other.sendTransaction({ to: this.mock.target, value });
10361

104-
await expect(ethers.provider.getCode(this.simulator)).to.eventually.not.equal('0x');
62+
// perform simulated call
63+
const txPromise = this.mock.$simulateCall(
64+
ethers.Typed.address(this.target),
65+
ethers.Typed.uint256(value),
66+
ethers.Typed.bytes(this.target.interface.encodeFunctionData('mockFunctionExtra')),
67+
);
10568

106-
// Following calls use the same simulator
107-
await expect(this.mock.$getSimulator(ethers.Typed.bytes32(this.salt)))
108-
.to.emit(this.mock, 'return$getSimulator_bytes32')
109-
.withArgs(this.simulator);
69+
await expect(txPromise).to.changeEtherBalances([this.mock, this.simulator, this.target], [0n, 0n, 0n]);
70+
await expect(txPromise)
71+
.to.emit(this.mock, 'return$simulateCall_address_uint256_bytes')
72+
.withArgs(true, ethers.AbiCoder.defaultAbiCoder().encode(['address', 'uint256'], [this.mock.target, value]))
73+
.to.not.emit(this.target, 'MockFunctionCalledExtra');
11074
});
11175

112-
describe('simulated call', function () {
113-
it('target success', async function () {
114-
const tx = this.mock.$simulateCall(
115-
ethers.Typed.address(this.target),
116-
ethers.Typed.bytes(this.target.interface.encodeFunctionData('mockFunctionWithArgsReturn', [10, 20])),
117-
ethers.Typed.bytes32(this.salt),
118-
);
119-
await expect(tx)
120-
.to.emit(this.mock, 'return$simulateCall_address_bytes_bytes32')
121-
.withArgs(true, ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'uint256'], [10, 20]))
122-
.to.not.emit(this.target, 'MockFunctionCalledWithArgs');
123-
});
124-
125-
it('target success (with value)', async function () {
126-
const value = 42n;
127-
128-
// fund the mock
129-
await this.other.sendTransaction({ to: this.mock.target, value });
130-
131-
// perform simulated call
132-
const tx = this.mock.$simulateCall(
133-
ethers.Typed.address(this.receiver),
134-
ethers.Typed.uint256(value),
135-
ethers.Typed.bytes('0x'),
136-
ethers.Typed.bytes32(this.salt),
137-
);
138-
139-
await expect(tx).to.changeEtherBalances([this.mock, this.simulator, this.receiver], [0n, 0n, 0n]);
140-
await expect(tx).to.emit(this.mock, 'return$simulateCall_address_uint256_bytes_bytes32').withArgs(true, '0x');
141-
});
142-
143-
it('target revert', async function () {
144-
const tx = this.mock.$simulateCall(
145-
ethers.Typed.address(this.target),
146-
ethers.Typed.bytes(this.target.interface.encodeFunctionData('mockFunctionRevertsReason', [])),
147-
ethers.Typed.bytes32(this.salt),
148-
);
149-
150-
await expect(tx)
151-
.to.emit(this.mock, 'return$simulateCall_address_bytes_bytes32')
152-
.withArgs(false, this.target.interface.encodeErrorResult('Error', ['CallReceiverMock: reverting']));
153-
});
76+
it('target revert', async function () {
77+
const txPromise = this.mock.$simulateCall(
78+
ethers.Typed.address(this.target),
79+
ethers.Typed.bytes(this.target.interface.encodeFunctionData('mockFunctionRevertsReason')),
80+
);
81+
82+
await expect(txPromise).to.changeEtherBalances([this.mock, this.simulator, this.target], [0n, 0n, 0n]);
83+
await expect(txPromise)
84+
.to.emit(this.mock, 'return$simulateCall_address_bytes')
85+
.withArgs(false, this.target.interface.encodeErrorResult('Error', ['CallReceiverMock: reverting']));
15486
});
15587
});
15688
});

0 commit comments

Comments
 (0)