Skip to content

Commit 3b8acfa

Browse files
committed
refactor as library
1 parent 1bc3f03 commit 3b8acfa

File tree

2 files changed

+58
-31
lines changed

2 files changed

+58
-31
lines changed

contracts/utils/RelayedCall.sol renamed to contracts/utils/IndirectCall.sol

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
pragma solidity ^0.8.20;
44

55
import {Address} from "./Address.sol";
6+
import {Create2} from "./Create2.sol";
67

78
/**
89
* @dev Helper contract for performing potentially dangerous calls through a relay the hide the address of the
@@ -14,34 +15,33 @@ import {Address} from "./Address.sol";
1415
* "senderCreator" when calling account factories. Similarly ERC-6942 does factory calls that could be dangerous if
1516
* performed directly.
1617
*
17-
* This contract provides a `_relayedCall` that can be used to perform dangerous calls. These calls are relayed
18-
* through a minimal relayer. This relayer is deployed at construction and its address is stored in immutable storage.
18+
* This contract provides a `indirectCall` that can be used to perform dangerous calls. These calls are indirect
19+
* through a minimal relayer.
1920
*/
20-
abstract contract RelayedCall {
21-
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
22-
/// TODO: should be internal, but hardhat-exposed doesn't expose that correctly in 0.3.19
23-
address public immutable _relayer = _deployRelayer();
24-
25-
function _relayedCallStrict(address target, bytes memory data) internal returns (bytes memory) {
26-
return _relayedCallStrict(target, 0, data);
21+
library IndirectCall {
22+
function indirectCall(address target, bytes memory data) internal returns (bool, bytes memory) {
23+
return indirectCall(target, 0, data);
2724
}
2825

29-
function _relayedCallStrict(address target, uint256 value, bytes memory data) internal returns (bytes memory) {
30-
(bool success, bytes memory returndata) = _relayedCall(target, value, data);
31-
return Address.verifyCallResult(success, returndata);
26+
function indirectCall(address target, uint256 value, bytes memory data) internal returns (bool, bytes memory) {
27+
return getRelayer().call{value: value}(abi.encodePacked(target, data));
3228
}
3329

34-
function _relayedCall(address target, bytes memory data) internal returns (bool, bytes memory) {
35-
return _relayedCall(target, 0, data);
30+
function indirectCallStrict(address target, bytes memory data) internal returns (bytes memory) {
31+
return indirectCallStrict(target, 0, data);
3632
}
3733

38-
function _relayedCall(address target, uint256 value, bytes memory data) internal returns (bool, bytes memory) {
39-
return _relayer.call{value: value}(abi.encodePacked(target, data));
34+
function indirectCallStrict(address target, uint256 value, bytes memory data) internal returns (bytes memory) {
35+
(bool success, bytes memory returndata) = indirectCall(target, value, data);
36+
return Address.verifyCallResult(success, returndata);
4037
}
4138

42-
function _deployRelayer() private returns (address addr) {
39+
function getRelayer() internal returns (address) {
40+
// [Relayer details]
41+
//
4342
// deployment prefix: 3d602f80600a3d3981f3
4443
// deployed bytecode: 60133611600a575f5ffd5b6014360360145f375f5f601436035f345f3560601c5af13d5f5f3e5f3d91602d57fd5bf3
44+
// bytecode hash: 7bc0ea09c689dc0a6de3865d8789dae51a081efcf6569589ddae4b677df5dd3f
4545
//
4646
// offset | bytecode | opcode | stack
4747
// -------|----------|----------------|--------
@@ -85,15 +85,23 @@ abstract contract RelayedCall {
8585
// 0x002c | fd | revert |
8686
// 0x002d | 5b | jumpdest | 0 rds
8787
// 0x002e | f3 | return |
88-
assembly ("memory-safe") {
89-
mstore(0x19, 0x1436035f345f3560601c5af13d5f5f3e5f3d91602d57fd5bf3)
90-
mstore(0x00, 0x3d602f80600a3d3981f360133611600a575f5ffd5b6014360360145f375f5f60)
91-
addr := create2(0, 0, 0x39, 0)
92-
if iszero(addr) {
93-
let ptr := mload(0x40)
94-
returndatacopy(ptr, 0, returndatasize())
95-
revert(ptr, returndatasize())
88+
89+
// Create2 address computation, and deploy it if not yet available
90+
address relayer = Create2.computeAddress(
91+
bytes32(0),
92+
0x7bc0ea09c689dc0a6de3865d8789dae51a081efcf6569589ddae4b677df5dd3f
93+
);
94+
if (relayer.code.length == 0) {
95+
assembly ("memory-safe") {
96+
mstore(0x19, 0x1436035f345f3560601c5af13d5f5f3e5f3d91602d57fd5bf3)
97+
mstore(0x00, 0x3d602f80600a3d3981f360133611600a575f5ffd5b6014360360145f375f5f60)
98+
if iszero(create2(0, 0, 0x39, 0)) {
99+
let ptr := mload(0x40)
100+
returndatacopy(ptr, 0, returndatasize())
101+
revert(ptr, returndatasize())
102+
}
96103
}
97104
}
105+
return relayer;
98106
}
99107
}

test/utils/RelayedCall.test.js renamed to test/utils/IndirectCall.test.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,66 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
55
async function fixture() {
66
const [admin, receiver] = await ethers.getSigners();
77

8-
const mock = await ethers.deployContract('$RelayedCall');
9-
const relayer = await mock._relayer();
8+
const mock = await ethers.deployContract('$IndirectCall');
9+
const relayer = await ethers.getCreate2Address(
10+
mock.target,
11+
ethers.ZeroHash,
12+
'0x7bc0ea09c689dc0a6de3865d8789dae51a081efcf6569589ddae4b677df5dd3f',
13+
);
1014

1115
const authority = await ethers.deployContract('$AccessManager', [admin]);
1216
const target = await ethers.deployContract('$AccessManagedTarget', [authority]);
1317

1418
return { mock, relayer, target, receiver };
1519
}
1620

17-
describe('RelayedCall', function () {
21+
describe('IndirectCall', function () {
1822
beforeEach(async function () {
1923
Object.assign(this, await loadFixture(fixture));
2024
});
2125

26+
it('automatic relayer deployment', async function () {
27+
await expect(ethers.provider.getCode(this.relayer)).to.eventually.equal('0x');
28+
29+
// First call performs deployment
30+
await expect(this.mock.$getRelayer()).to.emit(this.mock, 'return$getRelayer').withArgs(this.relayer);
31+
32+
await expect(ethers.provider.getCode(this.relayer)).to.eventually.not.equal('0x');
33+
34+
// Following calls use the same relayer
35+
await expect(this.mock.$getRelayer()).to.emit(this.mock, 'return$getRelayer').withArgs(this.relayer);
36+
});
37+
2238
describe('relayed call', function () {
2339
it('target success', async function () {
2440
await expect(
25-
this.mock.$_relayedCallStrict(this.target, this.target.interface.encodeFunctionData('fnUnrestricted', [])),
41+
this.mock.$indirectCallStrict(this.target, this.target.interface.encodeFunctionData('fnUnrestricted', [])),
2642
)
2743
.to.emit(this.target, 'CalledUnrestricted')
2844
.withArgs(this.relayer);
2945
});
3046

3147
it('target success (with value)', async function () {
3248
const value = 42n;
33-
await expect(this.mock.$_relayedCallStrict(this.receiver, value, '0x', { value })).to.changeEtherBalances(
49+
await expect(this.mock.$indirectCallStrict(this.receiver, value, '0x', { value })).to.changeEtherBalances(
3450
[this.mock, this.relayer, this.receiver],
3551
[0n, 0n, value],
3652
);
3753
});
3854

3955
it('target revert', async function () {
4056
await expect(
41-
this.mock.$_relayedCallStrict(this.target, this.target.interface.encodeFunctionData('fnRestricted', [])),
57+
this.mock.$indirectCallStrict(this.target, this.target.interface.encodeFunctionData('fnRestricted', [])),
4258
)
4359
.to.be.revertedWithCustomError(this.target, 'AccessManagedUnauthorized')
4460
.withArgs(this.relayer);
4561
});
4662
});
4763

4864
it('direct call to the relayer', async function () {
65+
// deploy relayer
66+
await this.mock.$getRelayer();
67+
4968
// 20 bytes (address + empty data) - OK
5069
await expect(
5170
this.mock.runner.sendTransaction({ to: this.relayer, data: '0x7859821024E633C5dC8a4FcF86fC52e7720Ce525' }),

0 commit comments

Comments
 (0)