Skip to content

Commit ebfa4c6

Browse files
Lohannagryaznov
andauthored
Feature - Support GMP Token Bridge (#12)
* forge install: analog-gmp * Integrate with GMP * update docs * remove typo * fix anothe typo * gmp bridge-related fixes (#13) * fixes to make it compile * deployment script update * rm REMOTE_ADDRESS * make AnlogTokenV1Test work --------- Co-authored-by: Alexander Gryaznov <[email protected]>
1 parent 2c6025e commit ebfa4c6

File tree

7 files changed

+256
-8
lines changed

7 files changed

+256
-8
lines changed

.env.anvil

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ export UPGRADER=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
99
export PAUSER=0x90F79bf6EB2c4f870365E785982E1f101E93b906
1010
# well-known (thus key compromised) Anvil default account(4)
1111
export UNPAUSER=0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
12+
# Teleport-specific
13+
export GATEWAY=0x49877F1e26d523e716d941a424af46B86EcaF09E
14+
export TIMECHAIN_ROUTE_ID=1000
15+
export MINIMAL_TELEPORT_VALUE=1000000000000

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77
[submodule "lib/openzeppelin-contracts-upgradeable"]
88
path = lib/openzeppelin-contracts-upgradeable
99
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
10+
[submodule "lib/analog-gmp"]
11+
path = lib/analog-gmp
12+
url = https://github.com/Analog-Labs/analog-gmp

lib/analog-gmp

Submodule analog-gmp added at 8db5a43

remappings.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts
22
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts
3+
@analog-gmp/=lib/analog-gmp/src

script/00_Deploy.s.sol

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.13;
33

44
import {Script, console} from "forge-std/Script.sol";
5-
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
5+
import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
66
import {AnlogTokenV1} from "../src/AnlogTokenV1.sol";
77

88
contract AnlogTokenScript is Script {
@@ -18,10 +18,18 @@ contract AnlogTokenScript is Script {
1818
address pauser = vm.envAddress("PAUSER");
1919
address unpauser = vm.envAddress("UNPAUSER");
2020

21+
// Teleport-related
22+
address gateway = vm.envAddress("GATEWAY");
23+
uint16 timechainId = uint16(vm.envUint("TIMECHAIN_ROUTE_ID"));
24+
uint256 minimalTeleport = vm.envUint("MINIMAL_TELEPORT_VALUE");
25+
2126
vm.startBroadcast(deployer);
2227

28+
Options memory opts;
29+
opts.constructorData = abi.encode(gateway, timechainId, minimalTeleport);
30+
2331
address proxyAddress = Upgrades.deployUUPSProxy(
24-
"AnlogTokenV1.sol", abi.encodeCall(AnlogTokenV1.initialize, (minter, upgrader, pauser, unpauser))
32+
"AnlogTokenV1.sol", abi.encodeCall(AnlogTokenV1.initialize, (minter, upgrader, pauser, unpauser)), opts
2533
);
2634

2735
vm.stopBroadcast();

src/AnlogTokenV1.sol

Lines changed: 219 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {ERC20PausableUpgradeable} from
1010
"@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol";
1111
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
1212
import {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
}

test/AnlogTokenV1.t.sol

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.22;
33

44
import {Test, console} from "forge-std/Test.sol";
5-
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
5+
import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
66
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
77
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
88
import {AnlogTokenV1} from "../src/AnlogTokenV1.sol";
@@ -19,13 +19,29 @@ contract AnlogTokenV1Test is Test {
1919
address constant UNPAUSER = address(3);
2020
address constant NEW_MINTER = address(4);
2121

22+
// Teleport-related
23+
address constant GATEWAY = 0xEb73D0D236DE8F8D09dc6A52916e5849ff1E8dfA;
24+
uint16 constant TIMECHAIN_ID = 1000;
25+
uint256 constant MIN_TELEPORT_VAL = 1000000000000;
26+
27+
// fork testing
28+
string SEPOLIA_RPC_URL = vm.envString("SEPOLIA_RPC_URL");
29+
uint256 sepoliaFork;
30+
2231
/// @notice deploys an UUPS proxy.
2332
/// Here we start with the V1 implementation right away.
2433
/// For V0->V1 upgrade see another test.
2534
function setUp() public {
35+
sepoliaFork = vm.createFork(SEPOLIA_RPC_URL);
36+
vm.selectFork(sepoliaFork);
37+
assertEq(vm.activeFork(), sepoliaFork);
38+
39+
Options memory opts;
40+
opts.constructorData = abi.encode(GATEWAY, TIMECHAIN_ID, MIN_TELEPORT_VAL);
41+
2642
// deploy proxy with a distinct address assigned to each role
2743
address proxy = Upgrades.deployUUPSProxy(
28-
"AnlogTokenV1.sol", abi.encodeCall(AnlogTokenV1.initialize, (MINTER, UPGRADER, PAUSER, UNPAUSER))
44+
"AnlogTokenV1.sol", abi.encodeCall(AnlogTokenV1.initialize, (MINTER, UPGRADER, PAUSER, UNPAUSER)), opts
2945
);
3046
token = AnlogTokenV1(proxy);
3147
}

0 commit comments

Comments
 (0)