Skip to content

Commit

Permalink
feat: write test for property 5
Browse files Browse the repository at this point in the history
* refactor: update setup

* chore: create optimism superchain erc20 mock contract with compatible compiler version

* refactor: update 3 and 4 properties tests
  • Loading branch information
0xDiscotech committed Sep 13, 2024
1 parent 8fac159 commit 984947c
Show file tree
Hide file tree
Showing 2 changed files with 375 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity 0.8.15;

import { Predeploys } from "src/libraries/Predeploys.sol";
import { L2StandardBridgeInterop } from "src/L2/L2StandardBridgeInterop.sol";
import { L2StandardBridge } from "src/L2/L2StandardBridge.sol";
import { Test } from "forge-std/Test.sol";
import { KontrolCheats } from "kontrol-cheatcodes/KontrolCheats.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
Expand All @@ -14,21 +13,48 @@ import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol
import { ILegacyMintableERC20 } from "src/universal/OptimismMintableERC20.sol";
import { IOptimismERC20Factory } from "src/L2/interfaces/IOptimismERC20Factory.sol";
import { IOptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol";
import { OptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol";
import { OptimismSuperchainERC20Mock } from "./helpers/OptimismSuperchainERC20Mock.sol";

import "forge-std/Test.sol";

contract L2StandardBridgeInteropKontrol is Test, KontrolCheats {
L2StandardBridgeInterop public l2StandardBridgeInterop;
address payable public constant otherBridge = payable(address(uint160(uint256(keccak256("otherBridge")))));
address public constant from = address(uint160(uint256(keccak256("from"))));
address public constant to = address(uint160(uint256(keccak256("to"))));
address payable public constant OTHER_BRIDGE = payable(address(uint160(uint256(keccak256("otherBridge")))));
address payable public constant REMOTE_TOKEN = payable(address(uint160(uint256(keccak256("remoteToken")))));
uint256 public constant RANDOM_AMOUNT = 100;
uint256 public constant ZERO_AMOUNT = 0;
uint8 public constant DECIMALS = 18;
L2StandardBridgeInterop public immutable L2_BRIDGE = L2StandardBridgeInterop(payable(Predeploys.L2_STANDARD_BRIDGE));
string public legacyName = "Legacy";
string public legacySymbol = "LEGACY";
string public superName = "Super";
string public superSymbol = "SUPER";

OptimismMintableERC20 public legacyToken;
OptimismSuperchainERC20Mock public superToken;

// Not declaring as `setUp` for performance reasons
function setUpInlined() public {
l2StandardBridgeInterop = L2StandardBridgeInterop(
payable(
address(
new ERC1967Proxy(
address(new L2StandardBridgeInterop()),
abi.encodeCall(L2StandardBridge.initialize, (StandardBridge(otherBridge)))
// Deploy L2 Standard Bridge Interop and etch it into the L2 Standard Bridge predeploy
address l2StandardBridgeInteropImpl = address(new L2StandardBridgeInterop());
vm.etch(address(L2_BRIDGE), address(l2StandardBridgeInteropImpl).code);

// Update the implementation slot and initialize the bridge
bytes32 _implementationSlot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
vm.store(address(L2_BRIDGE), _implementationSlot, bytes32(uint256(uint160(l2StandardBridgeInteropImpl))));
L2_BRIDGE.initialize(StandardBridge(OTHER_BRIDGE));

// Deploy legacy
legacyToken = new OptimismMintableERC20(address(L2_BRIDGE), REMOTE_TOKEN, legacyName, legacySymbol, DECIMALS);

// Deploy supertoken
superToken = OptimismSuperchainERC20Mock(
address(
// TODO: Update to beacon proxy
new ERC1967Proxy(
address(new OptimismSuperchainERC20Mock()),
abi.encodeCall(
OptimismSuperchainERC20Mock.initialize, (REMOTE_TOKEN, superName, superSymbol, DECIMALS)
)
)
)
Expand All @@ -39,116 +65,198 @@ contract L2StandardBridgeInteropKontrol is Test, KontrolCheats {
return keccak256(abi.encode(a)) == keccak256(abi.encode(b));
}

// TODO: Remove convert prefix
function prove_convertSetup() public {
/// @notice Check the setup works as expected
function test_convertSetup() public {
setUpInlined();
assert(eqStrings(l2StandardBridgeInterop.version(), "1.11.1-beta.1+interop"));
assertEq(address(l2StandardBridgeInterop.OTHER_BRIDGE()), otherBridge);

// L2 Standard Bridge Interop checks
assert(address(L2_BRIDGE.OTHER_BRIDGE()) == OTHER_BRIDGE);
assert(eqStrings(L2_BRIDGE.version(), "1.11.1-beta.1+interop"));

// Legacy token checks
assert(legacyToken.REMOTE_TOKEN() == REMOTE_TOKEN);
assert(legacyToken.BRIDGE() == address(L2_BRIDGE));
assert(legacyToken.decimals() == DECIMALS);
assert(eqStrings(legacyToken.version(), "1.3.1-beta.1"));
assert(eqStrings(legacyToken.name(), legacyName));
assert(eqStrings(legacyToken.symbol(), legacySymbol));

// Super token checks
assert(eqStrings(superToken.version(), "1.0.0-beta.2"));
assert(superToken.remoteToken() == REMOTE_TOKEN);
assert(eqStrings(superToken.name(), superName));
assert(eqStrings(superToken.symbol(), superSymbol));
assert(superToken.decimals() == DECIMALS);
}

/// @custom:property-id 3
/// @custom:property convert() only allows migrations between tokens representing the same remote asset
function prove_convertOnlyOnSameRemoteAsset(
address _fromRemoteAddress,
address _toRemoteAddress,
bool _supportsIERC165,
bool _supportsILegacyMintableERC20,
bool _supportsIOptimismMintableERC20
function test_convertOnlyOnSameRemoteAsset(
bool _legacyIsFrom,
address _legacyRemoteAddress,
address _superRemoteAddress,
address _sender
)
public
{
setUpInlined();

/* Preconditions */
// Mock the decimals of the tokens to the same values so it doesn't revert
uint8 _decimals = 18;
vm.mockCall(from, abi.encodeWithSelector(IERC20Metadata.decimals.selector), abi.encode(_decimals));
vm.mockCall(to, abi.encodeWithSelector(IERC20Metadata.decimals.selector), abi.encode(_decimals));
// Mock the call over `supportsInterface` - not in the scope of the test, but required to avoid a revert
vm.mockCall(
from,
abi.encodeWithSelector(IERC165.supportsInterface.selector, type(IERC165).interfaceId),
abi.encode(_supportsIERC165)
);
// Mock the call over `supportsInterface` - not in the scope of the test, but required to avoid a revert
vm.mockCall(
from,
abi.encodeWithSelector(IERC165.supportsInterface.selector, type(ILegacyMintableERC20).interfaceId),
abi.encode(_supportsILegacyMintableERC20)
);
vm.mockCall(
from,
abi.encodeWithSelector(IERC165.supportsInterface.selector, type(IOptimismMintableERC20).interfaceId),
abi.encode(_supportsIOptimismMintableERC20)
);
vm.assume(_sender != address(0));

// Mock the call over `deployments` for both tokens with the given symbolic remote addresses
vm.mockCall(
Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY,
abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector),
abi.encode(address(_fromRemoteAddress))
abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector, address(legacyToken)),
abi.encode(address(_legacyRemoteAddress))
);
vm.mockCall(
Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY,
abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector),
abi.encode(address(_toRemoteAddress))
abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector, address(superToken)),
abi.encode(address(_superRemoteAddress))
);

uint256 _randomAmount = 100;
/* Action */
try l2StandardBridgeInterop.convert(from, to, _randomAmount) {
/* Postconditions */
// Assume the addresses are not zero and they match
assert(_fromRemoteAddress != address(0));
assert(_fromRemoteAddress == _toRemoteAddress);
} catch {
// Assert the addresses differ or that they are both zero
assert(_fromRemoteAddress != _toRemoteAddress || _fromRemoteAddress == address(0));
vm.startPrank(_sender);

bool success;
if (_legacyIsFrom) {
/* Action */
// Execute `convert` with `legacyToken` as the `from` token
(success,) = address(L2_BRIDGE).call(
abi.encodeWithSelector(
L2_BRIDGE.convert.selector,
address(legacyToken),
address(superToken),
ZERO_AMOUNT // Amount is not relevant for this test
)
);
} else {
// Execute `convert` with `superToken` as the `from` token
(success,) = address(L2_BRIDGE).call(
abi.encodeWithSelector(
L2_BRIDGE.convert.selector,
address(superToken),
address(legacyToken),
ZERO_AMOUNT // Amount is not relevant for this test
)
);
}

/* Preconditions */
// The property should hold regardless of the order of the tokens
if (success) {
assert(_legacyRemoteAddress != address(0));
assert(_legacyRemoteAddress == _superRemoteAddress);
} else {
assert(_legacyRemoteAddress != _superRemoteAddress || _legacyRemoteAddress == address(0));
}
}

/// @custom:property-id 4
/// @custom:property convert() only allows migrations from tokens with the same decimals
function prove_convertOnlyTokenWithSameDecimals(uint8 _decimalsFrom, uint8 _decimalsTo) public {
function test_convertOnlyTokenWithSameDecimals(
bool _fromIsLegacy,
uint8 _decimalsLegacy,
uint8 _decimalsSuper,
address _sender
)
public
{
setUpInlined();

/* Preconditions */
// Mock the decimals of the tokens with the given symbolic decimals values
vm.mockCall(from, abi.encodeWithSelector(IERC20Metadata.decimals.selector), abi.encode(_decimalsFrom));
vm.mockCall(to, abi.encodeWithSelector(IERC20Metadata.decimals.selector), abi.encode(_decimalsTo));
vm.assume(_sender != address(0));

// Mock the call over `supportsInterface` - not in the scope of the test, but required to avoid a revert
// Mock calls over `decimals`
vm.mockCall(
from,
abi.encodeWithSelector(IERC165.supportsInterface.selector, type(IERC165).interfaceId),
abi.encode(true)
address(legacyToken), abi.encodeWithSelector(IERC20Metadata.decimals.selector), abi.encode(_decimalsLegacy)
);
vm.mockCall(
from,
abi.encodeWithSelector(IERC165.supportsInterface.selector, type(ILegacyMintableERC20).interfaceId),
abi.encode(true)
address(superToken), abi.encodeWithSelector(IERC20Metadata.decimals.selector), abi.encode(_decimalsSuper)
);

// Mock the call over `deployments` - not in the scope of the test, but required to avoid a revert
address _randomAddress = address(420);
vm.mockCall(
Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY,
abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector),
abi.encode(address(_randomAddress))
abi.encode(REMOTE_TOKEN)
);
vm.mockCall(
Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY,
abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector),
abi.encode(address(_randomAddress))
abi.encode(REMOTE_TOKEN)
);

uint256 _randomAmount = 100;
vm.prank(_sender);

// Using zero amount since it is not relevant for this test
bool _success;
if (_fromIsLegacy) {
/* Action */
// Execute `convert` with `legacyToken` as the `from` token
(_success,) = address(L2_BRIDGE).call(
abi.encodeWithSelector(
L2_BRIDGE.convert.selector,
address(legacyToken),
address(superToken),
ZERO_AMOUNT // Amount is not relevant for this test
)
);
} else {
// Execute `convert` with `superToken` as the `from` token
(_success,) = address(L2_BRIDGE).call(
abi.encodeWithSelector(
L2_BRIDGE.convert.selector,
address(superToken),
address(legacyToken),
ZERO_AMOUNT // Amount is not relevant for this test
)
);
}

/* Preconditions */
// The property should hold regardless of the order of the tokens
if (_success) assert(_decimalsLegacy == _decimalsSuper);
else assert(_decimalsLegacy != _decimalsSuper);
}

/// @custom:property-id 5
/// @custom:property convert() burns the same amount of legacy token that it mints of supertoken, and viceversa
function test_mintAndBurnSameAmount(address _sender, bool _legacyIsFrom, uint256 _amount) public {
setUpInlined();

/* Preconditions */
vm.assume(_sender != address(0));
// Mock the call over `deployments` - not in the scope of the test, but required to avoid a revert
vm.mockCall(
Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY,
abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector),
abi.encode(REMOTE_TOKEN)
);
vm.mockCall(
Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY,
abi.encodeWithSelector(IOptimismERC20Factory.deployments.selector),
abi.encode(REMOTE_TOKEN)
);

// Mint tokens to the sender and get the balances before the conversion
vm.prank(address(L2_BRIDGE));
if (_legacyIsFrom) legacyToken.mint(_sender, _amount);
else superToken.mint(_sender, _amount);
uint256 legacyBalanceBefore = legacyToken.balanceOf(_sender);
uint256 superBalanceBefore = superToken.balanceOf(_sender);

vm.prank(_sender);
/* Action */
try l2StandardBridgeInterop.convert(from, to, _randomAmount) {
if (_legacyIsFrom) {
L2_BRIDGE.convert(address(legacyToken), address(superToken), _amount);
/* Postconditions */
assert(_decimalsFrom == _decimalsTo);
} catch {
assert(_decimalsFrom != _decimalsTo);
assert(legacyToken.balanceOf(_sender) == legacyBalanceBefore - _amount);
assert(superToken.balanceOf(_sender) == superBalanceBefore + _amount);
} else {
L2_BRIDGE.convert(address(superToken), address(legacyToken), _amount);
assert(legacyToken.balanceOf(_sender) == legacyBalanceBefore + _amount);
assert(superToken.balanceOf(_sender) == superBalanceBefore - _amount);
}
}
}
Loading

0 comments on commit 984947c

Please sign in to comment.