From c4f98e496102fe61008bfd8b2aaadc0db3735662 Mon Sep 17 00:00:00 2001 From: Matjaz Verbole Date: Thu, 23 Nov 2023 11:59:40 +0100 Subject: [PATCH] Implement Lisk L2 token smart contract (#7) * Remove old unneeded interface from L2LiskToken * Implement unit tests for L2LiskToken * Extend unit tests for mint and burn to check total supply --- script/L2LiskToken.s.sol | 3 +- src/L2/L2LiskToken.sol | 139 ++++++++---------------------- test/L2/L2LiskToken.t.sol | 176 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 103 deletions(-) create mode 100644 test/L2/L2LiskToken.t.sol diff --git a/script/L2LiskToken.s.sol b/script/L2LiskToken.s.sol index 82572c53..28449947 100644 --- a/script/L2LiskToken.s.sol +++ b/script/L2LiskToken.s.sol @@ -31,13 +31,14 @@ contract L2LiskTokenScript is Script { // deploy L2LiskToken contract vm.startBroadcast(deployerPrivateKey); - L2LiskToken l2LiskToken = new L2LiskToken(L2_STANDARD_BRIDGE, l1AddressesConfig.L1LiskToken, "Lisk", "LSK", 18); + L2LiskToken l2LiskToken = new L2LiskToken(L2_STANDARD_BRIDGE, l1AddressesConfig.L1LiskToken); vm.stopBroadcast(); assert(address(l2LiskToken) != address(0)); assert(keccak256(bytes(l2LiskToken.name())) == keccak256(bytes("Lisk"))); assert(keccak256(bytes(l2LiskToken.symbol())) == keccak256(bytes("LSK"))); assert(l2LiskToken.decimals() == 18); + assert(l2LiskToken.totalSupply() == 0); assert(l2LiskToken.REMOTE_TOKEN() == l1AddressesConfig.L1LiskToken); assert(l2LiskToken.BRIDGE() == L2_STANDARD_BRIDGE); diff --git a/src/L2/L2LiskToken.sol b/src/L2/L2LiskToken.sol index b4e18686..7dd148a4 100644 --- a/src/L2/L2LiskToken.sol +++ b/src/L2/L2LiskToken.sol @@ -10,43 +10,28 @@ import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol /// custom implementations of OptimismMintableERC20. interface IOptimismMintableERC20 is IERC165 { function remoteToken() external view returns (address); - function bridge() external returns (address); - - function mint(address _to, uint256 _amount) external; - - function burn(address _from, uint256 _amount) external; + function mint(address to, uint256 amount) external; + function burn(address from, uint256 amount) external; } -/// @custom:legacy -/// @title ILegacyMintableERC20 -/// @notice This interface was available on the legacy L2StandardERC20 contract. -/// It remains available on the OptimismMintableERC20 contract for -/// backwards compatibility. -interface ILegacyMintableERC20 is IERC165 { - function l1Token() external view returns (address); +/// @title L2LiskToken +/// @notice L2LiskToken is a standard extension of the base ERC20 and IOptimismMintableERC20 token contracts designed to +/// allow the StandardBridge contract to mint and burn tokens. This makes it possible to use an L2LiskToken as +/// the L2 representation of an L1LiskToken. +contract L2LiskToken is IOptimismMintableERC20, ERC20 { + /// @notice Name of the token. + string private constant NAME = "Lisk"; - function mint(address _to, uint256 _amount) external; + /// @notice Symbol of the token. + string private constant SYMBOL = "LSK"; - function burn(address _from, uint256 _amount) external; -} - -/// @title OptimismMintableERC20 -/// @notice OptimismMintableERC20 is a standard extension of the base ERC20 token contract designed -/// to allow the StandardBridge contracts to mint and burn tokens. This makes it possible to -/// use an OptimismMintablERC20 as the L2 representation of an L1 token, or vice-versa. -/// Designed to be backwards compatible with the older StandardL2ERC20 token which was only -/// meant for use on L2. -contract L2LiskToken is IOptimismMintableERC20, ILegacyMintableERC20, ERC20 { - /// @notice Address of the corresponding version of this token on the remote chain. + /// @notice Address of the corresponding version of this token on the remote chain (on L1). address public immutable REMOTE_TOKEN; - /// @notice Address of the StandardBridge on this network. + /// @notice Address of the StandardBridge on this (deployed) network. address public immutable BRIDGE; - /// @notice Decimals of the token - uint8 private immutable DECIMALS; - /// @notice Emitted whenever tokens are minted for an account. /// @param account Address of the account tokens are being minted for. /// @param amount Amount of tokens minted. @@ -59,103 +44,53 @@ contract L2LiskToken is IOptimismMintableERC20, ILegacyMintableERC20, ERC20 { /// @notice A modifier that only allows the bridge to call modifier onlyBridge() { - require(msg.sender == BRIDGE, "OptimismMintableERC20: only bridge can mint and burn"); + require(msg.sender == BRIDGE, "L2LiskToken: only bridge can mint or burn"); _; } - /// @param _bridge Address of the L2 standard bridge. - /// @param _remoteToken Address of the corresponding L1 token. - /// @param _name ERC20 name. - /// @param _symbol ERC20 symbol. - constructor( - address _bridge, - address _remoteToken, - string memory _name, - string memory _symbol, - uint8 _decimals - ) - ERC20(_name, _symbol) - { - REMOTE_TOKEN = _remoteToken; - BRIDGE = _bridge; - DECIMALS = _decimals; + /// @notice Constructs the L2LiskToken contract. + /// @param bridgeAddr Address of the L2 standard bridge. + /// @param remoteTokenAddr Address of the corresponding L1LiskToken. + constructor(address bridgeAddr, address remoteTokenAddr) ERC20(NAME, SYMBOL) { + REMOTE_TOKEN = remoteTokenAddr; + BRIDGE = bridgeAddr; } /// @notice Allows the StandardBridge on this network to mint tokens. - /// @param _to Address to mint tokens to. - /// @param _amount Amount of tokens to mint. - function mint( - address _to, - uint256 _amount - ) - external - virtual - override(IOptimismMintableERC20, ILegacyMintableERC20) - onlyBridge - { - _mint(_to, _amount); - emit Mint(_to, _amount); + /// @param to Address to mint tokens to. + /// @param amount Amount of tokens to mint. + function mint(address to, uint256 amount) external virtual override(IOptimismMintableERC20) onlyBridge { + _mint(to, amount); + emit Mint(to, amount); } /// @notice Allows the StandardBridge on this network to burn tokens. - /// @param _from Address to burn tokens from. - /// @param _amount Amount of tokens to burn. - function burn( - address _from, - uint256 _amount - ) - external - virtual - override(IOptimismMintableERC20, ILegacyMintableERC20) - onlyBridge - { - _burn(_from, _amount); - emit Burn(_from, _amount); + /// @param from Address to burn tokens from. + /// @param amount Amount of tokens to burn. + function burn(address from, uint256 amount) external virtual override(IOptimismMintableERC20) onlyBridge { + _burn(from, amount); + emit Burn(from, amount); } /// @notice ERC165 interface check function. - /// @param _interfaceId Interface ID to check. + /// @param interfaceId Interface ID to check. /// @return Whether or not the interface is supported by this contract. - function supportsInterface(bytes4 _interfaceId) external pure virtual returns (bool) { + function supportsInterface(bytes4 interfaceId) external pure virtual returns (bool) { bytes4 iface1 = type(IERC165).interfaceId; - // Interface corresponding to the legacy L2StandardERC20. - bytes4 iface2 = type(ILegacyMintableERC20).interfaceId; - // Interface corresponding to the updated OptimismMintableERC20 (this contract). - bytes4 iface3 = type(IOptimismMintableERC20).interfaceId; - return _interfaceId == iface1 || _interfaceId == iface2 || _interfaceId == iface3; - } - - /// @custom:legacy - /// @notice Legacy getter for the remote token. Use REMOTE_TOKEN going forward. - function l1Token() public view returns (address) { - return REMOTE_TOKEN; + // Interface corresponding to the L2LiskToken (this contract). + bytes4 iface2 = type(IOptimismMintableERC20).interfaceId; + return interfaceId == iface1 || interfaceId == iface2; } - /// @custom:legacy - /// @notice Legacy getter for the bridge. Use BRIDGE going forward. - function l2Bridge() public view returns (address) { - return BRIDGE; - } - - /// @custom:legacy /// @notice Legacy getter for REMOTE_TOKEN. + /// @return Address of the corresponding L1LiskToken on the remote chain. function remoteToken() public view returns (address) { return REMOTE_TOKEN; } - /// @custom:legacy /// @notice Legacy getter for BRIDGE. + /// @return Address of the L2 standard bridge. function bridge() public view returns (address) { return BRIDGE; } - - /// @dev Returns the number of decimals used to get its user representation. - /// For example, if `decimals` equals `2`, a balance of `505` tokens should - /// be displayed to a user as `5.05` (`505 / 10 ** 2`). - /// NOTE: This information is only used for _display_ purposes: it in - /// no way affects any of the arithmetic of the contract, including - /// {IERC20-balanceOf} and {IERC20-transfer}. - function decimals() public view override returns (uint8) { - return DECIMALS; - } } diff --git a/test/L2/L2LiskToken.t.sol b/test/L2/L2LiskToken.t.sol new file mode 100644 index 00000000..e4ba37d6 --- /dev/null +++ b/test/L2/L2LiskToken.t.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Test, console2 } from "forge-std/Test.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { L2LiskToken, IOptimismMintableERC20 } from "src/L2/L2LiskToken.sol"; + +contract L2LiskTokenTest is Test { + L2LiskToken public l2LiskToken; + address public remoteToken; + address public bridge; + + function setUp() public { + bridge = vm.addr(1); + remoteToken = vm.addr(2); + l2LiskToken = new L2LiskToken(bridge, remoteToken); + } + + function test_Initialize() public { + assertEq(l2LiskToken.name(), "Lisk"); + assertEq(l2LiskToken.symbol(), "LSK"); + assertEq(l2LiskToken.decimals(), 18); + assertEq(l2LiskToken.totalSupply(), 0); + assertEq(l2LiskToken.remoteToken(), remoteToken); + assertEq(l2LiskToken.bridge(), bridge); + + // check that an IERC165 interface is supported + assertEq(l2LiskToken.supportsInterface(type(IERC165).interfaceId), true); + + // check that an IOptimismMintableERC20 interface is supported + assertEq(l2LiskToken.supportsInterface(type(IOptimismMintableERC20).interfaceId), true); + } + + function test_GetBridge() public { + assertEq(l2LiskToken.bridge(), bridge); + assertEq(l2LiskToken.BRIDGE(), bridge); + } + + function test_GetRemoteToken() public { + assertEq(l2LiskToken.remoteToken(), remoteToken); + assertEq(l2LiskToken.REMOTE_TOKEN(), remoteToken); + } + + function test_Mint() public { + address alice = vm.addr(3); + address bob = vm.addr(4); + + vm.prank(bridge); + l2LiskToken.mint(alice, 100 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(alice), 100 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(bob), 0); + assertEq(l2LiskToken.totalSupply(), 100 * 10 ** 18); + + vm.prank(bridge); + l2LiskToken.mint(alice, 50 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(alice), 150 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(bob), 0); + assertEq(l2LiskToken.totalSupply(), 150 * 10 ** 18); + + vm.prank(bridge); + l2LiskToken.mint(bob, 30 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(alice), 150 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(bob), 30 * 10 ** 18); + assertEq(l2LiskToken.totalSupply(), 180 * 10 ** 18); + } + + function test_MintFail_NotBridge() public { + address alice = vm.addr(3); + address bob = vm.addr(4); + + // try to mint new tokens beeing alice and not the Standard Bridge + vm.prank(alice); + vm.expectRevert(); + l2LiskToken.mint(bob, 100 * 10 ** 18); + } + + function test_Burn() public { + address alice = vm.addr(3); + address bob = vm.addr(4); + + vm.prank(bridge); + l2LiskToken.mint(alice, 100 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(alice), 100 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(bob), 0); + assertEq(l2LiskToken.totalSupply(), 100 * 10 ** 18); + + vm.prank(bridge); + l2LiskToken.mint(bob, 50 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(alice), 100 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(bob), 50 * 10 ** 18); + assertEq(l2LiskToken.totalSupply(), 150 * 10 ** 18); + + vm.prank(bridge); + l2LiskToken.burn(alice, 50 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(alice), 50 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(bob), 50 * 10 ** 18); + assertEq(l2LiskToken.totalSupply(), 100 * 10 ** 18); + + vm.prank(bridge); + l2LiskToken.burn(alice, 20 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(alice), 30 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(bob), 50 * 10 ** 18); + assertEq(l2LiskToken.totalSupply(), 80 * 10 ** 18); + + vm.prank(bridge); + l2LiskToken.burn(alice, 30 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(alice), 0); + assertEq(l2LiskToken.balanceOf(bob), 50 * 10 ** 18); + assertEq(l2LiskToken.totalSupply(), 50 * 10 ** 18); + + vm.prank(bridge); + l2LiskToken.burn(bob, 50 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(alice), 0); + assertEq(l2LiskToken.balanceOf(bob), 0); + assertEq(l2LiskToken.totalSupply(), 0); + } + + function test_BurnFail_NotBridge() public { + address alice = vm.addr(3); + address bob = vm.addr(4); + + vm.prank(bridge); + l2LiskToken.mint(bob, 100 * 10 ** 18); + assertEq(l2LiskToken.balanceOf(bob), 100 * 10 ** 18); + + // try to burn tokens beeing alice and not the Standard Bridge + vm.prank(alice); + vm.expectRevert(); + l2LiskToken.burn(bob, 100 * 10 ** 18); + } + + function testFuzz_Transfer(uint256 amount) public { + address alice = vm.addr(3); + address bob = vm.addr(4); + + // mint some tokens to alice + vm.prank(bridge); + l2LiskToken.mint(alice, amount); + assertEq(l2LiskToken.balanceOf(alice), amount); + + // send some tokens from alice to bob + vm.prank(alice); + l2LiskToken.transfer(bob, amount); + assertEq(l2LiskToken.balanceOf(alice), 0); + assertEq(l2LiskToken.balanceOf(bob), amount); + + // send some tokens from bob to alice + vm.prank(bob); + l2LiskToken.transfer(alice, amount); + assertEq(l2LiskToken.balanceOf(alice), amount); + assertEq(l2LiskToken.balanceOf(bob), 0); + } + + function testFuzz_Allowance(uint256 amount) public { + address alice = vm.addr(3); + address bob = vm.addr(4); + + // mint some tokens to alice + vm.prank(bridge); + l2LiskToken.mint(alice, amount); + assertEq(l2LiskToken.balanceOf(alice), amount); + + // alice approves bob to spend some tokens + vm.prank(alice); + l2LiskToken.approve(bob, amount); + assertEq(l2LiskToken.allowance(alice, bob), amount); + + // test that bob can call transferFrom + vm.prank(bob); + l2LiskToken.transferFrom(alice, bob, amount); + // test alice balance + assertEq(l2LiskToken.balanceOf(alice), 0); + // test bob balance + assertEq(l2LiskToken.balanceOf(bob), amount); + } +}