diff --git a/foundry.toml b/foundry.toml index b7545998..ae4ad3e3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -33,3 +33,8 @@ line_length = 120 multiline_func_header = 'all' bracket_spacing = true wrap_comments = true + +[invariant] +runs = 50 +depth = 50 +fail_on_revert = false \ No newline at end of file diff --git a/script/example/L2ClaimTokens.s.sol b/script/example/L2ClaimTokens.s.sol index 7d7d9b5a..a31ff3e3 100644 --- a/script/example/L2ClaimTokens.s.sol +++ b/script/example/L2ClaimTokens.s.sol @@ -6,7 +6,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Script, console2 } from "forge-std/Script.sol"; import { stdJson } from "forge-std/StdJson.sol"; import { L2Claim, ED25519Signature, MultisigKeys } from "src/L2/L2Claim.sol"; -import { Signature, MerkleTreeLeaf, MerkleLeaves } from "test/L2/L2Claim.t.sol"; +import { Signature, MerkleTreeLeaf, MerkleLeaves } from "test/L2/helper/L2ClaimHelper.sol"; import "script/contracts/Utils.sol"; /// @title L2ClaimTokensScript - L2 Claim Lisk tokens script diff --git a/test/L2/L2Claim.t.sol b/test/L2/L2Claim.t.sol index 7a0dc0a5..ce2ff3f3 100644 --- a/test/L2/L2Claim.t.sol +++ b/test/L2/L2Claim.t.sol @@ -6,39 +6,10 @@ import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093. import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { OwnableUpgradeable } from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import { Test, console, stdJson } from "forge-std/Test.sol"; -import { L2Claim, ED25519Signature, MultisigKeys } from "src/L2/L2Claim.sol"; +import { Test, stdJson } from "forge-std/Test.sol"; import { Utils } from "script/contracts/Utils.sol"; -import { MockERC20 } from "../mock/MockERC20.sol"; - -struct SigPair { - bytes32 pubKey; - bytes32 r; - bytes32 s; -} - -struct Signature { - bytes message; - SigPair[] sigs; -} - -/// @notice This struct stores merkleTree leaf. -/// @dev Limitation of parseJSON, only bytes32 is supported. -/// To convert b32Address back to bytes20, shift 96 bits to the left. -/// i.e. bytes20(leaf.b32Address << 96) -struct MerkleTreeLeaf { - bytes32 b32Address; - uint64 balanceBeddows; - bytes32[] mandatoryKeys; - uint256 numberOfSignatures; - bytes32[] optionalKeys; - bytes32[] proof; -} - -/// @notice This struct is used to read MerkleLeaves from JSON file. -struct MerkleLeaves { - MerkleTreeLeaf[] leaves; -} +import { L2Claim, ED25519Signature, MultisigKeys } from "src/L2/L2Claim.sol"; +import { L2ClaimHelper, Signature, MerkleTreeLeaf } from "test/L2/helper/L2ClaimHelper.sol"; contract L2ClaimV2Mock is L2Claim { function initializeV2(uint256 _recoverPeriodTimestamp) public reinitializer(2) { @@ -51,48 +22,9 @@ contract L2ClaimV2Mock is L2Claim { } } -contract L2ClaimTest is Test { +contract L2ClaimTest is L2ClaimHelper { using stdJson for string; - /// @notice recover LSK tokens after 2 years - uint256 public constant RECOVER_PERIOD = 730 days; - - /// @notice The destination address for claims as `address(uint160(uint256(keccak256("foundry default caller"))))` - /// and `nonce=2`. - address public constant RECIPIENT_ADDRESS = address(0x34A1D3fff3958843C43aD80F30b94c510645C316); - - ERC20 public lsk; - L2Claim public l2ClaimImplementation; - L2Claim public l2Claim; - Utils public utils; - - string public signatureJson; - string public MerkleLeavesJson; - string public MerkleRootJson; - - address public daoAddress; - - function getSignature(uint256 _index) internal view returns (Signature memory) { - return abi.decode( - signatureJson.parseRaw(string(abi.encodePacked(".[", Strings.toString(_index), "]"))), (Signature) - ); - } - - // get detailed MerkleTree, which is located in `test/L2/data` and only being used by testing scripts - function getMerkleLeaves() internal view returns (MerkleLeaves memory) { - return abi.decode(MerkleLeavesJson.parseRaw("."), (MerkleLeaves)); - } - - // get MerkleRoot struct - function getMerkleRoot() internal view returns (Utils.MerkleRoot memory) { - return abi.decode(MerkleRootJson.parseRaw("."), (Utils.MerkleRoot)); - } - - // helper function to "invalidate" a proof or sig. (e.g. 0xabcdef -> 0xabcdf0) - function bytes32AddOne(bytes32 _value) internal pure returns (bytes32) { - return bytes32(uint256(_value) + 1); - } - function claimRegularAccount(uint256 _accountIndex) internal { uint256 originalBalance = lsk.balanceOf(RECIPIENT_ADDRESS); MerkleTreeLeaf memory leaf = getMerkleLeaves().leaves[_accountIndex]; @@ -117,43 +49,7 @@ contract L2ClaimTest is Test { } function setUp() public { - utils = new Utils(); - lsk = new MockERC20(10_000_000 * 10 ** 18); - (daoAddress,) = makeAddrAndKey("DAO"); - - console.log("L2ClaimTest Address is: %s", address(this)); - - // read Pre-signed Signatures, Merkle Leaves and a Merkle Root in a json format from different files - string memory rootPath = string.concat(vm.projectRoot(), "/test/L2/data"); - signatureJson = vm.readFile(string.concat(rootPath, "/signatures.json")); - MerkleLeavesJson = vm.readFile(string.concat(rootPath, "/merkle-leaves.json")); - MerkleRootJson = vm.readFile(string.concat(rootPath, "/merkle-root.json")); - - // get MerkleRoot struct - Utils.MerkleRoot memory merkleRoot = getMerkleRoot(); - - // deploy L2Claim Implementation contract - l2ClaimImplementation = new L2Claim(); - - // deploy L2Claim contract via Proxy and initialize it at the same time - l2Claim = L2Claim( - address( - new ERC1967Proxy( - address(l2ClaimImplementation), - abi.encodeWithSelector( - l2Claim.initialize.selector, - address(lsk), - merkleRoot.merkleRoot, - block.timestamp + RECOVER_PERIOD - ) - ) - ) - ); - assertEq(address(l2Claim.l2LiskToken()), address(lsk)); - assertEq(l2Claim.merkleRoot(), merkleRoot.merkleRoot); - - // send bunch of MockLSK to Claim contract - lsk.transfer(address(l2Claim), lsk.balanceOf(address(this))); + setUpL2Claim(); } function test_Initialize_RevertWhenL2LiskTokenIsZero() public { @@ -591,8 +487,11 @@ contract L2ClaimTest is Test { ); } - function test_SetDAOAddress_RevertWhenNotCalledByOwner() public { - address nobody = vm.addr(1); + function testFuzz_SetDAOAddress_RevertWhenNotCalledByOwner(uint256 _addressSeed) public { + _addressSeed = bound(_addressSeed, 1, type(uint160).max); + address nobody = vm.addr(_addressSeed); + + if (nobody == address(this)) return; vm.prank(nobody); vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, nobody)); @@ -626,9 +525,13 @@ contract L2ClaimTest is Test { l2Claim.recoverLSK(); } - function test_RecoverLSK_RevertWhenNotCalledByOwner() public { - l2Claim.setDAOAddress(daoAddress); - address nobody = vm.addr(1); + function testFuzz_RecoverLSK_RevertWhenNotCalledByOwner(uint256 _addressSeed) public { + _addressSeed = bound(_addressSeed, 1, type(uint160).max); + address nobody = vm.addr(_addressSeed); + + if (nobody == address(this)) { + return; + } vm.prank(nobody); vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, nobody)); @@ -669,9 +572,14 @@ contract L2ClaimTest is Test { assertEq(l2Claim.owner(), newOwner); } - function test_TransferOwnership_RevertWhenNotCalledByOwner() public { + function testFuzz_TransferOwnership_RevertWhenNotCalledByOwner(uint256 _addressSeed) public { + _addressSeed = bound(_addressSeed, 1, type(uint160).max); + address nobody = vm.addr(_addressSeed); address newOwner = vm.addr(1); - address nobody = vm.addr(2); + + if (nobody == address(this)) { + return; + } // owner is this contract assertEq(l2Claim.owner(), address(this)); @@ -683,22 +591,32 @@ contract L2ClaimTest is Test { vm.stopPrank(); } - function test_TransferOwnership_RevertWhenNotCalledByPendingOwner() public { + function testFuzz_TransferOwnership_RevertWhenNotCalledByPendingOwner(uint256 _addressSeed) public { address newOwner = vm.addr(1); l2Claim.transferOwnership(newOwner); assertEq(l2Claim.owner(), address(this)); - address nobody = vm.addr(2); + _addressSeed = bound(_addressSeed, 1, type(uint160).max); + address nobody = vm.addr(_addressSeed); + + if (nobody == newOwner) { + return; + } vm.prank(nobody); vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, nobody)); l2Claim.acceptOwnership(); } - function test_UpgradeToAndCall_RevertWhenNotOwner() public { + function testFuzz_UpgradeToAndCall_RevertWhenNotOwner(uint256 _addressSeed) public { // deploy L2Claim Implementation contract L2ClaimV2Mock l2ClaimV2Implementation = new L2ClaimV2Mock(); - address nobody = vm.addr(1); + _addressSeed = bound(_addressSeed, 1, type(uint160).max); + address nobody = vm.addr(_addressSeed); + + if (nobody == address(this)) { + return; + } vm.prank(nobody); vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, nobody)); diff --git a/test/L2/helper/L2ClaimHelper.sol b/test/L2/helper/L2ClaimHelper.sol new file mode 100644 index 00000000..0bede4b3 --- /dev/null +++ b/test/L2/helper/L2ClaimHelper.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.23; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Test, console, stdJson } from "forge-std/Test.sol"; +import { L2Claim, ED25519Signature, MultisigKeys } from "src/L2/L2Claim.sol"; +import { Utils } from "script/contracts/Utils.sol"; +import { MockERC20 } from "test/mock/MockERC20.sol"; + +struct SigPair { + bytes32 pubKey; + bytes32 r; + bytes32 s; +} + +struct Signature { + bytes message; + SigPair[] sigs; +} + +/// @notice This struct stores merkleTree leaf. +/// @dev Limitation of parseJSON, only bytes32 is supported. +/// To convert b32Address back to bytes20, shift 96 bits to the left. +/// i.e. bytes20(leaf.b32Address << 96) +struct MerkleTreeLeaf { + bytes32 b32Address; + uint64 balanceBeddows; + bytes32[] mandatoryKeys; + uint256 numberOfSignatures; + bytes32[] optionalKeys; + bytes32[] proof; +} + +/// @notice This struct is used to read MerkleLeaves from JSON file. +struct MerkleLeaves { + MerkleTreeLeaf[] leaves; +} + +contract L2ClaimHelper is Test { + using stdJson for string; + + Utils public utils; + + ERC20 public lsk; + L2Claim public l2ClaimImplementation; + L2Claim public l2Claim; + address public daoAddress; + + string public signatureJson; + string public merkleLeavesJson; + string public merkleRootJson; + bytes32 public merkleRootHex; + + /// @notice The destination address for claims as `address(uint160(uint256(keccak256("foundry default caller"))))` + /// and `nonce=2`. + address public constant RECIPIENT_ADDRESS = address(0x34A1D3fff3958843C43aD80F30b94c510645C316); + + /// @notice recover LSK tokens after 2 years + uint256 public constant RECOVER_PERIOD = 730 days; + + /// @notice initial balance of claim contract + uint256 public constant INIT_BALANCE = 10_000_000 ether; + + function getSignature(uint256 _index) internal view returns (Signature memory) { + return abi.decode( + signatureJson.parseRaw(string(abi.encodePacked(".[", Strings.toString(_index), "]"))), (Signature) + ); + } + + // get detailed MerkleTree, which is located in `test/L2/data` and only being used by testing scripts + function getMerkleLeaves() internal view returns (MerkleLeaves memory) { + return abi.decode(merkleLeavesJson.parseRaw("."), (MerkleLeaves)); + } + + // get MerkleRoot struct + function getMerkleRoot() internal view returns (Utils.MerkleRoot memory) { + return abi.decode(merkleRootJson.parseRaw("."), (Utils.MerkleRoot)); + } + + // helper function to "invalidate" a proof or sig. (e.g. 0xabcdef -> 0xabcdf0) + function bytes32AddOne(bytes32 _value) internal pure returns (bytes32) { + return bytes32(uint256(_value) + 1); + } + + function setUpL2Claim() internal { + lsk = new MockERC20(INIT_BALANCE); + (daoAddress,) = makeAddrAndKey("DAO"); + + console.log("L2ClaimTest Address is: %s", address(this)); + + // read Pre-signed Signatures, Merkle Leaves and a Merkle Root in a json format from different files + string memory rootPath = string.concat(vm.projectRoot(), "/test/L2/data"); + signatureJson = vm.readFile(string.concat(rootPath, "/signatures.json")); + merkleLeavesJson = vm.readFile(string.concat(rootPath, "/merkle-leaves.json")); + merkleRootJson = vm.readFile(string.concat(rootPath, "/merkle-root.json")); + + // get MerkleRoot struct + Utils.MerkleRoot memory merkleRoot = getMerkleRoot(); + merkleRootHex = merkleRoot.merkleRoot; + + // deploy L2Claim Implementation contract + l2ClaimImplementation = new L2Claim(); + + // deploy L2Claim contract via Proxy and initialize it at the same time + l2Claim = L2Claim( + address( + new ERC1967Proxy( + address(l2ClaimImplementation), + abi.encodeWithSelector( + l2Claim.initialize.selector, + address(lsk), + merkleRoot.merkleRoot, + block.timestamp + RECOVER_PERIOD + ) + ) + ) + ); + assertEq(address(l2Claim.l2LiskToken()), address(lsk)); + assertEq(l2Claim.merkleRoot(), merkleRoot.merkleRoot); + + // send bunch of MockLSK to Claim contract + lsk.transfer(address(l2Claim), lsk.balanceOf(address(this))); + } +} diff --git a/test/invariant/L1LiskTokenInvariants.t.sol b/test/invariant/L1LiskTokenInvariants.t.sol new file mode 100644 index 00000000..5810c343 --- /dev/null +++ b/test/invariant/L1LiskTokenInvariants.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.23; + +import { Test, console, stdJson } from "forge-std/Test.sol"; + +import { L1LiskToken } from "src/L1/L1LiskToken.sol"; +import { L1LiskTokenHandler } from "test/invariant/handler/L1LiskTokenHandler.t.sol"; + +contract L1LiskTokenInvariants is Test { + address public immutable burner = vm.addr(uint256(bytes32("burner"))); + L1LiskToken public l1LiskToken; + + L1LiskTokenHandler internal l1LiskTokenHandler; + + function setUp() public { + l1LiskToken = new L1LiskToken(); + l1LiskToken.addBurner(burner); + + l1LiskTokenHandler = new L1LiskTokenHandler(l1LiskToken); + + // L1 Token has fixed supply and not mintable, distributing tokens to addresses + uint256 totalPortions = l1LiskTokenHandler.numOfAddresses() * (l1LiskTokenHandler.numOfAddresses() + 1) / 2; + for (uint256 i = 1; i < l1LiskTokenHandler.numOfAddresses() + 1; i++) { + address balanceHolder = vm.addr(i); + + // Approve token to be burnt + vm.startPrank(balanceHolder); + l1LiskToken.approve(burner, type(uint256).max); + vm.stopPrank(); + + // #1 address gets 1 portion, #2 gets 2 portions etc. + l1LiskToken.transfer(balanceHolder, l1LiskToken.totalSupply() * i / totalPortions); + + // Last address gets the rest of the balances, to ensure address(this) has no balance left because of + // rounding + if (i == l1LiskTokenHandler.numOfAddresses()) { + l1LiskToken.transfer(balanceHolder, l1LiskToken.balanceOf(address(this))); + } + } + + // add the handler selectors to the fuzzing targets + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = L1LiskTokenHandler.burnFrom.selector; + + targetSelector(FuzzSelector({ addr: address(l1LiskTokenHandler), selectors: selectors })); + targetContract(address(l1LiskTokenHandler)); + } + + function invariant_L1LiskToken_metadataIsUnchanged() public view { + assertEq(l1LiskToken.name(), "Lisk"); + assertEq(l1LiskToken.symbol(), "LSK"); + assertEq(l1LiskToken.isBurner(burner), true); + } + + function invariant_L1LiskToken_totalBalancesEqualToTotalSupply() public view { + assertEq(l1LiskTokenHandler.totalBalances(), l1LiskToken.totalSupply()); + } +} diff --git a/test/invariant/L2ClaimInvariants.t.sol b/test/invariant/L2ClaimInvariants.t.sol new file mode 100644 index 00000000..a26b32cf --- /dev/null +++ b/test/invariant/L2ClaimInvariants.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.23; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { L2Claim } from "src/L2/L2Claim.sol"; +import { L2ClaimHandler } from "test/invariant/handler/L2ClaimHandler.t.sol"; +import { L2ClaimHelper, MerkleTreeLeaf } from "test/L2/helper/L2ClaimHelper.sol"; + +contract L2ClaimInvariants is L2ClaimHelper { + L2ClaimHandler internal l2ClaimHandler; + + function setUp() public { + setUpL2Claim(); + + l2ClaimHandler = new L2ClaimHandler(l2Claim, lsk, RECIPIENT_ADDRESS); + + // add the handler selectors to the fuzzing targets + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = L2ClaimHandler.claimRegularAccount.selector; + selectors[1] = L2ClaimHandler.claimMultisigAccount.selector; + + targetSelector(FuzzSelector({ addr: address(l2ClaimHandler), selectors: selectors })); + targetContract(address(l2ClaimHandler)); + } + + function invariant_L2Claim_metadataIsUnchanged() public view { + assertEq(address(l2Claim.l2LiskToken()), address(lsk)); + assertEq(l2Claim.merkleRoot(), merkleRootHex); + } + + function invariant_L2Claim_outAmountEqualToClaimAmount() public view { + assertEq(INIT_BALANCE - l2ClaimHandler.totalClaimed(), lsk.balanceOf(address(l2Claim))); + } +} diff --git a/test/invariant/L2LiskTokenInvariants.t.sol b/test/invariant/L2LiskTokenInvariants.t.sol new file mode 100644 index 00000000..b01afd64 --- /dev/null +++ b/test/invariant/L2LiskTokenInvariants.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.23; + +import { L2LiskToken } from "src/L2/L2LiskToken.sol"; +import { L2LiskTokenHandler } from "test/invariant/handler/L2LiskTokenHandler.t.sol"; +import { L2ClaimHelper } from "test/L2/helper/L2ClaimHelper.sol"; + +contract L2LiskTokenInvariants is L2ClaimHelper { + address public remoteToken; + address public bridge; + L2LiskToken public l2LiskToken; + + L2LiskTokenHandler internal l2LiskTokenHandler; + + function setUp() public { + bridge = vm.addr(uint256(bytes32("bridge"))); + remoteToken = vm.addr(uint256(bytes32("remoteToken"))); + + // msg.sender and tx.origin needs to be the same for the contract to be able to call initialize() + vm.prank(address(this), address(this)); + l2LiskToken = new L2LiskToken(remoteToken); + l2LiskToken.initialize(bridge); + vm.stopPrank(); + + l2LiskTokenHandler = new L2LiskTokenHandler(l2LiskToken); + + // add the handler selectors to the fuzzing targets + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = L2LiskTokenHandler.mint.selector; + selectors[1] = L2LiskTokenHandler.burn.selector; + + targetSelector(FuzzSelector({ addr: address(l2LiskTokenHandler), selectors: selectors })); + targetContract(address(l2LiskTokenHandler)); + } + + function invariant_L2LiskToken_metadataIsUnchanged() public view { + assertEq(l2LiskToken.name(), "Lisk"); + assertEq(l2LiskToken.symbol(), "LSK"); + assertEq(l2LiskToken.bridge(), bridge); + assertEq(l2LiskToken.remoteToken(), remoteToken); + } + + function invariant_L2LiskToken_totalBalancesEqualToTotalSupply() public view { + assertEq(l2LiskTokenHandler.totalBalances(), l2LiskToken.totalSupply()); + } +} diff --git a/test/invariant/handler/L1LiskTokenHandler.t.sol b/test/invariant/handler/L1LiskTokenHandler.t.sol new file mode 100644 index 00000000..7d6dc7d5 --- /dev/null +++ b/test/invariant/handler/L1LiskTokenHandler.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.23; + +import { Test, console } from "forge-std/Test.sol"; +import { L1LiskToken } from "src/L1/L1LiskToken.sol"; + +contract L1LiskTokenHandler is Test { + uint256 public constant numOfAddresses = 100; + + address public immutable burner = vm.addr(uint256(bytes32("burner"))); + L1LiskToken public immutable l1LiskToken; + + constructor(L1LiskToken _l1LiskToken) { + l1LiskToken = _l1LiskToken; + } + + function burnFrom(uint256 _addressIndex, uint256 _amount) public { + address from = vm.addr(bound(_addressIndex, 1, numOfAddresses)); + _amount = bound(_amount, 0, l1LiskToken.balanceOf(from)); + + vm.startPrank(burner); + l1LiskToken.burnFrom(from, _amount); + vm.stopPrank(); + } + + function totalBalances() public view returns (uint256 balances) { + for (uint256 i = 1; i <= numOfAddresses; i++) { + balances += l1LiskToken.balanceOf(vm.addr(i)); + } + + return balances; + } +} diff --git a/test/invariant/handler/L2ClaimHandler.t.sol b/test/invariant/handler/L2ClaimHandler.t.sol new file mode 100644 index 00000000..816c3f85 --- /dev/null +++ b/test/invariant/handler/L2ClaimHandler.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.23; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Test, stdJson } from "forge-std/Test.sol"; +import { L2Claim, ED25519Signature, MultisigKeys } from "src/L2/L2Claim.sol"; +import { L2ClaimHelper, MerkleTreeLeaf, Signature, MerkleLeaves } from "test/L2/helper/L2ClaimHelper.sol"; + +contract L2ClaimHandler is Test { + using stdJson for string; + + L2Claim public immutable l2Claim; + ERC20 public immutable lsk; + address public immutable recipientAddress; + + string public signatureJson; + string public merkleLeavesJson; + string public merkleRootJson; + bytes32 public merkleRootHex; + + // Invariant Test Params + uint256 public totalClaimed; + + constructor(L2Claim _l2Claim, ERC20 _lsk, address _recipientAddress) { + l2Claim = _l2Claim; + lsk = _lsk; + recipientAddress = _recipientAddress; + + string memory rootPath = string.concat(vm.projectRoot(), "/test/L2/data"); + signatureJson = vm.readFile(string.concat(rootPath, "/signatures.json")); + merkleLeavesJson = vm.readFile(string.concat(rootPath, "/merkle-leaves.json")); + merkleRootJson = vm.readFile(string.concat(rootPath, "/merkle-root.json")); + } + + function getSignature(uint256 _index) internal view returns (Signature memory) { + return abi.decode( + signatureJson.parseRaw(string(abi.encodePacked(".[", Strings.toString(_index), "]"))), (Signature) + ); + } + + // get detailed MerkleTree, which is located in `test/L2/data` and only being used by testing scripts + function getMerkleLeaves() internal view returns (MerkleLeaves memory) { + return abi.decode(merkleLeavesJson.parseRaw("."), (MerkleLeaves)); + } + + function claimRegularAccount(uint8 _index) public { + // index #0 - #49 are regular accounts + _index = uint8(bound(_index, 0, 49)); + + uint256 originalBalance = lsk.balanceOf(recipientAddress); + MerkleTreeLeaf memory leaf = getMerkleLeaves().leaves[_index]; + Signature memory signature = getSignature(_index); + + bytes32 pubKey = signature.sigs[0].pubKey; + + // check that the LSKClaimed event is emitted + vm.expectEmit(true, true, true, true); + emit L2Claim.LSKClaimed(bytes20(sha256(abi.encode(pubKey))), recipientAddress, leaf.balanceBeddows); + + l2Claim.claimRegularAccount( + leaf.proof, + pubKey, + leaf.balanceBeddows, + recipientAddress, + ED25519Signature(signature.sigs[0].r, signature.sigs[0].s) + ); + + assertEq(lsk.balanceOf(recipientAddress), originalBalance + leaf.balanceBeddows * l2Claim.LSK_MULTIPLIER()); + assertEq(l2Claim.claimedTo(bytes20(sha256(abi.encode(pubKey)))), recipientAddress); + + totalClaimed += leaf.balanceBeddows * l2Claim.LSK_MULTIPLIER(); + } + + function claimMultisigAccount(uint8 _index) public { + // index #50 - #53 are multisig accounts + _index = uint8(bound(_index, 0, 3)) + 50; + + MerkleTreeLeaf memory leaf = getMerkleLeaves().leaves[_index]; + Signature memory signature = getSignature(_index); + + ED25519Signature[] memory ed25519Signatures = + new ED25519Signature[](leaf.mandatoryKeys.length + leaf.optionalKeys.length); + for (uint256 i; i < leaf.numberOfSignatures; i++) { + ed25519Signatures[i] = ED25519Signature(signature.sigs[i].r, signature.sigs[i].s); + } + + bytes20 lskAddress = bytes20(leaf.b32Address << 96); + + // check that the LSKClaimed event is emitted + vm.expectEmit(true, true, true, true); + emit L2Claim.LSKClaimed(lskAddress, recipientAddress, leaf.balanceBeddows); + + l2Claim.claimMultisigAccount( + leaf.proof, + bytes20(leaf.b32Address << 96), + leaf.balanceBeddows, + MultisigKeys(leaf.mandatoryKeys, leaf.optionalKeys), + recipientAddress, + ed25519Signatures + ); + + assertEq(lsk.balanceOf(recipientAddress), leaf.balanceBeddows * l2Claim.LSK_MULTIPLIER()); + assertEq(l2Claim.claimedTo(lskAddress), recipientAddress); + + totalClaimed += leaf.balanceBeddows * l2Claim.LSK_MULTIPLIER(); + } +} diff --git a/test/invariant/handler/L2LiskTokenHandler.t.sol b/test/invariant/handler/L2LiskTokenHandler.t.sol new file mode 100644 index 00000000..bc7d1fef --- /dev/null +++ b/test/invariant/handler/L2LiskTokenHandler.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.23; + +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { Test, stdJson } from "forge-std/Test.sol"; +import { L2LiskToken } from "src/L2/L2LiskToken.sol"; + +contract L2LiskTokenHandler is Test { + using EnumerableSet for EnumerableSet.AddressSet; + + L2LiskToken public immutable l2LiskToken; + + EnumerableSet.AddressSet internal addressesWithInteraction; + + constructor(L2LiskToken _l2LiskToken) { + l2LiskToken = _l2LiskToken; + } + + function mint(uint256 _addressSeed, uint256 _amount) public { + address to = vm.addr(bound(_addressSeed, 1, type(uint160).max)); + _amount = bound(_amount, 0, type(uint96).max); + + vm.startPrank(l2LiskToken.bridge()); + l2LiskToken.mint(to, _amount); + vm.stopPrank(); + + addressesWithInteraction.add(to); + } + + function burn(uint256 _addressSeed, uint256 _amount) public { + address from = vm.addr(bound(_addressSeed, 1, type(uint160).max)); + _amount = bound(_amount, 0, l2LiskToken.balanceOf(from)); + + vm.startPrank(l2LiskToken.BRIDGE()); + l2LiskToken.burn(from, _amount); + vm.stopPrank(); + } + + function totalBalances() public view returns (uint256 balances) { + for (uint256 i; i < addressesWithInteraction.length(); i++) { + balances += l2LiskToken.balanceOf(addressesWithInteraction.at(i)); + } + + return balances; + } +}