-
Notifications
You must be signed in to change notification settings - Fork 58
/
ProgrammableTokenTransfers.sol
273 lines (239 loc) · 13.6 KB
/
ProgrammableTokenTransfers.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {IERC20} from
"@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from
"@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
/// @title - A simple messenger contract for sending/receiving messages and tokens across chains.
/// Pay using native tokens (e.g, ETH in Ethereum)
contract ProgrammableTokenTransfers is CCIPReceiver, OwnerIsCreator {
using SafeERC20 for IERC20;
// Custom errors to provide more descriptive revert messages.
error NoMessageReceived(); // Used when trying to access a message but no messages have been received.
error IndexOutOfBound(uint256 providedIndex, uint256 maxIndex); // Used when the provided index is out of bounds.
error MessageIdNotExist(bytes32 messageId); // Used when the provided message ID does not exist.
error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.
error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails.
error InsufficientFeeTokenAmount(); // Used when the contract balance isn't enough to pay fees.
// Event emitted when a message is sent to another chain.
// The chain selector of the destination chain.
// The address of the receiver on the destination chain.
// The message being sent.
// The token amount that was sent.
// The fees paid for sending the message.
event MessageSent( // The unique ID of the message.
bytes32 indexed messageId,
uint64 indexed destinationChainSelector,
address receiver,
string message,
Client.EVMTokenAmount tokenAmount,
uint256 fees
);
// Event emitted when a message is received from another chain.
// The chain selector of the source chain.
// The address of the sender from the source chain.
// The message that was received.
// The token amount that was received.
event MessageReceived( // The unique ID of the message.
bytes32 indexed messageId,
uint64 indexed sourceChainSelector,
address sender,
string message,
Client.EVMTokenAmount tokenAmount
);
// Struct to hold details of a message.
struct Message {
uint64 sourceChainSelector; // The chain selector of the source chain.
address sender; // The address of the sender.
string message; // The content of the message.
address token; // received token.
uint256 amount; // received amount.
}
// Storage variables.
bytes32[] public receivedMessages; // Array to keep track of the IDs of received messages.
mapping(bytes32 => Message) public messageDetail; // Mapping from message ID to Message struct, storing details of each received message.
/// @notice Constructor initializes the contract with the router address.
/// @param router The address of the router contract.
constructor(address router) CCIPReceiver(router) {}
/// @notice Sends data to receiver on the destination chain.
/// @dev Assumes your contract has sufficient native asset (e.g, ETH on Ethereum, MATIC on Polygon...).
/// @param destinationChainSelector The identifier (aka selector) for the destination blockchain.
/// @param receiver The address of the recipient on the destination blockchain.
/// @param message The string message to be sent.
/// @param token token address.
/// @param amount token amount.
/// @return messageId The ID of the message that was sent.
function sendMessage(
uint64 destinationChainSelector,
address receiver,
string calldata message,
address token,
uint256 amount
) external returns (bytes32 messageId) {
// set the tokent amounts
Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({token: token, amount: amount});
tokenAmounts[0] = tokenAmount;
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
receiver: abi.encode(receiver), // ABI-encoded receiver address
data: abi.encode(message), // ABI-encoded string message
tokenAmounts: tokenAmounts, // Tokens amounts
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit and allowing out-of-order execution.
// Best Practice: For simplicity, the values are hardcoded. It is advisable to use a more dynamic approach
// where you set the extra arguments off-chain. This allows adaptation depending on the lanes, messages,
// and ensures compatibility with future CCIP upgrades. Read more about it here: https://docs.chain.link/ccip/best-practices#using-extraargs
Client.EVMExtraArgsV2({
gasLimit: 200_000, // Gas limit for the callback on the destination chain
allowOutOfOrderExecution: true // Allows the message to be executed out of order relative to other messages from the same sender
})
),
feeToken: address(0) // Setting feeToken to zero address, indicating native asset will be used for fees
});
// Initialize a router client instance to interact with cross-chain router
IRouterClient router = IRouterClient(this.getRouter());
// approve the Router to spend tokens on contract's behalf. I will spend the amount of the given token
IERC20(token).approve(address(router), amount);
// Get the fee required to send the message
uint256 fees = router.getFee(destinationChainSelector, evm2AnyMessage);
// Reverts if this Contract doesn't have enough native tokens
if (address(this).balance < fees) revert InsufficientFeeTokenAmount();
// Send the message through the router and store the returned message ID
messageId = router.ccipSend{value: fees}(destinationChainSelector, evm2AnyMessage);
// Emit an event with message details
emit MessageSent(messageId, destinationChainSelector, receiver, message, tokenAmount, fees);
// Return the message ID
return messageId;
}
/// handle a received message
function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override {
bytes32 messageId = any2EvmMessage.messageId; // fetch the messageId
uint64 sourceChainSelector = any2EvmMessage.sourceChainSelector; // fetch the source chain identifier (aka selector)
address sender = abi.decode(any2EvmMessage.sender, (address)); // abi-decoding of the sender address
Client.EVMTokenAmount[] memory tokenAmounts = any2EvmMessage.destTokenAmounts;
address token = tokenAmounts[0].token; // we expect one token to be transfered at once but of course, you can transfer several tokens.
uint256 amount = tokenAmounts[0].amount; // we expect one token to be transfered at once but of course, you can transfer several tokens.
string memory message = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent string message
receivedMessages.push(messageId);
Message memory detail = Message(sourceChainSelector, sender, message, token, amount);
messageDetail[messageId] = detail;
emit MessageReceived(messageId, sourceChainSelector, sender, message, tokenAmounts[0]);
}
/// @notice Get the total number of received messages.
/// @return number The total number of received messages.
function getNumberOfReceivedMessages() external view returns (uint256 number) {
return receivedMessages.length;
}
/// @notice Fetches details of a received message by message ID.
/// @dev Reverts if the message ID does not exist.
/// @param messageId The ID of the message whose details are to be fetched.
/// @return sourceChainSelector The source chain identifier (aka selector).
/// @return sender The address of the sender.
/// @return message The received message.
/// @return token The received token.
/// @return amount The received token amount.
function getReceivedMessageDetails(bytes32 messageId)
external
view
returns (uint64 sourceChainSelector, address sender, string memory message, address token, uint256 amount)
{
Message memory detail = messageDetail[messageId];
if (detail.sender == address(0)) revert MessageIdNotExist(messageId);
return (detail.sourceChainSelector, detail.sender, detail.message, detail.token, detail.amount);
}
/// @notice Fetches details of a received message by its position in the received messages list.
/// @dev Reverts if the index is out of bounds.
/// @param index The position in the list of received messages.
/// @return messageId The ID of the message.
/// @return sourceChainSelector The source chain identifier (aka selector).
/// @return sender The address of the sender.
/// @return message The received message.
/// @return token The received token.
/// @return amount The received token amount.
function getReceivedMessageAt(uint256 index)
external
view
returns (
bytes32 messageId,
uint64 sourceChainSelector,
address sender,
string memory message,
address token,
uint256 amount
)
{
if (index >= receivedMessages.length) {
revert IndexOutOfBound(index, receivedMessages.length - 1);
}
messageId = receivedMessages[index];
Message memory detail = messageDetail[messageId];
return (messageId, detail.sourceChainSelector, detail.sender, detail.message, detail.token, detail.amount);
}
/// @notice Fetches the details of the last received message.
/// @dev Reverts if no messages have been received yet.
/// @return messageId The ID of the last received message.
/// @return sourceChainSelector The source chain identifier (aka selector) of the last received message.
/// @return sender The address of the sender of the last received message.
/// @return message The last received message.
/// @return token The last transferred token.
/// @return amount The last transferred token amount.
function getLastReceivedMessageDetails()
external
view
returns (
bytes32 messageId,
uint64 sourceChainSelector,
address sender,
string memory message,
address token,
uint256 amount
)
{
// Revert if no messages have been received
if (receivedMessages.length == 0) revert NoMessageReceived();
// Fetch the last received message ID
messageId = receivedMessages[receivedMessages.length - 1];
// Fetch the details of the last received message
Message memory detail = messageDetail[messageId];
return (messageId, detail.sourceChainSelector, detail.sender, detail.message, detail.token, detail.amount);
}
/// @notice Fallback function to allow the contract to receive Ether.
/// @dev This function has no function body, making it a default function for receiving Ether.
/// It is automatically called when Ether is sent to the contract without any data.
receive() external payable {}
/// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract.
/// @dev This function reverts if there are no funds to withdraw or if the transfer fails.
/// It should only be callable by the owner of the contract.
/// @param beneficiary The address to which the Ether should be sent.
function withdraw(address beneficiary) public onlyOwner {
// Retrieve the balance of this contract
uint256 amount = address(this).balance;
// Revert if there is nothing to withdraw
if (amount == 0) revert NothingToWithdraw();
// Attempt to send the funds, capturing the success status and discarding any return data
(bool sent,) = beneficiary.call{value: amount}("");
// Revert if the send failed, with information about the attempted transfer
if (!sent) revert FailedToWithdrawEth(msg.sender, beneficiary, amount);
}
/// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.
/// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.
/// @param beneficiary The address to which the tokens will be sent.
/// @param token The contract address of the ERC20 token to be withdrawn.
function withdrawToken(address beneficiary, address token) public onlyOwner {
// Retrieve the balance of this contract
uint256 amount = IERC20(token).balanceOf(address(this));
// Revert if there is nothing to withdraw
if (amount == 0) revert NothingToWithdraw();
IERC20(token).safeTransfer(beneficiary, amount);
}
}