Skip to content

Commit 3fed934

Browse files
committed
feat: replaced abi.decode with CCIPMessageDecoder lib
1 parent daffe3d commit 3fed934

File tree

9 files changed

+538
-76
lines changed

9 files changed

+538
-76
lines changed

foundry.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ src = "src"
1414
test = "test"
1515
via_ir = true
1616

17+
[profile.default.fuzz]
18+
max_test_rejects = 1_000_000 # Number of times `vm.assume` can fail
19+
runs = 1_000
20+
1721
[fmt]
1822
bracket_spacing = true
1923
contract_new_lines = false

src/bridge/EnsoCCIPReceiver.sol

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.24;
33

44
import { IEnsoCCIPReceiver } from "../interfaces/IEnsoCCIPReceiver.sol";
55
import { IEnsoRouter, Token, TokenType } from "../interfaces/IEnsoRouter.sol";
6+
import { CCIPMessageDecoder } from "../libraries/CCIPMessageDecoder.sol";
67
import { CCIPReceiver, Client } from "chainlink-ccip/applications/CCIPReceiver.sol";
78
import { Ownable, Ownable2Step } from "openzeppelin-contracts/access/Ownable2Step.sol";
89
import { IERC20, SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
@@ -101,15 +102,6 @@ contract EnsoCCIPReceiver is IEnsoCCIPReceiver, CCIPReceiver, Ownable2Step, Paus
101102
}
102103
}
103104

