Skip to content

Commit df76532

Browse files
committed
Ensure Inner Simulation Reverts
This PR makes a small modification the `simulate` function such that it ensures its inner `simulateAndRevert` reverts as expected. This is to prevent calls to the `CompatibilityFallbackHandler` from making unexpected state changes when not called by a Safe. While using the `CompatibilityFallbackHandler` for something other than a Safe fallback handler is explictely not supported, it makes sense to ensure the inner call inverts and prevent unintended behaviours.
1 parent c4859f4 commit df76532

5 files changed

Lines changed: 113 additions & 67 deletions

File tree

contracts/handler/CompatibilityFallbackHandler.sol

Lines changed: 64 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,28 @@ import {HandlerContext} from "./HandlerContext.sol";
77
import {TokenCallbackHandler} from "./TokenCallbackHandler.sol";
88

99
/**
10-
* @title Compatibility Fallback Handler - Provides compatibility between pre 1.3.0 and 1.3.0+ Safe Smart Account contracts.
10+
* @title Compatibility Fallback Handler
11+
* @notice Provides compatibility between pre 1.3.0 and 1.3.0+ Safe smart account contracts.
12+
* @dev ⚠️⚠️⚠️ This contract is only intended for being used as a fallback handler for a {Safe}.
13+
* Using it in other ways may cause undefined behavior. ⚠️⚠️⚠️
1114
* @author Richard Meissner - @rmeissner
1215
*/
1316
contract CompatibilityFallbackHandler is TokenCallbackHandler, ISignatureValidator, HandlerContext {
14-
// keccak256("SafeMessage(bytes message)");
17+
/**
18+
* @dev The precomputed EIP-712 type hash for the Safe message type.
19+
* Precomputed value of: `keccak256("SafeMessage(bytes message)")`.
20+
*/
1521
bytes32 private constant SAFE_MSG_TYPEHASH = 0x60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca;
1622

17-
bytes4 internal constant SIMULATE_SELECTOR = bytes4(keccak256("simulate(address,bytes)"));
18-
23+
/**
24+
* @dev The sentinel module value in the {ModuleManager.modules} linked list.
25+
* See {ModuleManager.SENTINEL_MODULES} for more information.
26+
*/
1927
address internal constant SENTINEL_MODULES = address(0x1);
2028

2129
/**
22-
* @dev Returns the hash of a message to be signed by owners.
30+
* @notice Returns the hash of a message to be signed by owners.
31+
* @dev This function assumes that the caller is a Safe contract.
2332
* @param message Raw message bytes.
2433
* @return Message hash.
2534
*/
@@ -28,7 +37,7 @@ contract CompatibilityFallbackHandler is TokenCallbackHandler, ISignatureValidat
2837
}
2938

