Skip to content

Commit

Permalink
Implement Lisk L2 token smart contract (#7)
Browse files Browse the repository at this point in the history
* Remove old unneeded interface from L2LiskToken

* Implement unit tests for L2LiskToken

* Extend unit tests for mint and burn to check total supply
  • Loading branch information
matjazv authored Nov 23, 2023
1 parent b1bc34c commit c4f98e4
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 103 deletions.
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;
}

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

0 comments on commit c4f98e4

Please sign in to comment.