Skip to content

Commit

Permalink
Implement Lisk L1 token smart contract (#13)
Browse files Browse the repository at this point in the history
* L1LiskToken is ERC20 contract

* Unauthorized error

* Ownable contract

* L1LiskToken is Ownable

* forge install: openzeppelin-solidity

solc-nightly

* BurnerRole contract

* L1LiskToken maintains burners through BurnerRole contract

* L1LiskToken allows burners to burn their tokens

* L1LiskToken is ERC20Permit contract

* L1LiskToken is Ownable from openzeppelin-contracts

* 🔥 Removes Ownable contract

* ♻️ ✅ BurnerRole declares and throws UnauthorizedBurnerAccount error

* 🔥 Removes Errors.sol

* L1LiskToken maintains burners

* 🔥 Removes BurnerRole contract

* L1LiskToken is ERC20Burnable

* ✅ L1LiskToken.permit

* ✅ L1LiskToken is AccessControl

* ♻️ ✅

* L1LiskToken maintains owner role

* Replaces OWNER_ROLE with DEFAULT_ADMIN_ROLE and removes owner()

* Increases total supply to 300 million

* Adds transferOwnership

* Verifies if deployer is owner and not a burner and total supply is 300 million

* ♻️ L1LiskToken declarations

* ♻️ Replaces _msgSender() with msg.sender

* ♻️ Sets DEFAULT_ADMIN_ROLE as the roleAdmin for BURNER_ROLE

* ♻️ Removes getBurnerRole() for BURNER_ROLE() ✅ Removes defaultAdminRole for DEFAULT_ADMIN_ROLE()

* ✅ Adds assertions for test_Initialize

* ✅ Owner is not a burner

* ♻️ ✅ Rearranges imports

* ✅ Adds assertions

* ✅ Only burner can burn from an account

* Transfers ownership of L1LiskToken contract to configured address after deployment

* ♻️ ✅ 🔥 Removes duplicate SigUtils

* Updates script/L1LiskToken.s.sol

Co-authored-by: Matjaz Verbole <[email protected]>

* ♻️ L1LiskToken script

* ♻️ ✅ Renames test methods for L1LiskToken

* 📝 L1LiskToken

* 📝 Updates renounceBurner NatSpec.

Co-authored-by: Matjaz Verbole <[email protected]>

* 📝 L1LiskTokenScript

* 📝 L1LiskTokenScript

Co-authored-by: Matjaz Verbole <[email protected]>

* 📝 L1LiskTokenScript

* ♻️ Removes call to _setRoleAdmin in ERC20 constructor

---------

Co-authored-by: Matjaz Verbole <[email protected]>
  • Loading branch information
has5aan and matjazv authored Jan 10, 2024
1 parent 84764c8 commit cb55515
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 127 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# Salt for deterministic Lisk L2 token address generation
L2_TOKEN_SALT="test_l2_token_salt"

# Owner address for L1LiskToken contract
L1_TOKEN_OWNER_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3

# L1 RPC URL, e.g. Infura, Alchemy, or your own node
L1_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_KEY

Expand Down
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,3 @@
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
4 changes: 1 addition & 3 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ solc_version = "0.8.21"
optimizer = true
optimizer_runs = 999999
remappings = [
'@openzeppelin/=lib/openzeppelin-contracts/',
'@openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/',
'ds-test/=lib/forge-std/lib/ds-test/src/',
'erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/',
'forge-std/=lib/forge-std/src/',
'@openzeppelin/=lib/openzeppelin-contracts/',
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/',
'openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/',
'openzeppelin-contracts/=lib/openzeppelin-contracts/',
]

Expand Down
1 change: 0 additions & 1 deletion lib/openzeppelin-contracts-upgradeable
Submodule openzeppelin-contracts-upgradeable deleted from 625fb3
51 changes: 22 additions & 29 deletions script/L1LiskToken.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
pragma solidity 0.8.21;

import { Script, console2 } from "forge-std/Script.sol";
import { L1LiskToken, UUPSProxy } from "src/L1/L1LiskToken.sol";
import { L1LiskToken } from "src/L1/L1LiskToken.sol";
import "script/Utils.sol";

/// @title L1LiskTokenScript - L1 Lisk token deployment script
/// @notice This contract is used to deploy L1 Lisk token contract and write its address to JSON file.
/// @notice This contract is used to deploy L1 Lisk token contract, transfers its ownership and writes its address to
/// JSON file.
contract L1LiskTokenScript is Script {
/// @notice Utils contract which provides functions to read and write JSON files containing L1 and L2 addresses.
Utils utils;
Expand All @@ -15,49 +16,41 @@ contract L1LiskTokenScript is Script {
utils = new Utils();
}

/// @notice This function deploys L1 Lisk token contract and writes its address to JSON file.
/// @notice This function deploys L1 Lisk token contract, transfers its ownership and writes its address to JSON
/// file.
function run() public {
// Deployer's private key. Owner of the L1 Lisk token. PRIVATE_KEY is set in .env file.
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

// Address, the ownership of L1 Lisk token contract is transferred to after deployment.
address ownerAddress = vm.envAddress("L1_TOKEN_OWNER_ADDRESS");

console2.log("Simulation: Deploying L1 Lisk token...");

// deploy L1LiskToken contract
// deploy L1LiskToken contract and transfer its ownership
vm.startBroadcast(deployerPrivateKey);
L1LiskToken l1LiskToken = new L1LiskToken();
l1LiskToken.transferOwnership(ownerAddress);
vm.stopBroadcast();

assert(address(l1LiskToken) != address(0));
assert(l1LiskToken.proxiableUUID() == 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc);

// deploy proxy contract and point it to the L1LiskToken contract
vm.startBroadcast(deployerPrivateKey);
UUPSProxy proxy = new UUPSProxy(address(l1LiskToken), "");
vm.stopBroadcast();

// wrap in ABI to support easier calls
vm.startBroadcast(deployerPrivateKey);
L1LiskToken wrappedProxy = L1LiskToken(address(proxy));
vm.stopBroadcast();

// initialize the proxy contract (calls the initialize function in L1LiskToken)
vm.startBroadcast(deployerPrivateKey);
wrappedProxy.initialize();
vm.stopBroadcast();

assert(keccak256(bytes(wrappedProxy.name())) == keccak256(bytes("Lisk")));
assert(keccak256(bytes(wrappedProxy.symbol())) == keccak256(bytes("LSK")));
assert(wrappedProxy.decimals() == 18);
assert(wrappedProxy.totalSupply() == 200000000 * 10 ** 18);
assert(wrappedProxy.balanceOf(vm.addr(deployerPrivateKey)) == 200000000 * 10 ** 18);
assert(wrappedProxy.owner() == vm.addr(deployerPrivateKey));
assert(keccak256(bytes(l1LiskToken.name())) == keccak256(bytes("Lisk")));
assert(keccak256(bytes(l1LiskToken.symbol())) == keccak256(bytes("LSK")));
assert(l1LiskToken.decimals() == 18);
assert(l1LiskToken.totalSupply() == 300000000 * 10 ** 18);
assert(l1LiskToken.balanceOf(vm.addr(deployerPrivateKey)) == 300000000 * 10 ** 18);
assert(l1LiskToken.hasRole(l1LiskToken.DEFAULT_ADMIN_ROLE(), vm.addr(deployerPrivateKey)) == false);
assert(l1LiskToken.hasRole(l1LiskToken.BURNER_ROLE(), vm.addr(deployerPrivateKey)) == false);
assert(l1LiskToken.hasRole(l1LiskToken.DEFAULT_ADMIN_ROLE(), ownerAddress) == true);
assert(l1LiskToken.hasRole(l1LiskToken.BURNER_ROLE(), ownerAddress) == false);
assert(l1LiskToken.balanceOf(ownerAddress) == 0);

console2.log("Simulation: L1 Lisk token successfully deployed!");
console2.log("Simulation: L1 Lisk token address: %s", address(wrappedProxy));
console2.log("Simulation: L1 Lisk token address: %s", address(l1LiskToken));

// write L1LiskToken address to l1addresses.json
Utils.L1AddressesConfig memory l1AddressesConfig;
l1AddressesConfig.L1LiskToken = address(wrappedProxy);
l1AddressesConfig.L1LiskToken = address(l1LiskToken);
utils.writeL1AddressesFile(l1AddressesConfig);
}
}
77 changes: 59 additions & 18 deletions src/L1/L1LiskToken.sol
Original file line number Diff line number Diff line change
@@ -1,30 +1,71 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;

import { ERC20Upgradeable } from "@openzeppelin-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol";
import { Initializable } from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol";
import { OwnableUpgradeable } from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract UUPSProxy is ERC1967Proxy {
constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) { }
}
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";

