Skip to content

Commit 5c79432

Browse files
Amxxernestognwarr00
authored
ERC20Bridgable (ERC-7802) (#5735)
Co-authored-by: ernestognw <ernestognw@gmail.com> Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
1 parent bbc4d7a commit 5c79432

File tree

7 files changed

+206
-0
lines changed

7 files changed

+206
-0
lines changed

.changeset/ripe-bears-hide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC20Bridgeable`: Implementation of ERC-7802 that makes an ERC-20 compatible with crosschain bridges.

contracts/interfaces/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ are useful to interact with third party contracts that implement them.
4545
- {IERC6909Metadata}
4646
- {IERC6909TokenSupply}
4747
- {IERC7674}
48+
- {IERC7802}
4849

4950
== Detailed ABI
5051

@@ -97,3 +98,5 @@ are useful to interact with third party contracts that implement them.
9798
{{IERC6909TokenSupply}}
9899

99100
{{IERC7674}}
101+
102+
{{IERC7802}}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.6.2;
3+
4+
import {IERC165} from "./IERC165.sol";
5+
6+
/// @title IERC7802
7+
/// @notice Defines the interface for crosschain ERC20 transfers.
8+
interface IERC7802 is IERC165 {
9+
/// @notice Emitted when a crosschain transfer mints tokens.
10+
/// @param to Address of the account tokens are being minted for.
11+
/// @param amount Amount of tokens minted.
12+
/// @param sender Address of the caller (msg.sender) who invoked crosschainMint.
13+
event CrosschainMint(address indexed to, uint256 amount, address indexed sender);
14+
15+
/// @notice Emitted when a crosschain transfer burns tokens.
16+
/// @param from Address of the account tokens are being burned from.
17+
/// @param amount Amount of tokens burned.
18+
/// @param sender Address of the caller (msg.sender) who invoked crosschainBurn.
19+
event CrosschainBurn(address indexed from, uint256 amount, address indexed sender);
20+
21+
/// @notice Mint tokens through a crosschain transfer.
22+
/// @param _to Address to mint tokens to.
23+
/// @param _amount Amount of tokens to mint.
24+
function crosschainMint(address _to, uint256 _amount) external;
25+
26+
/// @notice Burn tokens through a crosschain transfer.
27+
/// @param _from Address to burn tokens from.
28+
/// @param _amount Amount of tokens to burn.
29+
function crosschainBurn(address _from, uint256 _amount) external;
30+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC20, ERC20Bridgeable} from "../../token/ERC20/extensions/draft-ERC20Bridgeable.sol";
6+
7+
abstract contract ERC20BridgeableMock is ERC20Bridgeable {
8+
address private _bridge;
9+
10+
error OnlyTokenBridge();
11+
event OnlyTokenBridgeFnCalled(address caller);
12+
13+
constructor(address bridge) {
14+
_bridge = bridge;
15+
}
16+
17+
function onlyTokenBridgeFn() external onlyTokenBridge {
18+
emit OnlyTokenBridgeFnCalled(msg.sender);
19+
}
20+
21+
function _checkTokenBridge(address sender) internal view override {
22+
if (sender != _bridge) {
23+
revert OnlyTokenBridge();
24+
}
25+
}
26+
}

contracts/token/ERC20/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ There are a few core contracts that implement the behavior specified in the ERC-
1616
Additionally there are multiple custom extensions, including:
1717

1818
* {ERC20Permit}: gasless approval of tokens (standardized as ERC-2612).
19+
* {ERC20Bridgeable}: compatibility with crosschain bridges through ERC-7802.
1920
* {ERC20Burnable}: destruction of own tokens.
2021
* {ERC20Capped}: enforcement of a cap to the total supply when minting tokens.
2122
* {ERC20Pausable}: ability to pause token transfers.
@@ -50,6 +51,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
5051

5152
{{ERC20Permit}}
5253

54+
{{ERC20Bridgeable}}
55+
5356
{{ERC20Burnable}}
5457

5558
{{ERC20Capped}}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC20} from "../ERC20.sol";
6+
import {ERC165, IERC165} from "../../../utils/introspection/ERC165.sol";
7+
import {IERC7802} from "../../../interfaces/draft-IERC7802.sol";
8+
9+
/**
10+
* @dev ERC20 extension that implements the standard token interface according to
11+
* https://eips.ethereum.org/EIPS/eip-7802[ERC-7802].
12+
*/
13+
abstract contract ERC20Bridgeable is ERC20, ERC165, IERC7802 {
14+
/// @dev Modifier to restrict access to the token bridge.
15+
modifier onlyTokenBridge() {
16+
// Token bridge should never be impersonated using a relayer/forwarder. Using msg.sender is preferable to
17+
// _msgSender() for security reasons.
18+
_checkTokenBridge(msg.sender);
19+
_;
20+
}
21+
22+
/// @inheritdoc ERC165
23+
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
24+
return interfaceId == type(IERC7802).interfaceId || super.supportsInterface(interfaceId);
25+
}
26+
27+
/**
28+
* @dev See {IERC7802-crosschainMint}. Emits a {IERC7802-CrosschainMint} event.
29+
*/
30+
function crosschainMint(address to, uint256 value) public virtual override onlyTokenBridge {
31+
_mint(to, value);
32+
emit CrosschainMint(to, value, _msgSender());
33+
}
34+
35+
/**
36+
* @dev See {IERC7802-crosschainBurn}. Emits a {IERC7802-CrosschainBurn} event.
37+
*/
38+
function crosschainBurn(address from, uint256 value) public virtual override onlyTokenBridge {
39+
_burn(from, value);
40+
emit CrosschainBurn(from, value, _msgSender());
41+
}
42+
43+
/**
44+
* @dev Checks if the caller is a trusted token bridge. MUST revert otherwise.
45+
*
46+
* Developers should implement this function using an access control mechanism that allows
47+
* customizing the list of allowed senders. Consider using {AccessControl} or {AccessManaged}.
48+
*/
49+
function _checkTokenBridge(address caller) internal virtual;
50+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
5+
const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js');
6+
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
7+
8+
const name = 'My Token';
9+
const symbol = 'MTKN';
10+
const initialSupply = 100n;
11+
12+
async function fixture() {
13+
const [other, bridge, ...accounts] = await ethers.getSigners();
14+
15+
const token = await ethers.deployContract('$ERC20BridgeableMock', [name, symbol, bridge]);
16+
await token.$_mint(accounts[0], initialSupply);
17+
18+
return { bridge, other, accounts, token };
19+
}
20+
21+
describe('ERC20Bridgeable', function () {
22+
beforeEach(async function () {
23+
Object.assign(this, await loadFixture(fixture));
24+
});
25+
26+
describe('onlyTokenBridgeFn', function () {
27+
it('reverts when called by non-bridge', async function () {
28+
await expect(this.token.onlyTokenBridgeFn()).to.be.revertedWithCustomError(this.token, 'OnlyTokenBridge');
29+
});
30+
31+
it('does not revert when called by bridge', async function () {
32+
await expect(this.token.connect(this.bridge).onlyTokenBridgeFn())
33+
.to.emit(this.token, 'OnlyTokenBridgeFnCalled')
34+
.withArgs(this.bridge);
35+
});
36+
});
37+
38+
describe('crosschainMint', function () {
39+
it('reverts when called by non-bridge', async function () {
40+
await expect(this.token.crosschainMint(this.other, 100n)).to.be.revertedWithCustomError(
41+
this.token,
42+
'OnlyTokenBridge',
43+
);
44+
});
45+
46+
it('mints amount provided by the bridge when calling crosschainMint', async function () {
47+
const amount = 100n;
48+
await expect(this.token.connect(this.bridge).crosschainMint(this.other, amount))
49+
.to.emit(this.token, 'CrosschainMint')
50+
.withArgs(this.other, amount, this.bridge)
51+
.to.emit(this.token, 'Transfer')
52+
.withArgs(ethers.ZeroAddress, this.other, amount);
53+
54+
await expect(this.token.balanceOf(this.other)).to.eventually.equal(amount);
55+
});
56+
});
57+
58+
describe('crosschainBurn', function () {
59+
it('reverts when called by non-bridge', async function () {
60+
await expect(this.token.crosschainBurn(this.other, 100n)).to.be.revertedWithCustomError(
61+
this.token,
62+
'OnlyTokenBridge',
63+
);
64+
});
65+
66+
it('burns amount provided by the bridge when calling crosschainBurn', async function () {
67+
const amount = 100n;
68+
await this.token.$_mint(this.other, amount);
69+
70+
await expect(this.token.connect(this.bridge).crosschainBurn(this.other, amount))
71+
.to.emit(this.token, 'CrosschainBurn')
72+
.withArgs(this.other, amount, this.bridge)
73+
.to.emit(this.token, 'Transfer')
74+
.withArgs(this.other, ethers.ZeroAddress, amount);
75+
76+
await expect(this.token.balanceOf(this.other)).to.eventually.equal(0);
77+
});
78+
});
79+
80+
describe('ERC165', function () {
81+
shouldSupportInterfaces({
82+
ERC7802: ['crosschainMint(address,uint256)', 'crosschainBurn(address,uint256)'],
83+
});
84+
});
85+
86+
describe('ERC20 behavior', function () {
87+
shouldBehaveLikeERC20(initialSupply);
88+
});
89+
});

0 commit comments

Comments
 (0)