@@ -10,6 +10,9 @@ import {ERC20PausableUpgradeable} from
1010 "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol " ;
1111import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol " ;
1212import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol " ;
13+ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol " ;
14+ import {IGmpReceiver} from "@analog-gmp/interfaces/IGmpReceiver.sol " ;
15+ import {IGateway} from "@analog-gmp/interfaces/IGateway.sol " ;
1316
1417/// @notice V1: Roles Model implementation of upgradable ERC20 token.
1518/// This to be used as the initial implementation of UUPS proxy.
@@ -21,15 +24,99 @@ contract AnlogTokenV1 is
2124 ERC20BurnableUpgradeable ,
2225 ERC20PausableUpgradeable ,
2326 AccessControlUpgradeable ,
24- UUPSUpgradeable
27+ UUPSUpgradeable ,
28+ IGmpReceiver
2529{
2630 bytes32 public constant MINTER_ROLE = keccak256 ("MINTER_ROLE " );
2731 bytes32 public constant UPGRADER_ROLE = keccak256 ("UPGRADER_ROLE " );
2832 bytes32 public constant PAUSER_ROLE = keccak256 ("PAUSER_ROLE " );
2933 bytes32 public constant UNPAUSER_ROLE = keccak256 ("UNPAUSER_ROLE " );
3034
35+ /**
36+ * @dev Length of `OutboundTeleportCommand` struct encoded in bytes.
37+ * ```
38+ * uint256 messageLength = abi.encode(OutboundTeleportCommand({from: address(0), to: bytes32(0), amount: 0})).length;
39+ * ```
40+ */
41+ uint256 public constant TELEPORT_COMMAND_ENCODED_LEN = 96 ;
42+
43+ /**
44+ * @dev Minimun gas limit necessary to execute the `onGmpReceived` method defined in this contract.
45+ */
46+ uint256 public constant INBOUND_TRANSFER_GAS_LIMIT = 100_000 ;
47+
48+ /**
49+ * @dev Address of Analog Gateway deployed in the local network, work as "broker" to exchange messages,
50+ * between this contract and the Timechain.
51+ *
52+ * References:
53+ * - Protocol Overview: https://docs.analog.one/documentation/developers/analog-gmp
54+ * - Gateway source-code: https://github.com/Analog-Labs/analog-gmp
55+ */
56+ /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
57+ IGateway public immutable GATEWAY;
58+
59+ /**
60+ * @dev Timechain's Route ID, this is the unique identifier of Timechain's network.
61+ */
62+ /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
63+ uint16 public immutable TIMECHAIN_ROUTE_ID;
64+
65+ /**
66+ * @dev Minimal quantity of tokens allowed per teleport.
67+ *
68+ * IMPORTANT: This value MUST be equal or greater than the timechain's existential deposit.
69+ * see: https://github.com/paritytech/polkadot-sdk/blob/polkadot-v1.17.1/substrate/frame/balances/README.md?plain=1#L24-L29
70+ */
71+ /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
72+ uint256 public immutable MINIMAL_TELEPORT_VALUE;
73+
74+ /**
75+ * @dev Emitted when `amount` tokens are teleported from `source` account in the local network to `recipient` in Timechain.
76+ */
77+ event OutboundTransfer (bytes32 indexed id , address indexed source , bytes32 indexed recipient , uint256 amount );
78+
79+ /**
80+ * @dev @dev Emitted when `amount` tokens are teleported from `source` in Timechain to `recipient` in the local network.
81+ */
82+ event InboundTransfer (bytes32 indexed id , bytes32 indexed source , address indexed recipient , uint256 amount );
83+
84+ /**
85+ * @dev One or more preconditions of `onGmpReceived` method failed.
86+ */
87+ error Unauthorized ();
88+
89+ /**
90+ * @dev Command encoded in the `data` field on the `onGmpReceived` method, representing a teleport from Timechain to the local network.
91+ * @param from Timechain's account teleporting the tokens.
92+ * @param to Local account receing the tokens.
93+ * @param amount The amount of tokens teleported.
94+ */
95+ struct InboundTeleportCommand {
96+ bytes32 from;
97+ address to;
98+ uint256 amount;
99+ }
100+
101+ /**
102+ * @dev Command that that teleports tokens from the local network to the Timechain.
103+ * @param from Account in the local network teleporting the tokens.
104+ * @param to Account in Timechain receing the tokens.
105+ * @param amount The amount of tokens to teleport.
106+ */
107+ struct OutboundTeleportCommand {
108+ address from;
109+ bytes32 to;
110+ uint256 amount;
111+ }
112+
31113 /// @custom:oz-upgrades-unsafe-allow constructor
32- constructor () {
114+ constructor (address gateway , uint16 timechainId , uint256 minimalTeleport ) {
115+ require (gateway.code.length > 0 , "Gateway address is not a contract " );
116+ require (IGateway (gateway).networkId () != timechainId, "local network and Timechain must be different networks " );
117+ GATEWAY = IGateway (gateway);
118+ TIMECHAIN_ROUTE_ID = timechainId;
119+ MINIMAL_TELEPORT_VALUE = minimalTeleport;
33120 _disableInitializers ();
34121 }
35122
@@ -63,14 +150,142 @@ contract AnlogTokenV1 is
63150 _mint (to, amount);
64151 }
65152
153+ /**
154+ * @dev The following functions are overrides required by Solidity.
155+ */
66156 function _authorizeUpgrade (address newImplementation ) internal override onlyRole (UPGRADER_ROLE) {}
67157
68- // The following functions are overrides required by Solidity.
69-
70158 function _update (address from , address to , uint256 value )
71159 internal
72160 override (ERC20Upgradeable , ERC20PausableUpgradeable )
73161 {
74162 super ._update (from, to, value);
75163 }
164+
165+ /**
166+ * @dev Workaround for EVM compatibility, in some chains like `Astar` where `address(this).balance` can
167+ * be less than `msg.value` if this contract has no previous existential deposit.
168+ * Reference:
169+ * - https://github.com/polkadot-evm/frontier/blob/polkadot-v1.11.0/ts-tests/tests/test-balance.ts#L41
170+ */
171+ function _msgValue () private view returns (uint256 ) {
172+ return Math.min (msg .value , address (this ).balance);
173+ }
174+
175+ /**
176+ * @dev Teleport a `value` amount of tokens from the caller's account in the local chain to `to`
177+ * account in the Timechain.
178+ *
179+ * Returns the GMP message identifier.
180+ *
181+ * Requirements:
182+ * - `to` cannot be the zero address.
183+ * - `value` must be equal or greater than `MINIMAL_TELEPORT_VALUE`.
184+ * - the caller must have a balance of at least `value`.
185+ *
186+ * Emits a {OutboundTransfer} event.
187+ */
188+ function teleport (bytes32 to , uint256 value ) external payable returns (bytes32 messageID ) {
189+ return _teleportFrom (_msgSender (), to, value);
190+ }
191+
192+ /**
193+ * @dev Teleports a `value` amount of tokens from `from` account in the local chain to `to` account
194+ * in the Timechain using the allowance mechanism. `value` is then deducted from the caller's
195+ * allowance.
196+ *
197+ * Returns the GMP message identifier.
198+ *
199+ * NOTE: Does not update the allowance if the current allowance
200+ * is the maximum `uint256`.
201+ *
202+ * Requirements:
203+ * - `from` and `to` cannot be the zero address.
204+ * - `from` must have a balance of at least `value`.
205+ * - `value` must be equal or greater than `MINIMAL_TELEPORT_VALUE`.
206+ * - the caller must have allowance for ``from``'s tokens of at least
207+ * `value`.
208+ *
209+ * Emits a {OutboundTransfer} event.
210+ */
211+ function teleportFrom (address from , bytes32 to , uint256 value ) external payable returns (bytes32 messageID ) {
212+ address spender = _msgSender ();
213+ _spendAllowance (from, spender, value);
214+ return _teleportFrom (from, to, value);
215+ }
216+
217+ /**
218+ * @dev Teleports a `value` amount of tokens from `from` account in the local chain to `to` account
219+ * in the Timechain.
220+ *
221+ * Requirements:
222+ * - `from` and `to` cannot be the zero address.
223+ * - `from` must have a balance of at least `value`.
224+ * - `value` must be equal or greater than `MINIMAL_TELEPORT_VALUE`.
225+ *
226+ * Emits a {OutboundTransfer} event.
227+ */
228+ function _teleportFrom (address from , bytes32 to , uint256 value ) private returns (bytes32 messageID ) {
229+ if (from == address (0 )) {
230+ revert ERC20InvalidSender (address (0 ));
231+ }
232+ if (to == bytes32 (bytes20 (address (0 )))) {
233+ revert ERC20InvalidReceiver (address (0 ));
234+ }
235+ require (value >= MINIMAL_TELEPORT_VALUE, "value below minimum required " );
236+ _burn (from, value);
237+ bytes memory message = abi.encode (OutboundTeleportCommand ({from: from, to: to, amount: value}));
238+ messageID = GATEWAY.submitMessage {value: _msgValue ()}(
239+ address (0 ), TIMECHAIN_ROUTE_ID, INBOUND_TRANSFER_GAS_LIMIT, message
240+ );
241+ emit OutboundTransfer (messageID, from, to, value);
242+ }
243+
244+ /**
245+ * @dev Estimate the teleport cost in native tokens, the returned is the amount of ether to send to `teleport` method.
246+ */
247+ function estimateTeleportCost () public view returns (uint256 ) {
248+ return GATEWAY.estimateMessageCost (TIMECHAIN_ROUTE_ID, TELEPORT_COMMAND_ENCODED_LEN, INBOUND_TRANSFER_GAS_LIMIT);
249+ }
250+
251+ /**
252+ * @dev Handles the receipt of a single GMP message.
253+ * The contract must verify the msg.sender, it must be the Gateway Contract address.
254+ *
255+ * @param id The global unique identifier of the message.
256+ * @param network The unique identifier of the source chain who send the message
257+ * @param payload The message payload with no specified format
258+ * @return 32 byte result which will be stored together with GMP message
259+ *
260+ * * Requirements:
261+ * - the caller must be the `GATEWAY` contract.
262+ * - `network` must be the `TIMECHAIN_ROUTE_ID`.
263+ * - `source` must be the `REMOTE_ADDRESS`.
264+ * - `payload` must be the struct `InboundTeleportCommand` encoded.
265+ *
266+ * Emits a {InboundTransfer} event.
267+ */
268+ function onGmpReceived (bytes32 id , uint128 network , bytes32 , bytes calldata payload )
269+ external
270+ payable
271+ returns (bytes32 )
272+ {
273+ // Check preconditions
274+ require (msg .sender == address (GATEWAY), Unauthorized ());
275+ require (network == TIMECHAIN_ROUTE_ID, Unauthorized ());
276+
277+ // Decode the command
278+ InboundTeleportCommand memory command = abi.decode (payload, (InboundTeleportCommand));
279+
280+ // Mint the tokens to the recipient account
281+ if (command.to != address (0 ) && command.amount > 0 ) {
282+ _mint (command.to, command.amount);
283+ }
284+ emit InboundTransfer (id, command.from, command.to, command.amount);
285+
286+ // Returns the current total supply as result, the result is included in the `GmpExecuted` event
287+ // emitted by the gateway. It allows the Timechain to verify if the amount of tokens locked matches
288+ // the total supply of this contract.
289+ return bytes32 (totalSupply ());
290+ }
76291}
0 commit comments