Skip to content

Commit ab96c48

Browse files
Amxxernestognwfrangio
authored
Cherrypick #5353 into release-v5.2 (#5367)
Signed-off-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Ernesto García <ernestognw@gmail.com> Co-authored-by: Francisco Giordano <fg@frang.io>
1 parent ac4198f commit ab96c48

11 files changed

+528
-27
lines changed

.changeset/seven-insects-taste.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': patch
3+
---
4+
5+
`ERC7579Utils`: Add ABI decoding checks on calldata bounds within `decodeBatch`

contracts/account/utils/draft-ERC7579Utils.sol

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ library ERC7579Utils {
6262
/// @dev The module type is not supported.
6363
error ERC7579UnsupportedModuleType(uint256 moduleTypeId);
6464

65+
/// @dev Input calldata not properly formatted and possibly malicious.
66+
error ERC7579DecodingError();
67+
6568
/// @dev Executes a single call.
6669
function execSingle(
67-
ExecType execType,
68-
bytes calldata executionCalldata
70+
bytes calldata executionCalldata,
71+
ExecType execType
6972
) internal returns (bytes[] memory returnData) {
7073
(address target, uint256 value, bytes calldata callData) = decodeSingle(executionCalldata);
7174
returnData = new bytes[](1);
@@ -74,8 +77,8 @@ library ERC7579Utils {
7477

7578
/// @dev Executes a batch of calls.
7679
function execBatch(
77-
ExecType execType,
78-
bytes calldata executionCalldata
80+
bytes calldata executionCalldata,
81+
ExecType execType
7982
) internal returns (bytes[] memory returnData) {
8083
Execution[] calldata executionBatch = decodeBatch(executionCalldata);
8184
returnData = new bytes[](executionBatch.length);
@@ -92,8 +95,8 @@ library ERC7579Utils {
9295

9396
/// @dev Executes a delegate call.
9497
function execDelegateCall(
95-
ExecType execType,
96-
bytes calldata executionCalldata
98+
bytes calldata executionCalldata,
99+
ExecType execType
97100
) internal returns (bytes[] memory returnData) {
98101
(address target, bytes calldata callData) = decodeDelegate(executionCalldata);
99102
returnData = new bytes[](1);
@@ -170,12 +173,40 @@ library ERC7579Utils {
170173
}
171174

172175
/// @dev Decodes a batch of executions. See {encodeBatch}.
176+
///
177+
/// NOTE: This function runs some checks and will throw a {ERC7579DecodingError} if the input is not properly formatted.
173178
function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) {
174-
assembly ("memory-safe") {
175-
let ptr := add(executionCalldata.offset, calldataload(executionCalldata.offset))
176-
// Extract the ERC7579 Executions
177-
executionBatch.offset := add(ptr, 32)
178-
executionBatch.length := calldataload(ptr)
179+
unchecked {
180+
uint256 bufferLength = executionCalldata.length;
181+
182+
// Check executionCalldata is not empty.
183+
if (bufferLength < 32) revert ERC7579DecodingError();
184+
185+
// Get the offset of the array (pointer to the array length).
186+
uint256 arrayLengthOffset = uint256(bytes32(executionCalldata[0:32]));
187+
188+
// The array length (at arrayLengthOffset) should be 32 bytes long. We check that this is within the
189+
// buffer bounds. Since we know bufferLength is at least 32, we can subtract with no overflow risk.
190+
if (arrayLengthOffset > bufferLength - 32) revert ERC7579DecodingError();
191+
192+
// Get the array length. arrayLengthOffset + 32 is bounded by bufferLength so it does not overflow.
193+
uint256 arrayLength = uint256(bytes32(executionCalldata[arrayLengthOffset:arrayLengthOffset + 32]));
194+
195+
// Check that the buffer is long enough to store the array elements as "offset pointer":
196+
// - each element of the array is an "offset pointer" to the data.
197+
// - each "offset pointer" (to an array element) takes 32 bytes.
198+
// - validity of the calldata at that location is checked when the array element is accessed, so we only
199+
// need to check that the buffer is large enough to hold the pointers.
200+
//
201+
// Since we know bufferLength is at least arrayLengthOffset + 32, we can subtract with no overflow risk.
202+
// Solidity limits length of such arrays to 2**64-1, this guarantees `arrayLength * 32` does not overflow.
203+
if (arrayLength > type(uint64).max || bufferLength - arrayLengthOffset - 32 < arrayLength * 32)
204+
revert ERC7579DecodingError();
205+
206+
assembly ("memory-safe") {
207+
executionBatch.offset := add(add(executionCalldata.offset, arrayLengthOffset), 32)
208+
executionBatch.length := arrayLength
209+
}
179210
}
180211
}
181212

foundry.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ out = 'out'
88
libs = ['node_modules', 'lib']
99
test = 'test'
1010
cache_path = 'cache_forge'
11+
fs_permissions = [{ access = "read", path = "./test/bin" }]
1112

1213
[fuzz]
1314
runs = 5000

test/account/utils/draft-ERC4337Utils.test.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ const { expect } = require('chai');
33
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
44

55
const { packValidationData, UserOperation } = require('../../helpers/erc4337');
6+
const { deployEntrypoint } = require('../../helpers/erc4337-entrypoint');
67
const { MAX_UINT48 } = require('../../helpers/constants');
78
const ADDRESS_ONE = '0x0000000000000000000000000000000000000001';
89

910
const fixture = async () => {
10-
const [authorizer, sender, entrypoint, factory, paymaster] = await ethers.getSigners();
11+
const { entrypoint } = await deployEntrypoint();
12+
const [authorizer, sender, factory, paymaster] = await ethers.getSigners();
1113
const utils = await ethers.deployContract('$ERC4337Utils');
1214
const SIG_VALIDATION_SUCCESS = await utils.$SIG_VALIDATION_SUCCESS();
1315
const SIG_VALIDATION_FAILED = await utils.$SIG_VALIDATION_FAILED();
@@ -167,11 +169,19 @@ describe('ERC4337Utils', function () {
167169
describe('hash', function () {
168170
it('returns the operation hash with specified entrypoint and chainId', async function () {
169171
const userOp = new UserOperation({ sender: this.sender, nonce: 1 });
170-
const chainId = 0xdeadbeef;
172+
const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId);
173+
const otherChainId = 0xdeadbeef;
171174

175+
// check that helper matches entrypoint logic
176+
expect(this.entrypoint.getUserOpHash(userOp.packed)).to.eventually.equal(userOp.hash(this.entrypoint, chainId));
177+
178+
// check library against helper
172179
expect(this.utils.$hash(userOp.packed, this.entrypoint, chainId)).to.eventually.equal(
173180
userOp.hash(this.entrypoint, chainId),
174181
);
182+
expect(this.utils.$hash(userOp.packed, this.entrypoint, otherChainId)).to.eventually.equal(
183+
userOp.hash(this.entrypoint, otherChainId),
184+
);
175185
});
176186
});
177187

0 commit comments

Comments
 (0)