diff --git a/test/token/ERC20/ERC20Bridgeble/ERC20BridgeableFacet.t.sol b/test/token/ERC20/ERC20Bridgeble/ERC20BridgeableFacet.t.sol new file mode 100644 index 00000000..8e901e2c --- /dev/null +++ b/test/token/ERC20/ERC20Bridgeble/ERC20BridgeableFacet.t.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {ERC20BridgeableFacet} from "../../../../src/token/ERC20/ERC20Bridgeable/ERC20BridgeableFacet.sol"; +import {ERC20BridgeableHarness} from "./harnesses/ERC20BridgeableHarness.sol"; + +contract ERC20BridgeableFacetTest is Test { + ERC20BridgeableHarness public token; + + address public alice; + address public bob; + + uint256 constant INITIAL_SUPPLY = 1000000e18; + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + + token = new ERC20BridgeableHarness(); + token.setRole(alice, "trusted-bridge", true); + vm.prank(alice); + token.crosschainMint(alice, INITIAL_SUPPLY); + } + + // ====================================== + // CrossChainMint Tests + // ====================================== + + function test_CrossChainMintRevertsInvalidCaller(address to, uint256 amount, address invalidCaller) public { + vm.assume(to != address(0)); + vm.assume(amount > 0 && amount < INITIAL_SUPPLY); + vm.assume(invalidCaller != alice); + vm.prank(invalidCaller); + vm.expectRevert( + abi.encodeWithSelector( + ERC20BridgeableFacet.AccessControlUnauthorizedAccount.selector, invalidCaller, bytes32("trusted-bridge") + ) + ); + token.crosschainMint(to, amount); + } + + function test_CrossChainMintRevertsInvalidReceiver(uint256 amount) public { + address to = address(0); + vm.assume(amount > 0 && amount < INITIAL_SUPPLY); + vm.expectRevert(abi.encodeWithSelector(ERC20BridgeableFacet.ERC20InvalidReciever.selector, to)); + vm.prank(alice); + token.crosschainMint(to, amount); + } + + function test_CrossChainMint() public { + vm.prank(alice); + token.crosschainMint(bob, 500e18); + assertEq(token.balanceOf(bob), 500e18); + } + + // ====================================== + // CrossChainBurn Tests + // ====================================== + + function test_CrossChainBurnRevertsInvalidCaller(address from, uint256 amount, address invalidCaller) public { + vm.assume(from != address(0)); + vm.assume(amount > 0 && amount < INITIAL_SUPPLY); + vm.assume(invalidCaller != alice); + vm.prank(alice); + token.crosschainMint(from, amount); + vm.prank(invalidCaller); + vm.expectRevert( + abi.encodeWithSelector( + ERC20BridgeableFacet.AccessControlUnauthorizedAccount.selector, invalidCaller, bytes32("trusted-bridge") + ) + ); + token.crosschainBurn(from, amount); + } + + function test_CrossChainBurnRevertsInvalidFrom(uint256 amount) public { + address from = address(0); + vm.assume(amount > 0 && amount < INITIAL_SUPPLY); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(ERC20BridgeableFacet.ERC20InvalidReciever.selector, from)); + token.crosschainBurn(from, amount); + } + + function test_CrossChainBurn() public { + vm.prank(alice); + token.crosschainMint(bob, 500e18); + assertEq(token.balanceOf(bob), 500e18); + vm.prank(alice); + token.crosschainBurn(bob, 500e18); + assertEq(token.balanceOf(bob), 0); + } + + // ====================================== + // checkTokenBridge Tests + // ====================================== + + function test_CheckTokenBridgeSucceeds(address caller) public { + vm.prank(alice); + token.checkTokenBridge(alice); + } + + function test_CheckTokenBridgeReverts(address invalidCaller) public { + vm.assume(invalidCaller != alice); + vm.prank(invalidCaller); + vm.expectRevert(abi.encodeWithSelector(ERC20BridgeableFacet.ERC20InvalidBridgeAccount.selector, invalidCaller)); + token.checkTokenBridge(invalidCaller); + } + + function test_CheckTokenBridgeRevertsZeroAddress() public { + address invalidCaller = address(0); + vm.prank(invalidCaller); + vm.expectRevert(abi.encodeWithSelector(ERC20BridgeableFacet.ERC20InvalidBridgeAccount.selector, invalidCaller)); + token.checkTokenBridge(invalidCaller); + } + + function test_CheckTokenBridgeSucceedsAfterRevokingRole() public { + token.setRole(alice, "trusted-bridge", false); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(ERC20BridgeableFacet.ERC20InvalidBridgeAccount.selector, alice)); + token.checkTokenBridge(alice); + } +} diff --git a/test/token/ERC20/ERC20Bridgeble/LibERC20Bridgeable.t.sol b/test/token/ERC20/ERC20Bridgeble/LibERC20Bridgeable.t.sol new file mode 100644 index 00000000..1a00f50b --- /dev/null +++ b/test/token/ERC20/ERC20Bridgeble/LibERC20Bridgeable.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {LibERC20Bridgeable} from "../../../../src/token/ERC20/ERC20Bridgeable/LibERC20Bridgeable.sol"; +import {LibERC20BridgeableHarness} from "./harnesses/LibERC20BridgeableHarness.sol"; + +contract LibERC20BridgeableTest is Test { + LibERC20BridgeableHarness public token; + + address public alice; + address public bob; + + uint256 constant INITIAL_SUPPLY = 1000000e18; + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + + token = new LibERC20BridgeableHarness(); + token.setRole(alice, "trusted-bridge", true); + vm.prank(alice); + token.crosschainMint(alice, INITIAL_SUPPLY); + } + + // ====================================== + // CrossChainMint Tests + // ====================================== + + function test_CrossChainMintRevertsInvalidCaller(address to, uint256 amount, address invalidCaller) public { + vm.assume(to != address(0)); + vm.assume(amount > 0 && amount < INITIAL_SUPPLY); + vm.assume(invalidCaller != alice); + vm.prank(invalidCaller); + vm.expectRevert( + abi.encodeWithSelector( + LibERC20Bridgeable.AccessControlUnauthorizedAccount.selector, invalidCaller, bytes32("trusted-bridge") + ) + ); + token.crosschainMint(to, amount); + } + + function test_CrossChainMintRevertsInvalidReceiver(uint256 amount) public { + address to = address(0); + vm.assume(amount > 0 && amount < INITIAL_SUPPLY); + vm.expectRevert(abi.encodeWithSelector(LibERC20Bridgeable.ERC20InvalidReciever.selector, to)); + vm.prank(alice); + token.crosschainMint(to, amount); + } + + function test_CrossChainMint() public { + vm.prank(alice); + token.crosschainMint(bob, 500e18); + assertEq(token.balanceOf(bob), 500e18); + } + + // ====================================== + // CrossChainBurn Tests + // ====================================== + + function test_CrossChainBurnRevertsInvalidCaller(address from, uint256 amount, address invalidCaller) public { + vm.assume(from != address(0)); + vm.assume(amount > 0 && amount < INITIAL_SUPPLY); + vm.assume(invalidCaller != alice); + vm.prank(alice); + token.crosschainMint(from, amount); + vm.prank(invalidCaller); + vm.expectRevert( + abi.encodeWithSelector( + LibERC20Bridgeable.AccessControlUnauthorizedAccount.selector, invalidCaller, bytes32("trusted-bridge") + ) + ); + token.crosschainBurn(from, amount); + } + + function test_CrossChainBurnRevertsInvalidFrom(uint256 amount) public { + address from = address(0); + vm.assume(amount > 0 && amount < INITIAL_SUPPLY); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(LibERC20Bridgeable.ERC20InvalidReciever.selector, from)); + token.crosschainBurn(from, amount); + } + + function test_CrossChainBurn() public { + vm.prank(alice); + token.crosschainMint(bob, 500e18); + assertEq(token.balanceOf(bob), 500e18); + + vm.prank(alice); + token.crosschainBurn(bob, 200e18); + assertEq(token.balanceOf(bob), 300e18); + } + + // ====================================== + // checkTokenBridge Tests + // ====================================== + + function test_CheckTokenBridgeSucceedsValidCaller() public { + vm.prank(alice); + token.checkTokenBridge(alice); + } + + function test_CheckTokenBridgeRevertsInvalidCaller(address invalidCaller) public { + vm.assume(invalidCaller != alice); + vm.assume(invalidCaller != address(0)); + vm.prank(invalidCaller); + vm.expectRevert(abi.encodeWithSelector(LibERC20Bridgeable.ERC20InvalidBridgeAccount.selector, invalidCaller)); + token.checkTokenBridge(invalidCaller); + } + + function test_CheckTokenBridgeRevertsZeroCaller() public { + address zeroAddress = address(0); + vm.prank(zeroAddress); + vm.expectRevert(abi.encodeWithSelector(LibERC20Bridgeable.ERC20InvalidBridgeAccount.selector, zeroAddress)); + token.checkTokenBridge(zeroAddress); + } +} diff --git a/test/token/ERC20/ERC20Bridgeble/harnesses/ERC20BridgeableHarness.sol b/test/token/ERC20/ERC20Bridgeble/harnesses/ERC20BridgeableHarness.sol new file mode 100644 index 00000000..62cef3bc --- /dev/null +++ b/test/token/ERC20/ERC20Bridgeble/harnesses/ERC20BridgeableHarness.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC20BridgeableFacet} from "../../../../../src/token/ERC20/ERC20Bridgeable/ERC20BridgeableFacet.sol"; + +contract ERC20BridgeableHarness is ERC20BridgeableFacet { + /** + * @notice Sets or unsets a specific role for an account. + * @param account The address of the account to modify. + * @param role The bytes32 identifier of the role. + * @param value Set to true to grant the role, or false to revoke. + */ + function setRole(address account, bytes32 role, bool value) external { + ERC20BridgeableFacet.AccessControlStorage storage acs = ERC20BridgeableFacet.getAccessControlStorage(); + acs.hasRole[account][role] = value; + } + + /** + * @notice Gets the balance of a specified account. + * @param _account The address of the account to query. + * @return The balance of the specified account. + */ + function balanceOf(address _account) external view returns (uint256) { + ERC20BridgeableFacet.ERC20Storage storage s = ERC20BridgeableFacet.getERC20Storage(); + return s.balanceOf[_account]; + } +} diff --git a/test/token/ERC20/ERC20Bridgeble/harnesses/LibERC20BridgeableHarness.sol b/test/token/ERC20/ERC20Bridgeble/harnesses/LibERC20BridgeableHarness.sol new file mode 100644 index 00000000..59064847 --- /dev/null +++ b/test/token/ERC20/ERC20Bridgeble/harnesses/LibERC20BridgeableHarness.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {LibERC20Bridgeable} from "../../../../../src/token/ERC20/ERC20Bridgeable/LibERC20Bridgeable.sol"; + +contract LibERC20BridgeableHarness { + /// @notice Mints tokens to a specified address on behalf of the "trusted-bridge" role. + /// @param _to The address receiving the minted tokens. [NATPSEC: Only trusted bridge callers] + /// @param _amount The amount of tokens to mint. [NATPSEC: Only trusted bridge callers] + function crosschainMint(address _to, uint256 _amount) external { + LibERC20Bridgeable.crosschainMint(_to, _amount); + } + + /// @notice Returns the token balance of a specified account. + /// @param _account The account for which to query the balance. [NATPSEC: Read-only] + /// @return The current balance of the account. + function balanceOf(address _account) external view returns (uint256) { + LibERC20Bridgeable.ERC20Storage storage s = LibERC20Bridgeable.getERC20Storage(); + return s.balanceOf[_account]; + } + + /// @notice Burns tokens from a specified address on behalf of the "trusted-bridge" role. + /// @param _from The address whose tokens will be burned. [NATPSEC: Only trusted bridge callers] + /// @param _amount The amount of tokens to burn. [NATPSEC: Only trusted bridge callers] + function crosschainBurn(address _from, uint256 _amount) external { + LibERC20Bridgeable.crosschainBurn(_from, _amount); + } + + /// @notice Validates whether the caller has the token bridge role. + /// @param _caller The address to check for the "trusted-bridge" role. [NATPSEC: Internal access control] + function checkTokenBridge(address _caller) external view { + LibERC20Bridgeable.checkTokenBridge(_caller); + } + + /// @notice Grants or revokes a role for a given account, for harness/testing only. + /// @param account The account to grant or revoke the role for. [NATPSEC: Test utility only] + /// @param role The bytes32 identifier of the role. [NATPSEC: Test utility only] + /// @param value True to grant the role, false to revoke. [NATPSEC: Test utility only] + function setRole(address account, bytes32 role, bool value) external { + LibERC20Bridgeable.AccessControlStorage storage acs = LibERC20Bridgeable.getAccessControlStorage(); + acs.hasRole[account][role] = value; + } +}