ERC20Bridgable (ERC-7802) (#5735)
Co-authored-by: ernestognw <ernestognw@gmail.com> Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
This commit is contained in:
5
.changeset/ripe-bears-hide.md
Normal file
5
.changeset/ripe-bears-hide.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'openzeppelin-solidity': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
`ERC20Bridgeable`: Implementation of ERC-7802 that makes an ERC-20 compatible with crosschain bridges.
|
||||||
@ -45,6 +45,7 @@ are useful to interact with third party contracts that implement them.
|
|||||||
- {IERC6909Metadata}
|
- {IERC6909Metadata}
|
||||||
- {IERC6909TokenSupply}
|
- {IERC6909TokenSupply}
|
||||||
- {IERC7674}
|
- {IERC7674}
|
||||||
|
- {IERC7802}
|
||||||
|
|
||||||
== Detailed ABI
|
== Detailed ABI
|
||||||
|
|
||||||
@ -97,3 +98,5 @@ are useful to interact with third party contracts that implement them.
|
|||||||
{{IERC6909TokenSupply}}
|
{{IERC6909TokenSupply}}
|
||||||
|
|
||||||
{{IERC7674}}
|
{{IERC7674}}
|
||||||
|
|
||||||
|
{{IERC7802}}
|
||||||
|
|||||||
30
contracts/interfaces/draft-IERC7802.sol
Normal file
30
contracts/interfaces/draft-IERC7802.sol
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity >=0.6.2;
|
||||||
|
|
||||||
|
import {IERC165} from "./IERC165.sol";
|
||||||
|
|
||||||
|
/// @title IERC7802
|
||||||
|
/// @notice Defines the interface for crosschain ERC20 transfers.
|
||||||
|
interface IERC7802 is IERC165 {
|
||||||
|
/// @notice Emitted when a crosschain transfer mints tokens.
|
||||||
|
/// @param to Address of the account tokens are being minted for.
|
||||||
|
/// @param amount Amount of tokens minted.
|
||||||
|
/// @param sender Address of the caller (msg.sender) who invoked crosschainMint.
|
||||||
|
event CrosschainMint(address indexed to, uint256 amount, address indexed sender);
|
||||||
|
|
||||||
|
/// @notice Emitted when a crosschain transfer burns tokens.
|
||||||
|
/// @param from Address of the account tokens are being burned from.
|
||||||
|
/// @param amount Amount of tokens burned.
|
||||||
|
/// @param sender Address of the caller (msg.sender) who invoked crosschainBurn.
|
||||||
|
event CrosschainBurn(address indexed from, uint256 amount, address indexed sender);
|
||||||
|
|
||||||
|
/// @notice Mint tokens through a crosschain transfer.
|
||||||
|
/// @param _to Address to mint tokens to.
|
||||||
|
/// @param _amount Amount of tokens to mint.
|
||||||
|
function crosschainMint(address _to, uint256 _amount) external;
|
||||||
|
|
||||||
|
/// @notice Burn tokens through a crosschain transfer.
|
||||||
|
/// @param _from Address to burn tokens from.
|
||||||
|
/// @param _amount Amount of tokens to burn.
|
||||||
|
function crosschainBurn(address _from, uint256 _amount) external;
|
||||||
|
}
|
||||||
26
contracts/mocks/token/ERC20BridgeableMock.sol
Normal file
26
contracts/mocks/token/ERC20BridgeableMock.sol
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
|
import {ERC20, ERC20Bridgeable} from "../../token/ERC20/extensions/draft-ERC20Bridgeable.sol";
|
||||||
|
|
||||||
|
abstract contract ERC20BridgeableMock is ERC20Bridgeable {
|
||||||
|
address private _bridge;
|
||||||
|
|
||||||
|
error OnlyTokenBridge();
|
||||||
|
event OnlyTokenBridgeFnCalled(address caller);
|
||||||
|
|
||||||
|
constructor(address bridge) {
|
||||||
|
_bridge = bridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onlyTokenBridgeFn() external onlyTokenBridge {
|
||||||
|
emit OnlyTokenBridgeFnCalled(msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _checkTokenBridge(address sender) internal view override {
|
||||||
|
if (sender != _bridge) {
|
||||||
|
revert OnlyTokenBridge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ There are a few core contracts that implement the behavior specified in the ERC-
|
|||||||
Additionally there are multiple custom extensions, including:
|
Additionally there are multiple custom extensions, including:
|
||||||
|
|
||||||
* {ERC20Permit}: gasless approval of tokens (standardized as ERC-2612).
|
* {ERC20Permit}: gasless approval of tokens (standardized as ERC-2612).
|
||||||
|
* {ERC20Bridgeable}: compatibility with crosschain bridges through ERC-7802.
|
||||||
* {ERC20Burnable}: destruction of own tokens.
|
* {ERC20Burnable}: destruction of own tokens.
|
||||||
* {ERC20Capped}: enforcement of a cap to the total supply when minting tokens.
|
* {ERC20Capped}: enforcement of a cap to the total supply when minting tokens.
|
||||||
* {ERC20Pausable}: ability to pause token transfers.
|
* {ERC20Pausable}: ability to pause token transfers.
|
||||||
@ -50,6 +51,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
|
|||||||
|
|
||||||
{{ERC20Permit}}
|
{{ERC20Permit}}
|
||||||
|
|
||||||
|
{{ERC20Bridgeable}}
|
||||||
|
|
||||||
{{ERC20Burnable}}
|
{{ERC20Burnable}}
|
||||||
|
|
||||||
{{ERC20Capped}}
|
{{ERC20Capped}}
|
||||||
|
|||||||
50
contracts/token/ERC20/extensions/draft-ERC20Bridgeable.sol
Normal file
50
contracts/token/ERC20/extensions/draft-ERC20Bridgeable.sol
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
|
import {ERC20} from "../ERC20.sol";
|
||||||
|
import {ERC165, IERC165} from "../../../utils/introspection/ERC165.sol";
|
||||||
|
import {IERC7802} from "../../../interfaces/draft-IERC7802.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev ERC20 extension that implements the standard token interface according to
|
||||||
|
* https://eips.ethereum.org/EIPS/eip-7802[ERC-7802].
|
||||||
|
*/
|
||||||
|
abstract contract ERC20Bridgeable is ERC20, ERC165, IERC7802 {
|
||||||
|
/// @dev Modifier to restrict access to the token bridge.
|
||||||
|
modifier onlyTokenBridge() {
|
||||||
|
// Token bridge should never be impersonated using a relayer/forwarder. Using msg.sender is preferable to
|
||||||
|
// _msgSender() for security reasons.
|
||||||
|
_checkTokenBridge(msg.sender);
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ERC165
|
||||||
|
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
|
||||||
|
return interfaceId == type(IERC7802).interfaceId || super.supportsInterface(interfaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev See {IERC7802-crosschainMint}. Emits a {IERC7802-CrosschainMint} event.
|
||||||
|
*/
|
||||||
|
function crosschainMint(address to, uint256 value) public virtual override onlyTokenBridge {
|
||||||
|
_mint(to, value);
|
||||||
|
emit CrosschainMint(to, value, _msgSender());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev See {IERC7802-crosschainBurn}. Emits a {IERC7802-CrosschainBurn} event.
|
||||||
|
*/
|
||||||
|
function crosschainBurn(address from, uint256 value) public virtual override onlyTokenBridge {
|
||||||
|
_burn(from, value);
|
||||||
|
emit CrosschainBurn(from, value, _msgSender());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Checks if the caller is a trusted token bridge. MUST revert otherwise.
|
||||||
|
*
|
||||||
|
* Developers should implement this function using an access control mechanism that allows
|
||||||
|
* customizing the list of allowed senders. Consider using {AccessControl} or {AccessManaged}.
|
||||||
|
*/
|
||||||
|
function _checkTokenBridge(address caller) internal virtual;
|
||||||
|
}
|
||||||
89
test/token/ERC20/extensions/draft-ERC20Bridgeable.test.js
Normal file
89
test/token/ERC20/extensions/draft-ERC20Bridgeable.test.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
const { ethers } = require('hardhat');
|
||||||
|
const { expect } = require('chai');
|
||||||
|
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||||
|
|
||||||
|
const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js');
|
||||||
|
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
|
||||||
|
|
||||||
|
const name = 'My Token';
|
||||||
|
const symbol = 'MTKN';
|
||||||
|
const initialSupply = 100n;
|
||||||
|
|
||||||
|
async function fixture() {
|
||||||
|
const [other, bridge, ...accounts] = await ethers.getSigners();
|
||||||
|
|
||||||
|
const token = await ethers.deployContract('$ERC20BridgeableMock', [name, symbol, bridge]);
|
||||||
|
await token.$_mint(accounts[0], initialSupply);
|
||||||
|
|
||||||
|
return { bridge, other, accounts, token };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ERC20Bridgeable', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
Object.assign(this, await loadFixture(fixture));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onlyTokenBridgeFn', function () {
|
||||||
|
it('reverts when called by non-bridge', async function () {
|
||||||
|
await expect(this.token.onlyTokenBridgeFn()).to.be.revertedWithCustomError(this.token, 'OnlyTokenBridge');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not revert when called by bridge', async function () {
|
||||||
|
await expect(this.token.connect(this.bridge).onlyTokenBridgeFn())
|
||||||
|
.to.emit(this.token, 'OnlyTokenBridgeFnCalled')
|
||||||
|
.withArgs(this.bridge);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('crosschainMint', function () {
|
||||||
|
it('reverts when called by non-bridge', async function () {
|
||||||
|
await expect(this.token.crosschainMint(this.other, 100n)).to.be.revertedWithCustomError(
|
||||||
|
this.token,
|
||||||
|
'OnlyTokenBridge',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mints amount provided by the bridge when calling crosschainMint', async function () {
|
||||||
|
const amount = 100n;
|
||||||
|
await expect(this.token.connect(this.bridge).crosschainMint(this.other, amount))
|
||||||
|
.to.emit(this.token, 'CrosschainMint')
|
||||||
|
.withArgs(this.other, amount, this.bridge)
|
||||||
|
.to.emit(this.token, 'Transfer')
|
||||||
|
.withArgs(ethers.ZeroAddress, this.other, amount);
|
||||||
|
|
||||||
|
await expect(this.token.balanceOf(this.other)).to.eventually.equal(amount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('crosschainBurn', function () {
|
||||||
|
it('reverts when called by non-bridge', async function () {
|
||||||
|
await expect(this.token.crosschainBurn(this.other, 100n)).to.be.revertedWithCustomError(
|
||||||
|
this.token,
|
||||||
|
'OnlyTokenBridge',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('burns amount provided by the bridge when calling crosschainBurn', async function () {
|
||||||
|
const amount = 100n;
|
||||||
|
await this.token.$_mint(this.other, amount);
|
||||||
|
|
||||||
|
await expect(this.token.connect(this.bridge).crosschainBurn(this.other, amount))
|
||||||
|
.to.emit(this.token, 'CrosschainBurn')
|
||||||
|
.withArgs(this.other, amount, this.bridge)
|
||||||
|
.to.emit(this.token, 'Transfer')
|
||||||
|
.withArgs(this.other, ethers.ZeroAddress, amount);
|
||||||
|
|
||||||
|
await expect(this.token.balanceOf(this.other)).to.eventually.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ERC165', function () {
|
||||||
|
shouldSupportInterfaces({
|
||||||
|
ERC7802: ['crosschainMint(address,uint256)', 'crosschainBurn(address,uint256)'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ERC20 behavior', function () {
|
||||||
|
shouldBehaveLikeERC20(initialSupply);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user