From cb55515628ff2815e0c641d4ee6aa711fb910ceb Mon Sep 17 00:00:00 2001 From: has5aan <50018215+has5aan@users.noreply.github.com> Date: Wed, 10 Jan 2024 09:37:11 +0100 Subject: [PATCH] Implement Lisk L1 token smart contract (#13) * 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 * :fire: Removes Ownable contract * :recycle: :white_check_mark: BurnerRole declares and throws UnauthorizedBurnerAccount error * :fire: Removes Errors.sol * L1LiskToken maintains burners * :fire: Removes BurnerRole contract * L1LiskToken is ERC20Burnable * :white_check_mark: L1LiskToken.permit * :white_check_mark: L1LiskToken is AccessControl * :recycle: :white_check_mark: * 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 * :recycle: L1LiskToken declarations * :recycle: Replaces _msgSender() with msg.sender * :recycle: Sets DEFAULT_ADMIN_ROLE as the roleAdmin for BURNER_ROLE * :recycle: Removes getBurnerRole() for BURNER_ROLE() :white_check_mark: Removes defaultAdminRole for DEFAULT_ADMIN_ROLE() * :white_check_mark: Adds assertions for test_Initialize * :white_check_mark: Owner is not a burner * :recycle: :white_check_mark: Rearranges imports * :white_check_mark: Adds assertions * :white_check_mark: Only burner can burn from an account * Transfers ownership of L1LiskToken contract to configured address after deployment * :recycle: :white_check_mark: :fire: Removes duplicate SigUtils * Updates script/L1LiskToken.s.sol Co-authored-by: Matjaz Verbole * :recycle: L1LiskToken script * :recycle: :white_check_mark: Renames test methods for L1LiskToken * :memo: L1LiskToken * :memo: Updates renounceBurner NatSpec. Co-authored-by: Matjaz Verbole * :memo: L1LiskTokenScript * :memo: L1LiskTokenScript Co-authored-by: Matjaz Verbole * :memo: L1LiskTokenScript * :recycle: Removes call to _setRoleAdmin in ERC20 constructor --------- Co-authored-by: Matjaz Verbole --- .env.example | 3 + .gitmodules | 3 - foundry.toml | 4 +- lib/openzeppelin-contracts-upgradeable | 1 - script/L1LiskToken.s.sol | 51 +++--- src/L1/L1LiskToken.sol | 77 +++++++-- test/L1/L1LiskToken.t.sol | 229 +++++++++++++++++-------- 7 files changed, 241 insertions(+), 127 deletions(-) delete mode 160000 lib/openzeppelin-contracts-upgradeable diff --git a/.env.example b/.env.example index b21bbd79..90e5ba7f 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/.gitmodules b/.gitmodules index 9296efd5..690924b6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/foundry.toml b/foundry.toml index dc8494d7..51730993 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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/', ] diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable deleted file mode 160000 index 625fb3c2..00000000 --- a/lib/openzeppelin-contracts-upgradeable +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 625fb3c2b2696f1747ba2e72d1e1113066e6c177 diff --git a/script/L1LiskToken.s.sol b/script/L1LiskToken.s.sol index 33457171..71e68b33 100644 --- a/script/L1LiskToken.s.sol +++ b/script/L1LiskToken.s.sol @@ -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; @@ -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); } } diff --git a/src/L1/L1LiskToken.sol b/src/L1/L1LiskToken.sol index ff99a742..ec380c81 100644 --- a/src/L1/L1LiskToken.sol +++ b/src/L1/L1LiskToken.sol @@ -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); + } } diff --git a/test/L1/L1LiskToken.t.sol b/test/L1/L1LiskToken.t.sol index 4eee242f..65f23ebf 100644 --- a/test/L1/L1LiskToken.t.sol +++ b/test/L1/L1LiskToken.t.sol @@ -2,103 +2,186 @@ pragma solidity 0.8.21; import { Test, console2 } from "forge-std/Test.sol"; -import { L1LiskToken, UUPSProxy } from "src/L1/L1LiskToken.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import { L1LiskToken } from "src/L1/L1LiskToken.sol"; +import { SigUtils } from "test/SigUtils.sol"; contract L1LiskTokenTest is Test { - L1LiskToken public l1LiskToken; - UUPSProxy public proxy; - L1LiskToken public wrappedProxy; + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + event Transfer(address indexed from, address indexed to, uint256 value); + + string private constant NAME = "Lisk"; + string private constant SYMBOL = "LSK"; + uint256 private constant TOTAL_SUPPLY = 300_000_000 * 10 ** 18; //300 million LSK tokens + + L1LiskToken l1LiskToken; function setUp() public { l1LiskToken = new L1LiskToken(); + } + + function test_Initialize() public { + assertEq(l1LiskToken.name(), NAME); + assertEq(l1LiskToken.symbol(), SYMBOL); + assertEq(l1LiskToken.totalSupply(), TOTAL_SUPPLY); + assertEq(l1LiskToken.balanceOf(address(this)), TOTAL_SUPPLY); + assertEq(l1LiskToken.decimals(), 18); + assertTrue(l1LiskToken.hasRole(l1LiskToken.DEFAULT_ADMIN_ROLE(), address(this))); + assertFalse(l1LiskToken.hasRole(l1LiskToken.BURNER_ROLE(), address(this))); + } + + function test_OnlyOwnerAddsOrRenouncesBurner() public { + address alice = address(0x1); + + vm.startPrank(alice); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, alice, l1LiskToken.DEFAULT_ADMIN_ROLE() + ) + ); + l1LiskToken.addBurner(alice); - // deploy proxy contract and point it to the L1LiskToken contract - proxy = new UUPSProxy(address(l1LiskToken), ""); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, alice, l1LiskToken.DEFAULT_ADMIN_ROLE() + ) + ); + l1LiskToken.renounceBurner(alice); - // wrap in ABI to support easier calls - wrappedProxy = L1LiskToken(address(proxy)); + vm.stopPrank(); - // initialize the proxy contract (calls the initialize function in L1LiskToken) - wrappedProxy.initialize(); + vm.expectEmit(true, true, true, true, address(l1LiskToken)); + emit RoleGranted(l1LiskToken.BURNER_ROLE(), alice, address(this)); + l1LiskToken.addBurner(alice); + assertTrue(l1LiskToken.isBurner(alice)); + + vm.expectEmit(true, true, true, true, address(l1LiskToken)); + emit RoleRevoked(l1LiskToken.BURNER_ROLE(), alice, address(this)); + l1LiskToken.renounceBurner(alice); + assertFalse(l1LiskToken.isBurner(alice)); } - function test_Initialize() public { - assertEq(wrappedProxy.name(), "Lisk"); - assertEq(wrappedProxy.symbol(), "LSK"); - assertEq(wrappedProxy.decimals(), 18); - assertEq(wrappedProxy.totalSupply(), 200000000 * 10 ** 18); - assertEq(wrappedProxy.balanceOf(address(this)), 200000000 * 10 ** 18); - assertEq(wrappedProxy.owner(), address(this)); - assertEq(l1LiskToken.proxiableUUID(), 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc); + function test_OwnerIsNotABurner() public { + uint256 amountToBurn = 1000000; + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), l1LiskToken.BURNER_ROLE() + ) + ); + l1LiskToken.burn(amountToBurn); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), l1LiskToken.BURNER_ROLE() + ) + ); + l1LiskToken.burnFrom(address(0x1), amountToBurn); } - function test_Transfer() public { - address alice = vm.addr(1); - address bob = vm.addr(2); + function test_OnlyBurnerWithSufficientBalanceBurnsToken() public { + address alice = address(0x1); + uint256 amountToBurn = 1000000; + vm.startPrank(alice); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, alice, l1LiskToken.BURNER_ROLE() + ) + ); + l1LiskToken.burn(amountToBurn); + vm.stopPrank(); + + l1LiskToken.addBurner(alice); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 0, amountToBurn)); + l1LiskToken.burn(amountToBurn); - // send 1000 tokens to alice - wrappedProxy.transfer(alice, 1000); - assertEq(wrappedProxy.balanceOf(alice), 1000); + l1LiskToken.transfer(alice, amountToBurn * 2); + assertEq(l1LiskToken.balanceOf(alice), amountToBurn * 2); - // send 1000 tokens from alice to bob vm.prank(alice); - wrappedProxy.transfer(bob, 1000); - assertEq(wrappedProxy.balanceOf(alice), 0); - assertEq(wrappedProxy.balanceOf(bob), 1000); - - // send 1000 tokens from bob to alice - vm.prank(bob); - wrappedProxy.transfer(alice, 1000); - assertEq(wrappedProxy.balanceOf(alice), 1000); - assertEq(wrappedProxy.balanceOf(bob), 0); + vm.expectEmit(true, true, false, true, address(l1LiskToken)); + emit Transfer(alice, address(0), amountToBurn); + l1LiskToken.burn(amountToBurn); + + assertEq(l1LiskToken.balanceOf(alice), amountToBurn); + assertEq(l1LiskToken.totalSupply(), TOTAL_SUPPLY - amountToBurn); } - function test_Allowance() public { - address alice = vm.addr(1); - address bob = vm.addr(2); + function test_OnlyBurnerWithSufficientAllowanceBurnsTokensFromAnAccount() public { + address alice = address(0x1); + uint256 amountToBurn = 1000000; + vm.startPrank(alice); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, alice, l1LiskToken.BURNER_ROLE() + ) + ); + l1LiskToken.burnFrom(address(this), amountToBurn); + vm.stopPrank(); + + l1LiskToken.addBurner(alice); + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, alice, 0, amountToBurn) + ); + l1LiskToken.burnFrom(address(this), amountToBurn); - // send 1000 tokens to alice - wrappedProxy.transfer(alice, 1000); - assertEq(wrappedProxy.balanceOf(alice), 1000); + l1LiskToken.approve(alice, amountToBurn); + assertEq(l1LiskToken.allowance(address(this), alice), amountToBurn); - // alice approves bob to spend 1000 tokens vm.prank(alice); - wrappedProxy.approve(bob, 1000); - assertEq(wrappedProxy.allowance(alice, bob), 1000); - - // test that bob can call transferFrom - vm.prank(bob); - wrappedProxy.transferFrom(alice, bob, 1000); - // test alice balance - assertEq(wrappedProxy.balanceOf(alice), 0); - // test bob balance - assertEq(wrappedProxy.balanceOf(bob), 1000); + l1LiskToken.burnFrom(address(this), amountToBurn); + + assertEq(l1LiskToken.allowance(address(this), alice), 0); + assertEq(l1LiskToken.totalSupply(), TOTAL_SUPPLY - amountToBurn); } - function test_Upgrade() public { - // deploy new version of L1LiskToken and upgrade the proxy to point to it - L1LiskToken l1LiskToken_v2 = new L1LiskToken(); - wrappedProxy.upgradeToAndCall(address(l1LiskToken_v2), ""); - - // re-wrap the proxy - L1LiskToken wrappedProxy_v2 = L1LiskToken(address(proxy)); - - assertEq(wrappedProxy_v2.name(), "Lisk"); - assertEq(wrappedProxy_v2.symbol(), "LSK"); - assertEq(wrappedProxy_v2.decimals(), 18); - assertEq(wrappedProxy_v2.totalSupply(), 200000000 * 10 ** 18); - assertEq(wrappedProxy_v2.balanceOf(address(this)), 200000000 * 10 ** 18); - assertEq(wrappedProxy_v2.owner(), address(this)); - assertEq(l1LiskToken_v2.proxiableUUID(), 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc); + function test_Permit() public { + uint256 ownerPrivateKey = 0xB0B; + uint256 spenderPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + address spender = vm.addr(spenderPrivateKey); + SigUtils sigUtils = new SigUtils(l1LiskToken.DOMAIN_SEPARATOR()); + SigUtils.Permit memory permit = + SigUtils.Permit({ owner: owner, spender: spender, value: 1000000, nonce: 0, deadline: 1 days }); + bytes32 digest = sigUtils.getTypedDataHash(permit); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + l1LiskToken.transfer(permit.owner, 2000000); + l1LiskToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + + assertEq(l1LiskToken.allowance(owner, spender), permit.value); } - function test_UpgradeFail_NotOwner() public { - L1LiskToken l1LiskToken_v2 = new L1LiskToken(); + function test_OnlyOwnerTransfersTheOwnership() public { + address alice = address(0x1); + address bob = address(0x2); + vm.startPrank(alice); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, alice, l1LiskToken.DEFAULT_ADMIN_ROLE() + ) + ); + l1LiskToken.transferOwnership(bob); + vm.stopPrank(); + + vm.expectEmit(true, true, true, true, address(l1LiskToken)); + emit RoleGranted(l1LiskToken.DEFAULT_ADMIN_ROLE(), alice, address(this)); + vm.expectEmit(true, true, true, true, address(l1LiskToken)); + emit RoleRevoked(l1LiskToken.DEFAULT_ADMIN_ROLE(), address(this), address(this)); + l1LiskToken.transferOwnership(alice); + + assertFalse(l1LiskToken.hasRole(l1LiskToken.DEFAULT_ADMIN_ROLE(), address(this))); + assertTrue(l1LiskToken.hasRole(l1LiskToken.DEFAULT_ADMIN_ROLE(), alice)); + } - // try to upgrade the proxy while not being the owner - address alice = vm.addr(1); - vm.prank(alice); - vm.expectRevert(); - wrappedProxy.upgradeToAndCall(address(l1LiskToken_v2), ""); + function test_DefaultAdminRoleIsRoleAdminForBurnerRole() public { + assertEq(l1LiskToken.DEFAULT_ADMIN_ROLE(), l1LiskToken.getRoleAdmin(l1LiskToken.BURNER_ROLE())); } }