diff --git a/src/bridge/extra/ERC20MigrationOutbox.sol b/src/bridge/extra/ERC20MigrationOutbox.sol new file mode 100644 index 00000000..b7bcdcb6 --- /dev/null +++ b/src/bridge/extra/ERC20MigrationOutbox.sol @@ -0,0 +1,44 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro-contracts/blob/main/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.4; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Bridge} from "../IERC20Bridge.sol"; +import {IERC20MigrationOutbox} from "./IERC20MigrationOutbox.sol"; + +/** + * @title An outbox for migrating nativeToken of a rollup from the native bridge to a new address + * @notice For some custom fee token orbit chains, they might want to have their native token being managed by an external bridge. This + * contract allow permissionless migration of the native bridge collateral, without requiring any change in the vanilla outbox. + * @dev This contract should be allowed as an outbox in conjunction with the vanilla outbox contract. Nonzero value withdrawal via the + * native bridge (ArbSys) must be disabled on the child chain or funds and messages will be stuck. + */ +contract ERC20MigrationOutbox is IERC20MigrationOutbox { + IERC20Bridge public immutable bridge; + address public immutable nativeToken; + address public immutable destination; + + constructor(IERC20Bridge _bridge, address _destination) { + if (_destination == address(0)) { + revert InvalidDestination(); + } + bridge = _bridge; + destination = _destination; + nativeToken = bridge.nativeToken(); + } + + /// @inheritdoc IERC20MigrationOutbox + function migrate() external { + uint256 bal = IERC20(nativeToken).balanceOf(address(bridge)); + if (bal == 0) { + revert NoBalanceToMigrate(); + } + (bool success, bytes memory returndata) = bridge.executeCall(destination, bal, ""); + if (!success) { + revert MigrationFailed(returndata); + } + emit CollateralMigrated(destination, bal); + } +} diff --git a/src/bridge/extra/IERC20MigrationOutbox.sol b/src/bridge/extra/IERC20MigrationOutbox.sol new file mode 100644 index 00000000..8b748023 --- /dev/null +++ b/src/bridge/extra/IERC20MigrationOutbox.sol @@ -0,0 +1,38 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro-contracts/blob/main/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.4; + +import {IERC20Bridge} from "../IERC20Bridge.sol"; + +interface IERC20MigrationOutbox { + /// @notice Thrown when there is no balance to migrate. + error NoBalanceToMigrate(); + + /// @notice Thrown when the migration process fails. + /// @param returndata The return data from the failed migration call. + error MigrationFailed(bytes returndata); + + /// @notice Thrown when the destination address is invalid. + error InvalidDestination(); + + /// @notice Emitted when a migration is completed. + event CollateralMigrated(address indexed destination, uint256 amount); + + /// @notice Returns the address of the bridge contract. + /// @return The IERC20Bridge contract address. + function bridge() external view returns (IERC20Bridge); + + /// @notice Returns the address of the native token to be migrated. + /// @return The address of the native token. + function nativeToken() external view returns (address); + + /// @notice Returns the destination address for the migration. + /// @return The address where the native token will be migrated to. + function destination() external view returns (address); + + /// @notice Migrate the native token of the rollup to the destination address. + /// @dev Can be called by anyone. Reverts if there is no balance to migrate or if the migration fails. + function migrate() external; +} diff --git a/test/foundry/ERC20MigrationOutbox.t.sol b/test/foundry/ERC20MigrationOutbox.t.sol new file mode 100644 index 00000000..4ae2dc7c --- /dev/null +++ b/test/foundry/ERC20MigrationOutbox.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +import "forge-std/Test.sol"; +import "./util/TestUtil.sol"; +import "../../src/bridge/IERC20Bridge.sol"; +import "../../src/bridge/ERC20Bridge.sol"; +import "../../src/bridge/extra/ERC20MigrationOutbox.sol"; +import {NoZeroTransferToken} from "./util/NoZeroTransferToken.sol"; + +contract ERC20MigrationOutboxTest is Test { + IERC20Bridge public bridge; + + IERC20MigrationOutbox public erc20MigrationOutbox; + IERC20Bridge public erc20Bridge; + IERC20 public nativeToken; + + address public user = address(100); + address public rollup = address(1000); + address public seqInbox = address(1001); + address public constant dst = address(1337); + + function setUp() public { + // deploy token, bridge and erc20MigrationOutbox + nativeToken = new NoZeroTransferToken("Appchain Token", "App", 1_000_000, address(this)); + bridge = IERC20Bridge(TestUtil.deployProxy(address(new ERC20Bridge()))); + erc20Bridge = IERC20Bridge(address(bridge)); + + // init bridge + erc20Bridge.initialize(IOwnable(rollup), address(nativeToken)); + + // deploy erc20MigrationOutbox + erc20MigrationOutbox = new ERC20MigrationOutbox(bridge, dst); + + // set outbox + vm.prank(rollup); + bridge.setOutbox(address(erc20MigrationOutbox), true); + } + + function test_invalid_destination() public { + vm.expectRevert(IERC20MigrationOutbox.InvalidDestination.selector); + new ERC20MigrationOutbox(bridge, address(0)); + } + + function test_migrate() public { + nativeToken.transfer(address(bridge), 1000); + + vm.prank(user); + erc20MigrationOutbox.migrate(); + + assertEq(nativeToken.balanceOf(dst), 1000); + } + + function test_migrate_no_balance() public { + vm.expectRevert(IERC20MigrationOutbox.NoBalanceToMigrate.selector); + vm.prank(user); + erc20MigrationOutbox.migrate(); + } +} diff --git a/test/signatures/ERC20MigrationOutbox b/test/signatures/ERC20MigrationOutbox new file mode 100644 index 00000000..98d56e85 --- /dev/null +++ b/test/signatures/ERC20MigrationOutbox @@ -0,0 +1,13 @@ + +╭---------------+------------╮ +| Method | Identifier | ++============================+ +| bridge() | e78cea92 | +|---------------+------------| +| destination() | b269681d | +|---------------+------------| +| migrate() | 8fd3ab80 | +|---------------+------------| +| nativeToken() | e1758bd8 | +╰---------------+------------╯ + diff --git a/test/signatures/test-sigs.bash b/test/signatures/test-sigs.bash index 549e5adf..ebee3054 100755 --- a/test/signatures/test-sigs.bash +++ b/test/signatures/test-sigs.bash @@ -1,6 +1,6 @@ #!/bin/bash output_dir="./test/signatures" -for CONTRACTNAME in Bridge Inbox Outbox RollupCore RollupUserLogic RollupAdminLogic SequencerInbox EdgeChallengeManager ERC20Bridge ERC20Inbox ERC20Outbox BridgeCreator DeployHelper RollupCreator OneStepProofEntry CacheManager +for CONTRACTNAME in Bridge Inbox Outbox RollupCore RollupUserLogic RollupAdminLogic SequencerInbox EdgeChallengeManager ERC20Bridge ERC20Inbox ERC20Outbox BridgeCreator DeployHelper RollupCreator OneStepProofEntry CacheManager ERC20MigrationOutbox do echo "Checking for signature changes in $CONTRACTNAME" [ -f "$output_dir/$CONTRACTNAME" ] && mv "$output_dir/$CONTRACTNAME" "$output_dir/$CONTRACTNAME-old" diff --git a/test/storage/ERC20MigrationOutbox b/test/storage/ERC20MigrationOutbox new file mode 100644 index 00000000..1ec5dc07 --- /dev/null +++ b/test/storage/ERC20MigrationOutbox @@ -0,0 +1,6 @@ + +╭------+------+------+--------+-------+----------╮ +| Name | Type | Slot | Offset | Bytes | Contract | ++================================================+ +╰------+------+------+--------+-------+----------╯ + diff --git a/test/storage/test.bash b/test/storage/test.bash index d738f838..97e2b982 100755 --- a/test/storage/test.bash +++ b/test/storage/test.bash @@ -1,6 +1,6 @@ #!/bin/bash output_dir="./test/storage" -for CONTRACTNAME in Bridge Inbox Outbox RollupCore RollupUserLogic RollupAdminLogic SequencerInbox EdgeChallengeManager ERC20Bridge ERC20Inbox ERC20Outbox OneStepProofEntry CacheManager +for CONTRACTNAME in Bridge Inbox Outbox RollupCore RollupUserLogic RollupAdminLogic SequencerInbox EdgeChallengeManager ERC20Bridge ERC20Inbox ERC20Outbox OneStepProofEntry CacheManager ERC20MigrationOutbox do echo "Checking storage change of $CONTRACTNAME" [ -f "$output_dir/$CONTRACTNAME" ] && mv "$output_dir/$CONTRACTNAME" "$output_dir/$CONTRACTNAME-old"