Skip to content
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

Implement Lisk L2 token smart contract #7

Merged
merged 3 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion script/L2LiskToken.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
139 changes: 37 additions & 102 deletions src/L2/L2LiskToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
matjazv marked this conversation as resolved.
Show resolved Hide resolved
}

/// @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;
}
}
144 changes: 144 additions & 0 deletions test/L2/L2LiskToken.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// 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);

vm.prank(bridge);
l2LiskToken.mint(alice, 100 * 10 ** 18);
assertEq(l2LiskToken.balanceOf(alice), 100 * 10 ** 18);
AndreasKendziorra marked this conversation as resolved.
Show resolved Hide resolved

vm.prank(bridge);
l2LiskToken.mint(alice, 50 * 10 ** 18);
assertEq(l2LiskToken.balanceOf(alice), 150 * 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);

vm.prank(bridge);
l2LiskToken.mint(alice, 100 * 10 ** 18);
assertEq(l2LiskToken.balanceOf(alice), 100 * 10 ** 18);

vm.prank(bridge);
l2LiskToken.burn(alice, 50 * 10 ** 18);
assertEq(l2LiskToken.balanceOf(alice), 50 * 10 ** 18);

vm.prank(bridge);
l2LiskToken.burn(alice, 20 * 10 ** 18);
assertEq(l2LiskToken.balanceOf(alice), 30 * 10 ** 18);

vm.prank(bridge);
l2LiskToken.burn(alice, 30 * 10 ** 18);
assertEq(l2LiskToken.balanceOf(alice), 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);
}
}