104-
/// @inheritdoc IEnsoCCIPReceiver
105-
function decodeMessageData(bytes calldata _data) external view returns (address, uint256, bytes memory) {
106-
if (msg.sender != address(this)) {
107-
revert IEnsoCCIPReceiver.EnsoCCIPReceiver_OnlySelf();
108-
}
109-
// Temporary approach; will be replaced by a safe inline decoder.
110-
return abi.decode(_data, (address, uint256, bytes));
111-
}
112-
113105
/// @inheritdoc IEnsoCCIPReceiver
114106
function execute(address _token, uint256 _amount, bytes calldata _shortcutData) external {
115107
if (msg.sender != address(this)) {
@@ -159,6 +151,9 @@ contract EnsoCCIPReceiver is IEnsoCCIPReceiver, CCIPReceiver, Ownable2Step, Paus
159151
if (_errorCode == ErrorCode.PAUSED || _errorCode == ErrorCode.INSUFFICIENT_GAS) {
160152
return RefundKind.TO_RECEIVER;
161153
}
154+
// Only refund directly to the receiver when the payload decodes successfully.
155+
// If decoding fails (MALFORMED_MESSAGE_DATA), all fields (including `receiver`) must be treated as untrusted,
156+
// since a malformed payload could spoof a plausible receiver address.
162157
if (
163158
_errorCode == ErrorCode.NO_TOKENS || _errorCode == ErrorCode.TOO_MANY_TOKENS
164159
|| _errorCode == ErrorCode.NO_TOKEN_AMOUNT || _errorCode == ErrorCode.MALFORMED_MESSAGE_DATA
@@ -216,15 +211,11 @@ contract EnsoCCIPReceiver is IEnsoCCIPReceiver, CCIPReceiver, Ownable2Step, Paus
216211
return (token, amount, receiver, shortcutData, ErrorCode.NO_TOKEN_AMOUNT, errorData);
217212
}
218213

219-
// Decode payload (temporary external helper; to be replaced by safe inline decoder)
214+
// Decode payload
215+
bool decodeSuccess;
220216
uint256 estimatedGas;
221-
try this.decodeMessageData(_message.data) returns (
222-
address decodedReceiver, uint256 decodedEstimatedGas, bytes memory decodedShortcutData
223-
) {
224-
receiver = decodedReceiver;
225-
estimatedGas = decodedEstimatedGas;
226-
shortcutData = decodedShortcutData;
227-
} catch {
217+
(decodeSuccess, receiver, estimatedGas, shortcutData) = CCIPMessageDecoder.tryDecodeMessageData(_message.data);
218+
if (!decodeSuccess) {
228219
return (token, amount, receiver, shortcutData, ErrorCode.MALFORMED_MESSAGE_DATA, errorData);
229220
}
230221

src/interfaces/IEnsoCCIPReceiver.sol

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,6 @@ interface IEnsoCCIPReceiver {
100100
/// @param shortcutData ABI-encoded call data for the Enso Shortcuts entrypoint.
101101
function execute(address token, uint256 amount, bytes calldata shortcutData) external;
102102

103-
/// @notice Temporary helper to decode `(address receiver, uint256 estimatedGas, bytes shortcutData)` from
104-
/// a CCIP message payload. Implementations may replace this with safe inline decoding later.
105-
/// @dev SHOULD revert when called externally by non-self to avoid surfacing internals.
106-
/// Typical guard: `if (msg.sender != address(this)) revert EnsoCCIPReceiver_OnlySelf();`
107-
function decodeMessageData(bytes calldata data)
108-
external
109-
view
110-
returns (address receiver, uint256 estimatedGas, bytes memory shortcutData);
111-
112103
/// @notice Pauses the CCIP receiver, disabling new incoming message execution until unpaused.
113104
/// @dev Only callable by the contract owner.
114105
function pause() external;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
pragma solidity ^0.8.24;
3+
4+
library CCIPMessageDecoder {
5+
/// @dev Safe, non-reverting decoder for abi.encode(address,uint256,bytes) in MEMORY.
6+
/// Returns (ok, receiver, estimatedGas, shortcutData). On malformed input, ok=false.
7+
/// Layout: HEAD (96 bytes) = [receiver|estimatedGas|offset], TAIL at (base+off) = [len|bytes...].
8+
function tryDecodeMessageData(bytes memory _data)
9+
internal
10+
pure
11+
returns (bool success, address receiver, uint256 estimatedGas, bytes memory shortcutData)
12+
{
13+
// Need 3 head words (96) + 1 length word (32)
14+
if (_data.length < 128) {
15+
return (false, address(0), 0, bytes(""));
16+
}
17+
18+
// Pointer to first head word
19+
uint256 base;
20+
assembly { base := add(_data, 32) }
21+
22+
uint256 off;
23+
assembly ("memory-safe") {
24+
// Address is right-aligned in the word → keep low 20 bytes
25+
receiver := and(mload(base), 0x000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
26+
estimatedGas := mload(add(base, 32))
27+
off := mload(add(base, 64))
28+
}
29+
30+
// Word-aligned offset?
31+
if ((off & 31) != 0) {
32+
return (false, address(0), 0, bytes(""));
33+
}
34+
35+
uint256 baseLen = _data.length;
36+
37+
// Off must be at/after 3-word head and leave room for tail length word
38+
// i.e. off >= 96 && off <= baseLen - 32 (avoid off+32 overflow)
39+
if (off < 96 || off > baseLen - 32) {
40+
return (false, address(0), 0, bytes(""));
41+
}
42+
43+
// Safe now to compute tail start (no overflow)
44+
uint256 tailStart = off + 32;
45+
46+
// Read tail len
47+
uint256 len;
48+
assembly ("memory-safe") {
49+
len := mload(add(base, off))
50+
}
51+
52+
unchecked {
53+
// Available bytes remaining after the tail length word
54+
uint256 avail = baseLen - tailStart;
55+
56+
// Require len itself to fit in the available tail
57+
if (len > avail) {
58+
return (false, address(0), 0, bytes(""));
59+
}
60+
61+
// Ceil32(len) and ensure padded bytes also fit (defensive; usually implied by len<=avail)
62+
uint256 padded = (len + 31) & ~uint256(31);
63+
if (padded > avail) {
64+
return (false, address(0), 0, bytes(""));
65+
}
66+
67+
// Allocate and copy exactly `len` bytes (ignore padding)
68+
shortcutData = new bytes(len);
69+
if (len != 0) {
70+
assembly ("memory-safe") {
71+
let src := add(add(base, off), 32) // start of tail payload
72+
let dst := add(shortcutData, 32) // start of new bytes payload
73+
// Copy in 32-byte chunks up to padded boundary
74+
for { let i := 0 } lt(i, padded) { i := add(i, 32) } {
75+
mstore(add(dst, i), mload(add(src, i)))
76+
}
77+
}
78+
}
79+
}
80+
81+
return (true, receiver, estimatedGas, shortcutData);
82+
}
83+
}

test/unit/concrete/bridge/ensoCCIPReceiver/decodeMessageData.t.sol

Lines changed: 0 additions & 49 deletions
This file was deleted.

test/unit/concrete/delegate/ensoReceiver/executeMultiSend.t.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { EnsoReceiver } from "../../../../../src/delegate/EnsoReceiver.sol";
66
import { TokenBalanceHelper } from "../../../../utils/TokenBalanceHelper.sol";
77
import { EnsoReceiver_Unit_Concrete_Test } from "./EnsoReceiver.t.sol";
88
import { console2 } from "forge-std-1.9.7/Test.sol";
9-
import { ReentrancyGuardTransient } from "openzeppelin-contracts/utils/ReentrancyGuardTransient.sol";
109

1110
contract EnsoReceiver_ExecuteMultiSend_SenderIsEnsoReceiver_Unit_Concrete_Test is
1211
EnsoReceiver_Unit_Concrete_Test,

0 commit comments

Comments
 (0)