Skip to content

Commit 3cc93f1

Browse files
committed
Merge branch 'feature/erc7802bridge-v2' of https://github.com/OpenZeppelin/openzeppelin-community-contracts into feature/erc7802bridge-v2
2 parents 1451a5d + 2a8e5a9 commit 3cc93f1

File tree

4 files changed

+492
-0
lines changed

4 files changed

+492
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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+
event NewBridge(bytes32 indexed bridgeId, address indexed token);
35+
event NewBridgeLink(bytes32 indexed bridgeId, address gateway, bytes remote);
36+
37+
error ERC7802BridgeInvalidBidgeId(bytes32 bridgeId);
38+
error ERC7802BridgeMissingGateway(bytes32 bridgeId, bytes chain);
39+
error ERC7802BridgeMissingRemote(bytes32 bridgeId, bytes chain);
40+
error ERC7802BridgeDuplicate();
41+
error ERC7802BridgeInvalidGateway();
42+
error ERC7802BridgeInvalidSender();
43+
44+
function getBridgeEndpoint(bytes32 bridgeId) public returns (address) {
45+
return IndirectCall.getRelayer(bridgeId);
46+
}
47+
48+
function getBridgeToken(bytes32 bridgeId) public view returns (address token, bool isCustodial) {
49+
token = _bridges[bridgeId].token;
50+
isCustodial = _bridges[bridgeId].isCustodial;
51+
if (token == address(0)) revert ERC7802BridgeInvalidBidgeId(bridgeId);
52+
}
53+
54+
function getBridgeGateway(bytes32 bridgeId, bytes memory chain) public view returns (address) {
55+
address result = _bridges[bridgeId].gateway[chain];
56+
if (result == address(0)) revert ERC7802BridgeMissingGateway(bridgeId, chain);
57+
return result;
58+
}
59+
60+
function getBridgeRemote(bytes32 bridgeId, bytes memory chain) public view returns (bytes memory) {
61+
bytes memory result = _bridges[bridgeId].remote[chain];
62+
if (result.length == 0) revert ERC7802BridgeMissingRemote(bridgeId, chain);
63+
return result;
64+
}
65+
66+
struct Foreign {
67+
bytes32 id;
68+
address gateway;
69+
bytes remote;
70+
}
71+
function createBridge(address token, bool isCustodial, Foreign[] calldata foreign) public returns (bytes32) {
72+
bytes32[] memory ids = new bytes32[](foreign.length + 1);
73+
bytes32[] memory links = new bytes32[](foreign.length);
74+
for (uint256 i = 0; i < foreign.length; ++i) {
75+
require(foreign[i].gateway != address(0));
76+
require(foreign[i].remote.length > 0);
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.encodePacked(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+
emit NewBridge(bridgeId, token);
98+
99+
for (uint256 i = 0; i < foreign.length; ++i) {
100+
(bytes2 chainType, bytes memory chainReference, ) = foreign[i].remote.parseV1();
101+
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, "");
102+
require(details.gateway[chain] == address(0));
103+
details.gateway[chain] = foreign[i].gateway;
104+
details.remote[chain] = foreign[i].remote;
105+
106+
emit NewBridgeLink(bridgeId, foreign[i].gateway, foreign[i].remote);
107+
}
108+
109+
return bridgeId;
110+
}
111+
112+
function send(
113+
bytes32 bridgeId,
114+
bytes memory to,
115+
uint256 amount,
116+
bytes[] memory attributes
117+
) public payable virtual returns (bytes32) {
118+
address token = _fetchTokens(bridgeId, msg.sender, amount);
119+
120+
// identify destination chain
121+
(bytes2 chainType, bytes memory chainReference, bytes memory recipient) = to.parseV1();
122+
bytes memory destChain = InteroperableAddress.formatV1(chainType, chainReference, "");
123+
124+
// get details for that bridge: gateway, remote bridge, remote token
125+
address gateway = getBridgeGateway(bridgeId, destChain);
126+
bytes memory bridge = getBridgeRemote(bridgeId, destChain);
127+
128+
// prepare payload
129+
bytes memory payload = abi.encode(
130+
bridgeId,
131+
InteroperableAddress.formatEvmV1(block.chainid, msg.sender),
132+
recipient,
133+
amount
134+
);
135+
136+
// send crosschain signal
137+
bytes32 sendId = IERC7786GatewaySource(gateway).sendMessage{value: msg.value}(bridge, payload, attributes);
138+
emit Sent(token, msg.sender, to, amount);
139+
140+
return sendId;
141+
}
142+
143+
function executeMessage(
144+
bytes32 receiveId,
145+
bytes memory sender,
146+
bytes memory payload,
147+
bytes[] memory /*attributes*/
148+
) public payable virtual returns (bytes4) {
149+
// prevent duplicate
150+
require(!_processed.get(uint256(receiveId)), ERC7802BridgeDuplicate());
151+
_processed.set(uint256(receiveId));
152+
153+
// parse payload
154+
(bytes32 bridgeId, bytes memory from, bytes memory recipient, uint256 amount) = abi.decode(
155+
payload,
156+
(bytes32, bytes, bytes, uint256)
157+
);
158+
159+
// identify source chain and validate corresponding gateway
160+
(bytes2 chainType, bytes memory chainReference, ) = from.parseV1();
161+
bytes memory srcChain = InteroperableAddress.formatV1(chainType, chainReference, "");
162+
163+
require(msg.sender == getBridgeGateway(bridgeId, srcChain), ERC7802BridgeInvalidGateway());
164+
require(Bytes.equal(sender, getBridgeRemote(bridgeId, srcChain)), ERC7802BridgeInvalidSender());
165+
166+
// get recipient
167+
address to = address(bytes20(recipient));
168+
169+
// distribute bridged tokens
170+
address token = _distributeTokens(bridgeId, to, amount);
171+
emit Received(token, from, to, amount);
172+
173+
return IERC7786Receiver.executeMessage.selector;
174+
}
175+
176+
function _fetchTokens(bytes32 bridgeId, address from, uint256 amount) internal virtual returns (address) {
177+
(address token, bool isCustodial) = getBridgeToken(bridgeId);
178+
if (isCustodial) {
179+
(bool success, bytes memory returndata) = IndirectCall.indirectCall(
180+
token,
181+
abi.encodeCall(IERC20.transferFrom, (from, getBridgeEndpoint(bridgeId), amount)),
182+
bridgeId
183+
);
184+
require(success && (returndata.length == 0 ? token.code.length == 0 : uint256(bytes32(returndata)) == 1));
185+
} else {
186+
(bool success, ) = IndirectCall.indirectCall(
187+
token,
188+
abi.encodeCall(IERC7802.crosschainBurn, (from, amount)),
189+
bridgeId
190+
);
191+
require(success);
192+
}
193+
return token;
194+
}
195+
196+
function _distributeTokens(bytes32 bridgeId, address to, uint256 amount) internal virtual returns (address) {
197+
(address token, bool isCustodial) = getBridgeToken(bridgeId);
198+
if (isCustodial) {
199+
(bool success, bytes memory returndata) = IndirectCall.indirectCall(
200+
token,
201+
abi.encodeCall(IERC20.transfer, (to, amount)),
202+
bridgeId
203+
);
204+
require(success && (returndata.length == 0 ? token.code.length == 0 : uint256(bytes32(returndata)) == 1));
205+
} else {
206+
(bool success, ) = IndirectCall.indirectCall(
207+
token,
208+
abi.encodeCall(IERC7802.crosschainMint, (to, amount)),
209+
bridgeId
210+
);
211+
require(success);
212+
}
213+
return token;
214+
}
215+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
6+
import {ERC20Bridgeable} from "../../token/ERC20/extensions/ERC20Bridgeable.sol";
7+
8+
abstract contract ERC20BridgeableMock is ERC20Bridgeable, AccessControl {
9+
bytes32 public constant BRIDGE_ROLE = keccak256("BRIDGE");
10+
11+
constructor(address admin) {
12+
_grantRole(DEFAULT_ADMIN_ROLE, admin);
13+
}
14+
15+
function supportsInterface(
16+
bytes4 interfaceId
17+
) public view virtual override(AccessControl, ERC20Bridgeable) returns (bool) {
18+
return super.supportsInterface(interfaceId);
19+
}
20+
21+
function _checkTokenBridge(address sender) internal view override onlyRole(BRIDGE_ROLE) {}
22+
}

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+
}

0 commit comments

Comments
 (0)