Skip to content

Commit 44ea7b7

Browse files
committed
initial implementation
1 parent 1f40eb5 commit 44ea7b7

File tree

4 files changed

+326
-2
lines changed

4 files changed

+326
-2
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.26;
4+
5+
// Interfaces
6+
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
7+
import {IERC7802} from "@openzeppelin/contracts/interfaces/draft-IERC7802.sol";
8+
import {IERC7786GatewaySource, IERC7786Receiver} from "../interfaces/IERC7786.sol";
9+
10+
// Utilities
11+
import {Arrays} from "@openzeppelin/contracts/utils/Arrays.sol";
12+
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
13+
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
14+
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
15+
import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol";
16+
import {IndirectCall} from "../utils/IndirectCall.sol";
17+
18+
contract ERC7802Bridge is IERC7786Receiver {
19+
using BitMaps for BitMaps.BitMap;
20+
using InteroperableAddress for bytes;
21+
22+
struct BridgeMetadata {
23+
address token;
24+
bool isCustodial;
25+
mapping(bytes chain => address) gateway;
26+
mapping(bytes chain => bytes) remote;
27+
}
28+
29+
mapping(bytes32 bridgeId => BridgeMetadata) private _bridges;
30+
BitMaps.BitMap private _processed;
31+
32+
event Sent(address token, address from, bytes to, uint256 amount);
33+
event Received(address token, bytes from, address to, uint256 amount);
34+
35+
// event GatewayRegistered(address indexed gateway, bytes chain);
36+
// event RemoteBridgeRegistered(address indexed token, bytes chain);
37+
38+
error ERC7802BridgeInvalidBidgeId(bytes32 bridgeId);
39+
error ERC7802BridgeMissingGateway(bytes32 bridgeId, bytes chain);
40+
error ERC7802BridgeMissingRemote(bytes32 bridgeId, bytes chain);
41+
// error ERC7802BridgeImproperChainIdentifier(bytes chain);
42+
error ERC7802BridgeDuplicate();
43+
error ERC7802BridgeInvalidGateway();
44+
error ERC7802BridgeInvalidSender();
45+
46+
function getBridgeEndpoint(bytes32 bridgeId) public returns (address) {
47+
return IndirectCall.getRelayer(bridgeId);
48+
}
49+
50+
function getBridgeToken(bytes32 bridgeId) public view returns (address token, bool isCustodial) {
51+
token = _bridges[bridgeId].token;
52+
isCustodial = _bridges[bridgeId].isCustodial;
53+
if (token == address(0)) revert ERC7802BridgeInvalidBidgeId(bridgeId);
54+
}
55+
56+
function getBridgeGateway(bytes32 bridgeId, bytes memory chain) public view returns (address) {
57+
address result = _bridges[bridgeId].gateway[chain];
58+
if (result == address(0)) revert ERC7802BridgeMissingGateway(bridgeId, chain);
59+
return result;
60+
}
61+
62+
function getBridgeRemote(bytes32 bridgeId, bytes memory chain) public view returns (bytes memory) {
63+
bytes memory result = _bridges[bridgeId].remote[chain];
64+
if (result.length == 0) revert ERC7802BridgeMissingRemote(bridgeId, chain);
65+
return result;
66+
}
67+
68+
struct Foreign {
69+
bytes32 id;
70+
address gateway;
71+
bytes remote;
72+
}
73+
function createBridge(address token, bool isCustodial, Foreign[] calldata foreign) public returns (bytes32) {
74+
bytes32[] memory ids = new bytes32[](foreign.length + 1);
75+
bytes32[] memory links = new bytes32[](foreign.length);
76+
for (uint256 i = 0; i < foreign.length; ++i) {
77+
ids[i] = foreign[i].id;
78+
links[i] = keccak256(
79+
abi.encode(InteroperableAddress.formatEvmV1(block.chainid, foreign[i].gateway), foreign[i].remote)
80+
);
81+
}
82+
ids[foreign.length] = keccak256(
83+
abi.encode(
84+
InteroperableAddress.formatEvmV1(block.chainid, token), // bytes token
85+
bytes32(SafeCast.toUint(isCustodial)), // bytes32 tokenOptions
86+
Arrays.sort(links)
87+
)
88+
);
89+
90+
bytes32 bridgeId = keccak256(abi.encode(Arrays.sort(ids)));
91+
92+
// Should we check for collision. I don't think that is necessary
93+
BridgeMetadata storage details = _bridges[bridgeId];
94+
details.token = token;
95+
details.isCustodial = isCustodial;
96+
97+
for (uint256 i = 0; i < foreign.length; ++i) {
98+
(bytes2 chainType, bytes memory chainReference, ) = foreign[i].remote.parseV1();
99+
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, "");
100+
details.gateway[chain] = foreign[i].gateway;
101+
details.remote[chain] = foreign[i].remote;
102+
}
103+
104+
return bridgeId;
105+
}
106+
107+
function send(
108+
bytes32 bridgeId,
109+
bytes memory to,
110+
uint256 amount,
111+
bytes[] memory attributes
112+
) public payable virtual returns (bytes32) {
113+
address token = _fetchTokens(bridgeId, msg.sender, amount);
114+
115+
// identify destination chain
116+
(bytes2 chainType, bytes memory chainReference, bytes memory recipient) = to.parseV1();
117+
bytes memory destChain = InteroperableAddress.formatV1(chainType, chainReference, "");
118+
119+
// get details for that bridge: gateway, remote bridge, remote token
120+
address gateway = getBridgeGateway(bridgeId, destChain);
121+
bytes memory bridge = getBridgeRemote(bridgeId, destChain);
122+
123+
// prepare payload
124+
bytes memory payload = abi.encode(
125+
bridgeId,
126+
InteroperableAddress.formatEvmV1(block.chainid, msg.sender),
127+
recipient,
128+
amount
129+
);
130+
131+
// send crosschain signal
132+
bytes32 sendId = IERC7786GatewaySource(gateway).sendMessage{value: msg.value}(bridge, payload, attributes);
133+
emit Sent(token, msg.sender, to, amount);
134+
135+
return sendId;
136+
}
137+
138+
function executeMessage(
139+
bytes32 receiveId,
140+
bytes memory sender,
141+
bytes memory payload,
142+
bytes[] memory /*attributes*/
143+
) public payable virtual returns (bytes4) {
144+
// prevent duplicate
145+
require(!_processed.get(uint256(receiveId)), ERC7802BridgeDuplicate());
146+
_processed.set(uint256(receiveId));
147+
148+
// parse payload
149+
(bytes32 bridgeId, bytes memory from, bytes memory recipient, uint256 amount) = abi.decode(
150+
payload,
151+
(bytes32, bytes, bytes, uint256)
152+
);
153+
154+
// identify source chain and validate corresponding gateway
155+
(bytes2 chainType, bytes memory chainReference, ) = from.parseV1();
156+
bytes memory srcChain = InteroperableAddress.formatV1(chainType, chainReference, "");
157+
158+
require(msg.sender == getBridgeGateway(bridgeId, srcChain), ERC7802BridgeInvalidGateway());
159+
require(Bytes.equal(sender, getBridgeRemote(bridgeId, srcChain)), ERC7802BridgeInvalidSender());
160+
161+
// get recipient
162+
address to = address(bytes20(recipient));
163+
164+
// distribute bridged tokens
165+
address token = _distributeTokens(bridgeId, to, amount);
166+
emit Received(token, from, to, amount);
167+
168+
return IERC7786Receiver.executeMessage.selector;
169+
}
170+
171+
function _fetchTokens(bytes32 bridgeId, address from, uint256 amount) internal virtual returns (address) {
172+
(address token, bool isCustodial) = getBridgeToken(bridgeId);
173+
if (isCustodial) {
174+
(bool success, bytes memory returndata) = IndirectCall.indirectCall(
175+
token,
176+
abi.encodeCall(IERC20.transferFrom, (from, getBridgeEndpoint(bridgeId), amount)),
177+
bridgeId
178+
);
179+
require(success && (returndata.length == 0 ? token.code.length == 0 : uint256(bytes32(returndata)) == 1));
180+
} else {
181+
(bool success, ) = IndirectCall.indirectCall(
182+
token,
183+
abi.encodeCall(IERC7802.crosschainBurn, (from, amount)),
184+
bridgeId
185+
);
186+
require(success);
187+
}
188+
return token;
189+
}
190+
191+
function _distributeTokens(bytes32 bridgeId, address to, uint256 amount) internal virtual returns (address) {
192+
(address token, bool isCustodial) = getBridgeToken(bridgeId);
193+
if (isCustodial) {
194+
(bool success, bytes memory returndata) = IndirectCall.indirectCall(
195+
token,
196+
abi.encodeCall(IERC20.transfer, (to, amount)),
197+
bridgeId
198+
);
199+
require(success && (returndata.length == 0 ? token.code.length == 0 : uint256(bytes32(returndata)) == 1));
200+
} else {
201+
(bool success, ) = IndirectCall.indirectCall(
202+
token,
203+
abi.encodeCall(IERC7802.crosschainMint, (to, amount)),
204+
bridgeId
205+
);
206+
require(success);
207+
}
208+
return token;
209+
}
210+
}

