@@ -10,15 +10,15 @@ import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
1010import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol " ;
1111
1212/// @title CCIPReceiverExample
13- /// @notice CCIP receiver with 3 modes: token-only, data-only, and data+tokens.
13+ /// @notice CCIP receiver with 3 modes: token-only, data-only (inbox) , and data+tokens.
1414/// @dev Implements the defensive pattern recommended by Chainlink:
1515/// - Separates message reception from business logic via try/catch
1616/// - Uses try/catch to prevent message failure from locking tokens
1717/// - Tracks failed message IDs for owner-driven recovery via withdrawToken
1818///
1919/// Message modes:
2020/// - Token-only: holds received tokens in the contract.
21- /// - Data-only: emits event with the data payload .
21+ /// - Data-only: stores messages in an on-chain inbox, queryable by anyone .
2222/// - Data+tokens: decodes a recipient address from data and forwards tokens.
2323///
2424/// Security patterns:
@@ -32,6 +32,26 @@ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol
3232contract CCIPReceiverExample is CCIPReceiver , Ownable2Step , Pausable , ReentrancyGuard {
3333 using SafeERC20 for IERC20 ;
3434
35+ // Cross-chain inbox: stores data-only messages for on-chain querying
36+ struct InboxMessage {
37+ bytes32 messageId;
38+ uint64 sourceChainSelector;
39+ address sender;
40+ bytes data;
41+ uint256 timestamp;
42+ }
43+
44+ InboxMessage[] public inbox;
45+ /// @dev Stores index + 1, so 0 means "not found". Subtract 1 to get the actual inbox index.
46+ mapping (bytes32 messageId = > uint256 indexPlusOne ) public messageIndex;
47+
48+ // Batch allowlist entry
49+ struct AllowlistEntry {
50+ uint64 sourceChainSelector;
51+ address sender;
52+ bool allowed;
53+ }
54+
3555 // Allowlisting: sender is allowlisted per source chain to prevent
3656 // a contract on chain B from impersonating an allowlisted sender on chain A.
3757 mapping (uint64 sourceChainSelector = > mapping (address sender = > bool allowed )) public allowlistedSenders;
@@ -48,8 +68,9 @@ contract CCIPReceiverExample is CCIPReceiver, Ownable2Step, Pausable, Reentrancy
4868
4969 // Events
5070 event TokensReceived (bytes32 indexed messageId , address [] tokens , uint256 [] amounts );
51- event DataReceived (bytes32 indexed messageId , bytes data );
71+ event DataReceived (bytes32 indexed messageId , uint64 indexed sourceChainSelector , address sender , bytes data );
5272 event TokensForwarded (bytes32 indexed messageId , address indexed recipient , address [] tokens , uint256 [] amounts );
73+ event AllowlistUpdated (uint64 indexed sourceChainSelector , address indexed sender , bool allowed );
5374 event MessageFailed (bytes32 indexed messageId , bytes reason );
5475
5576 /// @dev Only this contract can call processMessage (used by the try/catch pattern).
@@ -68,6 +89,15 @@ contract CCIPReceiverExample is CCIPReceiver, Ownable2Step, Pausable, Reentrancy
6889 allowlistedSenders[sourceChainSelector][sender] = allowed;
6990 }
7091
92+ /// @notice Batch update the allowlist — add/remove multiple (chain, sender) pairs in one call.
93+ /// @param entries Array of AllowlistEntry structs with chain selector, sender, and allowed flag.
94+ function updateAllowlist (AllowlistEntry[] calldata entries ) external onlyOwner {
95+ for (uint256 i = 0 ; i < entries.length ; i++ ) {
96+ allowlistedSenders[entries[i].sourceChainSelector][entries[i].sender] = entries[i].allowed;
97+ emit AllowlistUpdated (entries[i].sourceChainSelector, entries[i].sender, entries[i].allowed);
98+ }
99+ }
100+
71101 /// @notice Defensive receive: validates allowlists, then delegates to processMessage
72102 /// via try/catch. If processing fails, tokens stay in the contract and the
73103 /// message ID is recorded. The owner can recover tokens via withdrawToken.
@@ -112,7 +142,41 @@ contract CCIPReceiverExample is CCIPReceiver, Ownable2Step, Pausable, Reentrancy
112142 }
113143
114144 function _handleDataOnly (Client.Any2EVMMessage memory message ) internal {
115- emit DataReceived (message.messageId, message.data);
145+ address sender = abi.decode (message.sender, (address ));
146+ messageIndex[message.messageId] = inbox.length + 1 ; // +1 so that 0 means "not found"
147+ inbox.push (InboxMessage ({
148+ messageId: message.messageId,
149+ sourceChainSelector: message.sourceChainSelector,
150+ sender: sender,
151+ data: message.data,
152+ timestamp: block .timestamp
153+ }));
154+ emit DataReceived (message.messageId, message.sourceChainSelector, sender, message.data);
155+ }
156+
157+ /// @notice Get the total number of messages in the inbox.
158+ function getInboxLength () external view returns (uint256 ) {
159+ return inbox.length ;
160+ }
161+
162+ /// @notice Get a message from the inbox by index.
163+ /// @param index The inbox index (0-based).
164+ function getInboxMessage (uint256 index ) external view returns (InboxMessage memory ) {
165+ return inbox[index];
166+ }
167+
168+ /// @notice Get the latest N messages from the inbox.
169+ /// @param count Maximum number of messages to return.
170+ function getLatestMessages (uint256 count ) external view returns (InboxMessage[] memory ) {
171+ uint256 len = inbox.length ;
172+ if (count > len) {
173+ count = len;
174+ }
175+ InboxMessage[] memory result = new InboxMessage [](count);
176+ for (uint256 i = 0 ; i < count; i++ ) {
177+ result[i] = inbox[len - count + i];
178+ }
179+ return result;
116180 }
117181
118182 function _handleDataAndTokens (Client.Any2EVMMessage memory message ) internal {
0 commit comments