contract L1LiskToken is Initializable, ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
/// @title L1LiskToken
/// @notice L1LiskToken is an implementation of ERC20 token and is an extension of AccessControl, ERC20Permit and
/// ERC20Burnable token contracts.
/// It maintains the ownership of the deployed contract and only allows the owners to transfer the ownership.
/// L1LiskToken's only allows burners to burn the total supply and only the owner manages burner accounts.
contract L1LiskToken is ERC20Burnable, AccessControl, ERC20Permit {
/// @notice Name of the token.
string private constant NAME = "Lisk";

/// @notice Symbol of the token.
string private constant SYMBOL = "LSK";
uint256 private constant TOTAL_SUPPLY = 200_000_000 * 10 ** 18; //200 million LSK tokens

constructor() {
_disableInitializers();
}
/// @notice Total supply of the token.
uint256 private constant TOTAL_SUPPLY = 300_000_000 * 10 ** 18; //300 million LSK tokens

/// @notice Burner role.
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

function initialize() public initializer {
__ERC20_init(NAME, SYMBOL);
/// @notice Constructs the L1LiskToken contract.
constructor() ERC20(NAME, SYMBOL) ERC20Permit(NAME) {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_mint(msg.sender, TOTAL_SUPPLY);
__Ownable_init(msg.sender);
}

function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }
/// @notice Allows the owner to transfer the ownership of the contract.
/// @param account The new owner of the contract.
function transferOwnership(address account) public onlyRole(DEFAULT_ADMIN_ROLE) {
_grantRole(DEFAULT_ADMIN_ROLE, account);
_revokeRole(DEFAULT_ADMIN_ROLE, msg.sender);
}

/// @notice Verifies if an account is a burner.
/// @param account Account to be verified.
/// @return Whether or not the provided account is a burner.
function isBurner(address account) public view returns (bool) {
return hasRole(BURNER_ROLE, account);
}

/// @notice Allows the owner to grant burner role to an account.
/// @param account Account to be added as a burner.
function addBurner(address account) public onlyRole(DEFAULT_ADMIN_ROLE) {
_grantRole(BURNER_ROLE, account);
}

/// @notice Allows the owner to revoke burner role from an account.
/// @param account Account to be removed as a burner.
function renounceBurner(address account) public onlyRole(DEFAULT_ADMIN_ROLE) {
_revokeRole(BURNER_ROLE, account);
}

/// @notice Allows a burner to burn token.
/// @param value Amount to be burned.
function burn(uint256 value) public override onlyRole(BURNER_ROLE) {
super.burn(value);
}

/// @notice Allows a burner to burn its allowance from an account.
/// @param account Account to burn tokens from.
/// @param value Amount to burned.
function burnFrom(address account, uint256 value) public override onlyRole(BURNER_ROLE) {
super.burnFrom(account, value);
}
}
Loading

0 comments on commit cb55515

Please sign in to comment.