-
Notifications
You must be signed in to change notification settings - Fork 23
Add an ERC-7802 bridge (v2) #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Amxx
wants to merge
16
commits into
master
Choose a base branch
from
feature/erc7802bridge-v2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
44ea7b7
initial implementation
Amxx 2a8e5a9
add tests
Amxx 3cc93f1
Merge branch 'feature/erc7802bridge-v2' of https://github.com/OpenZep…
Amxx 6ccf896
fix
Amxx 2a442fe
rebuild package-lock.json
Amxx fca7c03
fix pragma validity checks
Amxx 3c1cf8c
up
Amxx 2edf9c7
fix bug commit from IndirectCall items not having access control + ma…
Amxx 833873f
use the version of IndirectCall that verifies call origin
Amxx 8b99a54
use IndirectCall lib for triggering calls
Amxx de057ba
test that endpoint cannot be called directly by a third party
Amxx 0135754
Update ERC7786Receiver
Amxx 107d9ef
Merge branch 'erc7786/receiveMessage-without-attributes' into feature…
Amxx 31f25b9
update
Amxx 86a9e05
Merge branch 'master' into feature/erc7802bridge-v2
Amxx 4bb0096
modularise half-bridge creations
Amxx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,293 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.27; | ||
|
||
// Interfaces | ||
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; | ||
import {IERC7802} from "@openzeppelin/contracts/interfaces/draft-IERC7802.sol"; | ||
import {IERC7786GatewaySource, IERC7786Receiver} from "../interfaces/IERC7786.sol"; | ||
|
||
// Utilities | ||
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | ||
import {Arrays} from "@openzeppelin/contracts/utils/Arrays.sol"; | ||
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; | ||
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; | ||
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; | ||
import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol"; | ||
import {IndirectCall} from "../utils/IndirectCall.sol"; | ||
|
||
abstract contract ERC7802Bridge is ERC721("ERC7802Bridge", "ERC7802Bridge"), IERC7786Receiver { | ||
using BitMaps for BitMaps.BitMap; | ||
using InteroperableAddress for bytes; | ||
|
||
struct BridgeMetadata { | ||
address token; | ||
bool isPaused; | ||
bool isCustodial; | ||
mapping(bytes chain => address) gateway; | ||
mapping(bytes chain => bytes) remote; | ||
} | ||
|
||
mapping(bytes32 bridgeId => BridgeMetadata) private _bridges; | ||
BitMaps.BitMap private _processed; | ||
|
||
event Sent(address token, address from, bytes to, uint256 amount); | ||
event Received(address token, bytes from, address to, uint256 amount); | ||
event BridgePaused(bytes32 indexed bridgeId, bool isPaused); | ||
event BridgeLinkSet(bytes32 indexed bridgeId, address gateway, bytes remote); | ||
|
||
error ERC7802BridgePaused(bytes32 bridgeId); | ||
error ERC7802BridgeInvalidBidgeId(bytes32 bridgeId); | ||
error ERC7802BridgeMissingGateway(bytes32 bridgeId, bytes chain); | ||
error ERC7802BridgeMissingRemote(bytes32 bridgeId, bytes chain); | ||
error ERC7802BridgeDuplicate(); | ||
error ERC7802BridgeInvalidGateway(); | ||
error ERC7802BridgeInvalidSender(); | ||
|
||
modifier bridgeAdminRestricted(bytes32 bridgeId) { | ||
_checkAuthorized(ownerOf(uint256(bridgeId)), msg.sender, uint256(bridgeId)); | ||
_; | ||
} | ||
|
||
// ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | ||
// │ Getters │ | ||
// └─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | ||
function getBridgeEndpoint(bytes32 bridgeId) public returns (address) { | ||
return IndirectCall.getRelayer(bridgeId); | ||
} | ||
|
||
function getBridgeToken(bytes32 bridgeId) public view returns (address token, bool isCustodial) { | ||
_requireOwned(uint256(bridgeId)); | ||
return (_bridges[bridgeId].token, _bridges[bridgeId].isCustodial); | ||
} | ||
|
||
function getBridgeGateway(bytes32 bridgeId, bytes memory chain) public view returns (address) { | ||
_requireOwned(uint256(bridgeId)); | ||
return _bridges[bridgeId].gateway[chain]; | ||
} | ||
|
||
function getBridgeRemote(bytes32 bridgeId, bytes memory chain) public view returns (bytes memory) { | ||
_requireOwned(uint256(bridgeId)); | ||
return _bridges[bridgeId].remote[chain]; | ||
} | ||
|
||
// ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | ||
// │ Bridge management │ | ||
// └─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | ||
|
||
function setPaused(bytes32 bridgeId, bool isPaused) public bridgeAdminRestricted(bridgeId) { | ||
_setPaused(bridgeId, isPaused); | ||
} | ||
|
||
function updateGateway( | ||
bytes32 bridgeId, | ||
bytes memory chain, | ||
address gateway, | ||
bytes memory remote | ||
) public virtual bridgeAdminRestricted(bridgeId) { | ||
_setGateway(bridgeId, chain, gateway, remote); | ||
} | ||
|
||
function _setBridge(bytes32 bridgeId, address token, address admin, bool isCustodial) internal { | ||
_safeMint(admin == address(0) ? address(1) : admin, uint256(bridgeId)); | ||
_bridges[bridgeId].token = token; | ||
_bridges[bridgeId].isCustodial = isCustodial; | ||
} | ||
|
||
function _setGateway(bytes32 bridgeId, bytes memory chain, address gateway, bytes memory remote) internal { | ||
_bridges[bridgeId].gateway[chain] = gateway; | ||
_bridges[bridgeId].remote[chain] = remote; | ||
emit BridgeLinkSet(bridgeId, gateway, remote); | ||
} | ||
|
||
function _setPaused(bytes32 bridgeId, bool isPaused) internal { | ||
_bridges[bridgeId].isPaused = isPaused; | ||
emit BridgePaused(bridgeId, isPaused); | ||
} | ||
|
||
// ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | ||
// │ Send / Receive tokens │ | ||
// └─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | ||
function send( | ||
bytes32 bridgeId, | ||
bytes memory to, | ||
uint256 amount, | ||
bytes[] memory attributes | ||
) public payable virtual returns (bytes32) { | ||
_requireOwned(uint256(bridgeId)); | ||
|
||
require(!_bridges[bridgeId].isPaused, ERC7802BridgePaused(bridgeId)); | ||
|
||
address token = _fetchTokens(bridgeId, msg.sender, amount); | ||
|
||
// identify destination chain | ||
(bytes2 chainType, bytes memory chainReference, bytes memory recipient) = to.parseV1(); | ||
bytes memory destChain = InteroperableAddress.formatV1(chainType, chainReference, ""); | ||
|
||
// get details for that bridge: gateway, remote bridge, remote token | ||
address gateway = _bridges[bridgeId].gateway[destChain]; | ||
bytes memory bridge = _bridges[bridgeId].remote[destChain]; | ||
|
||
// prepare payload | ||
bytes memory payload = abi.encode( | ||
bridgeId, | ||
InteroperableAddress.formatEvmV1(block.chainid, msg.sender), | ||
recipient, | ||
amount | ||
); | ||
|
||
// send crosschain signal | ||
bytes32 sendId = IERC7786GatewaySource(gateway).sendMessage{value: msg.value}(bridge, payload, attributes); | ||
emit Sent(token, msg.sender, to, amount); | ||
|
||
return sendId; | ||
} | ||
|
||
function receiveMessage( | ||
bytes32 receiveId, | ||
bytes memory sender, | ||
bytes memory payload | ||
) public payable virtual returns (bytes4) { | ||
// prevent duplicate | ||
require(!_processed.get(uint256(receiveId)), ERC7802BridgeDuplicate()); | ||
_processed.set(uint256(receiveId)); | ||
|
||
// parse payload | ||
(bytes32 bridgeId, bytes memory from, bytes memory recipient, uint256 amount) = abi.decode( | ||
payload, | ||
(bytes32, bytes, bytes, uint256) | ||
); | ||
|
||
_requireOwned(uint256(bridgeId)); | ||
|
||
// identify source chain and validate corresponding gateway | ||
(bytes2 chainType, bytes memory chainReference, ) = from.parseV1(); | ||
bytes memory srcChain = InteroperableAddress.formatV1(chainType, chainReference, ""); | ||
|
||
require(msg.sender == _bridges[bridgeId].gateway[srcChain], ERC7802BridgeInvalidGateway()); | ||
require(Bytes.equal(sender, _bridges[bridgeId].remote[srcChain]), ERC7802BridgeInvalidSender()); | ||
|
||
// get recipient | ||
address to = address(bytes20(recipient)); | ||
|
||
// distribute bridged tokens | ||
address token = _distributeTokens(bridgeId, to, amount); | ||
emit Received(token, from, to, amount); | ||
|
||
return IERC7786Receiver.receiveMessage.selector; | ||
} | ||
|
||
function _fetchTokens(bytes32 bridgeId, address from, uint256 amount) private returns (address) { | ||
address token = _bridges[bridgeId].token; | ||
if (_bridges[bridgeId].isCustodial) { | ||
(bool success, bytes memory returndata) = IndirectCall.indirectCall( | ||
token, | ||
abi.encodeCall(IERC20.transferFrom, (from, getBridgeEndpoint(bridgeId), amount)), | ||
bridgeId | ||
); | ||
require(success && (returndata.length == 0 ? token.code.length == 0 : uint256(bytes32(returndata)) == 1)); | ||
} else { | ||
(bool success, ) = IndirectCall.indirectCall( | ||
token, | ||
abi.encodeCall(IERC7802.crosschainBurn, (from, amount)), | ||
bridgeId | ||
); | ||
require(success); | ||
} | ||
return token; | ||
} | ||
|
||
function _distributeTokens(bytes32 bridgeId, address to, uint256 amount) private returns (address) { | ||
address token = _bridges[bridgeId].token; | ||
if (_bridges[bridgeId].isCustodial) { | ||
(bool success, bytes memory returndata) = IndirectCall.indirectCall( | ||
token, | ||
abi.encodeCall(IERC20.transfer, (to, amount)), | ||
bridgeId | ||
); | ||
require(success && (returndata.length == 0 ? token.code.length == 0 : uint256(bytes32(returndata)) == 1)); | ||
} else { | ||
(bool success, ) = IndirectCall.indirectCall( | ||
token, | ||
abi.encodeCall(IERC7802.crosschainMint, (to, amount)), | ||
bridgeId | ||
); | ||
require(success); | ||
} | ||
return token; | ||
} | ||
} | ||
|
||
contract ERC7802BridgeLinks is ERC7802Bridge { | ||
function createBridge(address token, bool isCustodial, bytes32 salt) public returns (bytes32) { | ||
bytes32 bridgeId = keccak256(abi.encodePacked(msg.sender, salt)); | ||
|
||
_setBridge(bridgeId, token, msg.sender, isCustodial); | ||
|
||
return bridgeId; | ||
} | ||
} | ||
|
||
contract ERC7802BridgeCounterfactual is ERC7802Bridge { | ||
using InteroperableAddress for bytes; | ||
|
||
struct Foreign { | ||
bytes32 id; | ||
address gateway; | ||
bytes remote; | ||
} | ||
|
||
function createBridge( | ||
address token, | ||
address admin, | ||
bool isCustodial, | ||
Foreign[] calldata foreign | ||
) public returns (bytes32) { | ||
bytes32 bridgeId = _counterfactualBridgeId( | ||
token, | ||
bytes32(bytes20(admin)) | bytes32(SafeCast.toUint(isCustodial)), | ||
foreign | ||
); | ||
|
||
_setBridge(bridgeId, token, admin, isCustodial); | ||
for (uint256 i = 0; i < foreign.length; ++i) { | ||
(bytes2 chainType, bytes memory chainReference, ) = foreign[i].remote.parseV1Calldata(); | ||
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, ""); | ||
_setGateway(bridgeId, chain, foreign[i].gateway, foreign[i].remote); | ||
} | ||
|
||
return bridgeId; | ||
} | ||
|
||
function updateGateway( | ||
bytes32 bridgeId, | ||
bytes memory chain, | ||
address gateway, | ||
bytes memory remote | ||
) public virtual override { | ||
require(gateway != address(0) && remote.length > 0); | ||
// super call is bridgeAdminRestricted(bridgeId) | ||
super.updateGateway(bridgeId, chain, gateway, remote); | ||
} | ||
|
||
function _counterfactualBridgeId( | ||
address token, | ||
bytes32 opts, | ||
Foreign[] calldata foreign | ||
) private view returns (bytes32) { | ||
bytes32[] memory ids = new bytes32[](foreign.length + 1); | ||
bytes32[] memory links = new bytes32[](foreign.length); | ||
for (uint256 i = 0; i < foreign.length; ++i) { | ||
require(foreign[i].gateway != address(0) && foreign[i].remote.length > 0); | ||
ids[i] = foreign[i].id; | ||
links[i] = keccak256( | ||
abi.encode(InteroperableAddress.formatEvmV1(block.chainid, foreign[i].gateway), foreign[i].remote) | ||
); | ||
} | ||
ids[foreign.length] = keccak256( | ||
abi.encode(InteroperableAddress.formatEvmV1(block.chainid, token), opts, Arrays.sort(links)) | ||
); | ||
|
||
return keccak256(abi.encodePacked(Arrays.sort(ids))); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.20; | ||
|
||
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; | ||
import {ERC20Bridgeable} from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Bridgeable.sol"; | ||
|
||
abstract contract ERC20BridgeableMock is ERC20Bridgeable, AccessControl { | ||
bytes32 public constant BRIDGE_ROLE = keccak256("BRIDGE"); | ||
|
||
constructor(address admin) { | ||
_grantRole(DEFAULT_ADMIN_ROLE, admin); | ||
} | ||
|
||
function supportsInterface( | ||
bytes4 interfaceId | ||
) public view virtual override(AccessControl, ERC20Bridgeable) returns (bool) { | ||
return super.supportsInterface(interfaceId); | ||
} | ||
|
||
function _checkTokenBridge(address sender) internal view override onlyRole(BRIDGE_ROLE) {} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently, it is possible to add new chains to a bridgeId. This should not be allowed unless there is a mechanism to create the bridgeId on that new chain that is not part of the initial config.