contracts/utils/IndirectCall.sol

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
6+
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
7+
8+
/**
9+
* @dev Helper contract for performing potentially dangerous calls through a relay the hide the address of the
10+
* original sender.
11+
*
12+
* Some contract are required to perform arbitrary action controlled by user input. This is dangerous if the contract
13+
* has special permissions, or holds assets. In such cases, using a relay contract can be useful to change the
14+
* msg.sender of the outgoing call. This pattern is used in the ERC-4337 entrypoint that relies on a helper called the
15+
* "senderCreator" when calling account factories. Similarly ERC-6942 does factory calls that could be dangerous if
16+
* performed directly.
17+
*
18+
* This contract provides a `indirectCall` that can be used to perform dangerous calls. These calls are indirect
19+
* through a minimal relayer.
20+
*/
21+
library IndirectCall {
22+
function indirectCall(address target, bytes memory data) internal returns (bool, bytes memory) {
23+
return indirectCall(target, 0, data);
24+
}
25+
26+
function indirectCall(address target, uint256 value, bytes memory data) internal returns (bool, bytes memory) {
27+
return indirectCall(target, value, data, bytes32(0));
28+
}
29+
30+
function indirectCall(address target, bytes memory data, bytes32 salt) internal returns (bool, bytes memory) {
31+
return indirectCall(target, 0, data, salt);
32+
}
33+
34+
function indirectCall(
35+
address target,
36+
uint256 value,
37+
bytes memory data,
38+
bytes32 salt
39+
) internal returns (bool, bytes memory) {
40+
return getRelayer(salt).call{value: value}(abi.encodePacked(target, data));
41+
}
42+
43+
function getRelayer() internal returns (address) {
44+
return getRelayer(bytes32(0));
45+
}
46+
47+
function getRelayer(bytes32 salt) internal returns (address) {
48+
// [Relayer details]
49+
//
50+
// deployment prefix: 3d602f80600a3d3981f3
51+
// deployed bytecode: 60133611600a575f5ffd5b6014360360145f375f5f601436035f345f3560601c5af13d5f5f3e5f3d91602d57fd5bf3
52+
// bytecode hash: 7bc0ea09c689dc0a6de3865d8789dae51a081efcf6569589ddae4b677df5dd3f
53+
//
54+
// offset | bytecode | opcode | stack
55+
// -------|----------|----------------|--------
56+
// 0x0000 | 6013 | push1 0x13 | 0x13
57+
// 0x0002 | 36 | calldatasize | cds 0x13
58+
// 0x0003 | 11 | gt | (cds>0x13)
59+
// 0x0004 | 600a | push1 0x0a | 0x0a (cds>0x13)
60+
// 0x0006 | 57 | jumpi | 0x0a (cds>0x13)
61+
// 0x0007 | 5f | push0 | 0
62+
// 0x0008 | 5f | push0 | 0 0
63+
// 0x0009 | fd | revert |
64+
// 0x000a | 5b | jumpdest |
65+
// 0x000b | 6014 | push1 0x14 | 0x14
66+
// 0x000d | 36 | calldatasize | cds 0x14
67+
// 0x000e | 03 | sub | (cds-0x14)
68+
// 0x000f | 6014 | push1 0x14 | 0x14 (cds-0x14)
69+
// 0x0011 | 5f | push0 | 0 0x14 (cds-0x14)
70+
// 0x0012 | 37 | calldatacopy |
71+
// 0x0013 | 5f | push0 | 0
72+
// 0x0014 | 5f | push0 | 0 0
73+
// 0x0015 | 6014 | push1 0x14 | 0x14 0 0
74+
// 0x0017 | 36 | calldatasize | cds 0x14 0 0
75+
// 0x0018 | 03 | sub | (cds-0x14) 0 0
76+
// 0x0019 | 5f | push0 | 0 (cds-0x14) 0 0
77+
// 0x001a | 34 | callvalue | value 0 (cds-0x14) 0 0
78+
// 0x001b | 5f | push0 | 0 value 0 (cds-0x14) 0 0
79+
// 0x001c | 35 | calldataload | cd[0] value 0 (cds-0x14) 0 0
80+
// 0x001d | 6060 | push1 0x60 | 0x60 cd[0] value 0 (cds-0x14) 0 0
81+
// 0x001f | 1c | shr | target value 0 (cds-0x14) 0 0
82+
// 0x0020 | 5a | gas | gas target value 0 (cds-0x14) 0 0
83+
// 0x0021 | f1 | call | suc
84+
// 0x0022 | 3d | returndatasize | rds suc
85+
// 0x0023 | 5f | push0 | 0 rds suc
86+
// 0x0024 | 5f | push0 | 0 0 rds suc
87+
// 0x0025 | 3e | returndatacopy | suc
88+
// 0x0026 | 5f | push0 | 0 suc
89+
// 0x0027 | 3d | returndatasize | rds 0 suc
90+
// 0x0028 | 91 | swap2 | suc 0 rds
91+
// 0x0029 | 602d | push1 0x2d | 0x2d suc 0 rds
92+
// 0x002b | 57 | jumpi | 0 rds
93+
// 0x002c | fd | revert |
94+
// 0x002d | 5b | jumpdest | 0 rds
95+
// 0x002e | f3 | return |
96+
97+
// Create2 address computation, and deploy it if not yet available
98+
address relayer = Create2.computeAddress(
99+
salt,
100+
0x7bc0ea09c689dc0a6de3865d8789dae51a081efcf6569589ddae4b677df5dd3f
101+
);
102+
if (relayer.code.length == 0) {
103+
assembly ("memory-safe") {
104+
mstore(0x19, 0x1436035f345f3560601c5af13d5f5f3e5f3d91602d57fd5bf3)
105+
mstore(0x00, 0x3d602f80600a3d3981f360133611600a575f5ffd5b6014360360145f375f5f60)
106+
if iszero(create2(0, 0, 0x39, salt)) {
107+
returndatacopy(0, 0, returndatasize())
108+
revert(0, returndatasize())
109+
}
110+
}
111+
}
112+
return relayer;
113+
}
114+
}

lib/@openzeppelin-contracts

0 commit comments

Comments
 (0)