3039
/**
31-
* @dev Returns the pre-image of the message hash (see getMessageHashForSafe).
40+
* @dev Returns the pre-image of the message hash (see {getMessageHashForSafe}).
3241
* @param safe Safe to which the message is targeted.
3342
* @param message Message that should be encoded.
3443
* @return Encoded message.
@@ -49,13 +58,14 @@ contract CompatibilityFallbackHandler is TokenCallbackHandler, ISignatureValidat
4958
}
5059

5160
/**
52-
* @notice Implementation of updated EIP-1271 signature validation method.
53-
* @param _dataHash Hash of the data signed on the behalf of address(msg.sender)
54-
* @param _signature Signature byte array associated with _dataHash
55-
* @return Updated EIP1271 magic value if signature is valid, otherwise 0x0
61+
* @notice Implementation of the EIP-1271 signature validation method.
62+
* @dev This implementation verifies signatures for a `ISafe(msg.sender)`.
63+
* @param _dataHash Hash of the data signed.
64+
* @param _signature Signature data.
65+
* @return The EIP-1271 magic value if the signature is valid, reverts otherwise.
5666
*/
5767
function isValidSignature(bytes32 _dataHash, bytes calldata _signature) public view override returns (bytes4) {
58-
// Caller should be a Safe
68+
// Caller should be a Safe.
5969
ISafe safe = ISafe(payable(msg.sender));
6070
bytes memory messageData = encodeMessageDataForSafe(safe, abi.encode(_dataHash));
6171
bytes32 messageHash = keccak256(messageData);
@@ -74,80 +84,72 @@ contract CompatibilityFallbackHandler is TokenCallbackHandler, ISignatureValidat
7484
* @return Array of modules.
7585
*/
7686
function getModules() external view returns (address[] memory) {
77-
// Caller should be a Safe
87+
// Caller should be a Safe.
7888
ISafe safe = ISafe(payable(msg.sender));
7989
(address[] memory array, ) = safe.getModulesPaginated(SENTINEL_MODULES, 10);
8090
return array;
8191
}
8292

8393
/**
84-
* @dev Performs a delegatecall on a targetContract in the context of self.
85-
* Internally reverts execution to avoid side effects (making it static). Catches revert and returns encoded result as bytes.
86-
* @dev Inspired by https://github.com/gnosis/util-contracts/blob/bb5fe5fb5df6d8400998094fb1b32a178a47c3a1/contracts/StorageAccessible.sol
94+
* @notice Performs a `DELEGATECALL` to a `targetContract` in the context of self.
95+
* @dev Internally reverts execution to avoid side effects (making it effectively static).
96+
* Catches the internal revert and returns encoded result as bytes.
97+
* Inspired by <https://github.com/gnosis/util-contracts/blob/bb5fe5fb5df6d8400998094fb1b32a178a47c3a1/contracts/StorageAccessible.sol>.
8798
* @param targetContract Address of the contract containing the code to execute.
8899
* @param calldataPayload Calldata that should be sent to the target contract (encoded method name and arguments).
89100
*/
90101
function simulate(address targetContract, bytes calldata calldataPayload) external returns (bytes memory response) {
91-
/**
92-
* Suppress compiler warnings about not using parameters, while allowing
93-
* parameters to keep names for documentation purposes. This does not
94-
* generate code.
95-
*/
102+
// Suppress compiler warnings about not using parameters, while allowing
103+
// parameters to keep names for documentation purposes. This does not
104+
// generate code.
96105
targetContract;
97106
calldataPayload;
98107

99108
/* solhint-disable no-inline-assembly */
100109
/// @solidity memory-safe-assembly
101110
assembly {
102111
let ptr := mload(0x40)
103-
/**
104-
* Store `simulateAndRevert.selector`.
105-
* String representation is used to force right padding
106-
*/
112+
// Store `simulateAndRevert.selector`.
113+
// String representation is used to force right padding.
107114
mstore(ptr, "\xb4\xfa\xba\x09")
108115

109-
/**
110-
* Abuse the fact that both this and the internal methods have the
111-
* same signature, and differ only in symbol name (and therefore,
112-
* selector) and copy calldata directly. This saves us approximately
113-
* 250 bytes of code and 300 gas at runtime over the
114-
* `abi.encodeWithSelector` builtin.
115-
*/
116+
// Abuse the fact that both this and the internal methods have the
117+
// same signature, and differ only in symbol name (and therefore,
118+
// selector) and copy calldata directly. This saves us approximately
119+
// 250 bytes of code and 300 gas at runtime over the
120+
// `abi.encodeWithSelector` builtin.
116121
calldatacopy(add(ptr, 0x04), 0x04, sub(calldatasize(), 0x04))
117122

118-
/**
119-
* `pop` is required here by the compiler, as top level expressions
120-
* can't have return values in inline assembly. `call` typically
121-
* returns a 0 or 1 value indicating whether or not it reverted, but
122-
* since we know it will always revert, we can safely ignore it.
123-
*/
124-
pop(
125-
call(
126-
gas(),
127-
// address() has been changed to caller() to use the implementation of the Safe
128-
caller(),
129-
0,
130-
ptr,
131-
calldatasize(),
132-
/**
133-
* The `simulateAndRevert` call always reverts, and
134-
* instead encodes whether or not it was successful in the return
135-
* data. The first 32-byte word of the return data contains the
136-
* `success` value, so write it to memory address 0x00 (which is
137-
* reserved Solidity scratch space and OK to use).
138-
*/
139-
0x00,
140-
0x20
141-
)
123+
let success := call(
124+
gas(),
125+
// `address()` has been changed to `caller()` to use the
126+
// implementation of the calling Safe.
127+
caller(),
128+
0,
129+
ptr,
130+
calldatasize(),
131+
// The `simulateAndRevert` call should always reverts, and
132+
// instead encodes whether or not it was successful in the return
133+
// data. The first 32-byte word of the return data contains the
134+
// `success` value, so write it to memory address 0x00 (which is
135+
// the Solidity scratch space and OK to use).
136+
0x00,
137+
0x20
142138
)
143139

144-
/**
145-
* Allocate and copy the response bytes, making sure to increment
146-
* the free memory pointer accordingly (in case this method is
147-
* called as an internal function). The remaining `returndata[0x20:]`
148-
* contains the ABI encoded response bytes, so we can just write it
149-
* as is to memory.
150-
*/
140+
// Double check that the call reverted as expected. It will always
141+
// revert if the caller is a Safe, but check anyway to make sure this
142+
// function does not make unexpected state changes when called by
143+
// other contracts.
144+
if success {
145+
revert(0, 0)
146+
}
147+
148+
// Allocate and copy the response bytes, making sure to increment
149+
// the free memory pointer accordingly (in case this method is
150+
// called as an internal function). The remaining `returndata[0x20:]`
151+
// contains the ABI encoded response bytes, so we can just write it
152+
// as is to memory.
151153
let responseSize := sub(returndatasize(), 0x20)
152154
response := mload(0x40)
153155
mstore(0x40, add(response, responseSize))

test/handlers/CompatibilityFallbackHandler.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
signHash,
1212
} from "../../src/utils/execution";
1313
import { chainId } from "../utils/encoding";
14-
import { killLibContract } from "../utils/contracts";
14+
import { badSimulatorContract, killLibContract } from "../utils/contracts";
1515

1616
describe("CompatibilityFallbackHandler", () => {
1717
const setupTests = hre.deployments.createFixture(async ({ deployments }) => {
@@ -31,11 +31,13 @@ describe("CompatibilityFallbackHandler", () => {
3131
const safeAddress = await safe.getAddress();
3232
const validator = await getCompatFallbackHandler(safeAddress);
3333
const killLib = await killLibContract(user1, hre.network.zksync);
34+
const badSimulator = await badSimulatorContract(user1);
3435
return {
3536
safe,
3637
validator,
3738
handler,
3839
killLib,
40+
badSimulator,
3941
signLib,
4042
signerSafe,
4143
signers,
@@ -200,8 +202,6 @@ describe("CompatibilityFallbackHandler", () => {
200202
});
201203

202204
describe("simulate", () => {
203-
it.skip("can be called for any Safe", async () => {});
204-
205205
it("should revert changes", async function () {
206206
/**
207207
* ## Test not applicable for zkSync, therefore should skip.
@@ -253,5 +253,11 @@ describe("CompatibilityFallbackHandler", () => {
253253
expect(value).to.be.eq(1n);
254254
expect(await killLib.value()).to.be.eq(0n);
255255
});
256+
257+
it("should revert for unsupported callers", async () => {
258+
const { handler, badSimulator } = await setupTests();
259+
const handlerAddress = await handler.getAddress();
260+
await expect(badSimulator.simulateFallbackHandler(handlerAddress)).to.be.reverted;
261+
});
256262
});
257263
});

test/utils/contracts.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@ export const killLibContract = async (deployer: Signer, zkSync?: boolean) => {
6868
return deployContractFromSource(deployer, killLibSource);
6969
};
7070

71+
export const badSimulatorSource = `
72+
interface ICompatibilityFallbackHandler {
73+
function simulate(address, bytes calldata) external returns (bytes memory);
74+
}
75+
76+
contract Test {
77+
function simulateFallbackHandler(address fallbackHandler) external {
78+
ICompatibilityFallbackHandler(fallbackHandler).simulate(address(0), "");
79+
}
80+
81+
function simulateAndRevert(address, bytes memory) external returns (bool, bytes memory) {
82+
// Oops! We don't revert.
83+
return (true, "");
84+
}
85+
}`;
86+
87+
export const badSimulatorContract = async (deployer: Signer) => {
88+
return deployContractFromSource(deployer, badSimulatorSource);
89+
};
90+
7191
/**
7292
* Retrieves the sender address from the contract runner.
7393
* It is useful when using methods like `hre.ethers.getContractAt` which automatically attach

test/utils/setup.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,16 @@ export const compile = async (source: string) => {
201201
throw Error("Could not compile contract");
202202
}
203203
const fileOutput = output["contracts"]["tmp.sol"];
204-
const contractOutput = fileOutput[Object.keys(fileOutput)[0]];
204+
// Find the first contract with bytecode in the output, this allows the
205+
// compiled code to include interfaces.
206+
const contractOutput = Object.values(fileOutput).find((output) => {
207+
const bytecode = (output["evm"]["bytecode"] ?? {})["object"] ?? "";
208+
return bytecode.length > 0;
209+
});
210+
if (!contractOutput) {
211+
console.log(output);
212+
throw Error("No contract with bytecode");
213+
}
205214
const abi = contractOutput["abi"];
206215
const data = "0x" + contractOutput["evm"]["bytecode"]["object"];
207216
return {

test/utils/zkSync.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,16 @@ export async function zkCompile(
7373
}
7474

7575
const fileOutput = output["contracts"]["tmp.sol"];
76-
const contractOutput = fileOutput[Object.keys(fileOutput)[0]];
76+
// Find the first contract with bytecode in the output, this allows the
77+
// compiled code to include interfaces.
78+
const contractOutput = Object.values(fileOutput).find((output) => {
79+
const bytecode = (output["evm"]["bytecode"] ?? {})["object"] ?? "";
80+
return bytecode.length > 0;
81+
});
82+
if (!contractOutput) {
83+
console.log(output);
84+
throw Error("No contract with bytecode");
85+
}
7786
const abi = contractOutput["abi"];
7887
const bytecode = "0x" + contractOutput["evm"]["bytecode"]["object"];
7988

0 commit comments

Comments
 (0)