Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions contracts/remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ src/universal/=lib/optimism/packages/contracts-bedrock/src/universal/
src/vendor/=lib/optimism/packages/contracts-bedrock/src/vendor/
scripts/=lib/optimism/packages/contracts-bedrock/scripts/
@nitro-validator/=lib/nitro-validator/src/
src/legacy/=lib/optimism/packages/contracts-bedrock/src/legacy/
15 changes: 15 additions & 0 deletions contracts/script/DeployPortalWithChainExit.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {Script} from "forge-std/Script.sol";
import {PortalWithChainExit} from "../src/PortalWithChainExit.sol";
import {console2 as console} from "forge-std/console2.sol";

contract DeployPortalWithChainExit is Script {
function run() public {
vm.startBroadcast();
PortalWithChainExit portalWithChainExit = new PortalWithChainExit();
vm.stopBroadcast();
console.log("Deploying PortalWithChainOwnerExit at:", address(portalWithChainExit));
}
}
16 changes: 3 additions & 13 deletions contracts/src/Portal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,7 @@ contract Portal is Initializable, ResourceMetering, ISemver {
}

_depositTransaction({
_to: _to,
_mint: _mint,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
_to: _to, _mint: _mint, _value: _value, _gasLimit: _gasLimit, _isCreation: _isCreation, _data: _data
});
}

Expand All @@ -410,12 +405,7 @@ contract Portal is Initializable, ResourceMetering, ISemver {
if (token != Constants.ETHER && msg.value != 0) revert NoValue();

_depositTransaction({
_to: _to,
_mint: msg.value,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
_to: _to, _mint: msg.value, _value: _value, _gasLimit: _gasLimit, _isCreation: _isCreation, _data: _data
});
}

Expand Down Expand Up @@ -505,4 +495,4 @@ contract Portal is Initializable, ResourceMetering, ISemver {
function _isFinalizationPeriodElapsed(uint256 _timestamp) internal view returns (bool) {
return block.timestamp > _timestamp;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think removing this bracket causes the code to not compile anymore

}
}
585 changes: 585 additions & 0 deletions contracts/src/PortalWithChainExit.sol

Large diffs are not rendered by default.

271 changes: 271 additions & 0 deletions contracts/test/PortalWithChainOwnerExit.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {Test} from "forge-std/Test.sol";
import {ProxyAdmin} from "@eth-optimism-bedrock/src/universal/ProxyAdmin.sol";
import {Proxy} from "@eth-optimism-bedrock/src/universal/Proxy.sol";
import {ISuperchainConfig} from "@eth-optimism-bedrock/src/L1/interfaces/ISuperchainConfig.sol";
import {ISystemConfig} from "@eth-optimism-bedrock/src/L1/interfaces/ISystemConfig.sol";
import {IResourceMetering} from "@eth-optimism-bedrock/src/L1/interfaces/IResourceMetering.sol";
import {Constants} from "@eth-optimism-bedrock/src/libraries/Constants.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import {PortalWithChainExit} from "../src/PortalWithChainExit.sol";
import {OutputOracle} from "../src/OutputOracle.sol";

/// @notice Mock ERC20 token for testing
contract MockERC20 is ERC20 {
constructor() ERC20("Mock Token", "MCK") {}

function mint(address to, uint256 amount) external {
_mint(to, amount);
}

function decimals() public pure override returns (uint8) {
return 18;
}
}

/// @notice Mock SuperchainConfig for testing
contract MockSuperchainConfig is ISuperchainConfig {
bool internal _paused;
address internal _guardian;

function setPaused(bool paused_) external {
_paused = paused_;
}

function paused() external view returns (bool) {
return _paused;
}

function guardian() external view returns (address) {
return _guardian;
}

function GUARDIAN_SLOT() external pure returns (bytes32) {
return bytes32(0);
}

function PAUSED_SLOT() external pure returns (bytes32) {
return bytes32(0);
}

function initialize(address, bool) external {}

function pause(string memory) external {}

function unpause() external {}

function version() external pure returns (string memory) {
return "1.0.0";
}
}

/// @notice Mock SystemConfig for testing
contract MockSystemConfig {
address internal _gasPayingToken;

function setGasPayingToken(address token) external {
_gasPayingToken = token;
}

function gasPayingToken() external view returns (address, uint8) {
if (_gasPayingToken == address(0)) {
return (Constants.ETHER, 18);
}
return (_gasPayingToken, 18);
}

function resourceConfig() external pure returns (IResourceMetering.ResourceConfig memory) {
return IResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: 1 gwei,
systemTxMaxGas: 1_000_000,
maximumBaseFee: type(uint128).max
});
}
}

contract PortalTest is Test {
PortalWithChainExit internal portalImpl;
PortalWithChainExit internal portal;
ProxyAdmin internal admin;
Proxy internal proxy;
MockSuperchainConfig internal superchainConfig;
MockSystemConfig internal systemConfig;
MockERC20 internal token;

address internal recipient = makeAddr("recipient");
address internal nonOwner = makeAddr("nonOwner");
address internal chainOwner = makeAddr("chainOwner");

/// @notice EIP-1967 admin slot
bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

/// @notice Emitted when an emergency withdrawal is executed.
event ChainOwnerExitWithdrawal(address indexed recipient, address indexed token, uint256 amount);

function setUp() public {
// Deploy mocks
superchainConfig = new MockSuperchainConfig();
systemConfig = new MockSystemConfig();
token = new MockERC20();

// Deploy Portal implementation
portalImpl = new PortalWithChainExit();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test expect contract to be named PortalWithChainExit


// Deploy proxy with admin
admin = new ProxyAdmin(address(this));
proxy = new Proxy(address(admin));

// Upgrade proxy to Portal implementation, setting chain owner in the same call
bytes memory _data = abi.encodeCall(PortalWithChainExit.setChainOwner, chainOwner);
admin.upgradeAndCall(payable(address(proxy)), address(portalImpl), _data);

// Get Portal interface on proxy
portal = PortalWithChainExit(payable(address(proxy)));

// Initialize portal
portal.initialize({
_l2Oracle: OutputOracle(address(0)),
_systemConfig: ISystemConfig(address(systemConfig)),
_superchainConfig: ISuperchainConfig(address(superchainConfig))
});
}

function test_chainOwnerExitPortalNetworkToken_ETH_success() public {
// Fund the portal with ETH
uint256 amount = 10 ether;
vm.deal(address(portal), amount);

uint256 recipientBalanceBefore = recipient.balance;

// Admin withdraws (admin is ProxyAdmin, which is owned by address(this))
// But the actual proxy admin is the ProxyAdmin contract, so we need to call from it
// Actually, the admin slot stores the ProxyAdmin address, so we need to prank as ProxyAdmin
vm.prank(address(chainOwner));
portal.chainOwnerExitPortalNetworkToken(recipient);

assertEq(address(portal).balance, 0);
assertEq(recipient.balance, recipientBalanceBefore + amount);
}

function test_chainOwnerExitPortal_ERC20_success() public {
// Fund the portal with ERC20 tokens
uint256 amount = 1000 ether;
token.mint(address(portal), amount);

uint256 recipientBalanceBefore = token.balanceOf(recipient);

// Admin withdraws specific ERC20 asset
vm.prank(address(chainOwner));
portal.chainOwnerExitPortal(address(token), recipient);

assertEq(token.balanceOf(address(portal)), 0);
assertEq(token.balanceOf(recipient), recipientBalanceBefore + amount);
}

function test_chainOwnerExitPortalNetworkToken_onlyOwner_reverts() public {
// Fund the portal
vm.deal(address(portal), 10 ether);

// Non-owner tries to withdraw
vm.prank(nonOwner);
vm.expectRevert("Portal: caller is not chain owner");
portal.chainOwnerExitPortalNetworkToken(recipient);
}

function test_chainOwnerExitPortalNetworkToken_zeroRecipient_reverts() public {
// Fund the portal
vm.deal(address(portal), 10 ether);

// Admin tries to withdraw to zero address
vm.prank(address(chainOwner));
vm.expectRevert("Portal: zero recipient");
portal.chainOwnerExitPortalNetworkToken(address(0));
}

function test_chainOwnerExitPortalNetworkToken_worksWhenPaused() public {
// Fund the portal
uint256 amount = 10 ether;
vm.deal(address(portal), amount);

// Pause the superchain config
superchainConfig.setPaused(true);
assertTrue(portal.paused());

uint256 recipientBalanceBefore = recipient.balance;

// Chain owner can still withdraw even when paused
vm.prank(address(chainOwner));
portal.chainOwnerExitPortalNetworkToken(recipient);

assertEq(address(portal).balance, 0);
assertEq(recipient.balance, recipientBalanceBefore + amount);
}

function test_chainOwnerExitPortalNetworkToken_emitsEvent() public {
// Fund the portal
uint256 amount = 10 ether;
vm.deal(address(portal), amount);

// Expect the ChainOwnerExitWithdrawal event
vm.expectEmit(true, true, false, true);
emit ChainOwnerExitWithdrawal(recipient, Constants.ETHER, amount);

vm.prank(address(chainOwner));
portal.chainOwnerExitPortalNetworkToken(recipient);
}

function test_chainOwnerExitPortal_emitsEvent_ERC20() public {
// Fund the portal
uint256 amount = 1000 ether;
token.mint(address(portal), amount);

// Expect the ChainOwnerExitWithdrawal event
vm.expectEmit(true, true, false, true);
emit ChainOwnerExitWithdrawal(recipient, address(token), amount);

vm.prank(address(chainOwner));
portal.chainOwnerExitPortal(address(token), recipient);
}

function test_chainOwnerExitPortalNetworkToken_zeroBalance_ETH() public {
// Portal has no ETH
assertEq(address(portal).balance, 0);

uint256 recipientBalanceBefore = recipient.balance;

// Admin withdraws (should succeed with 0 amount)
vm.expectEmit(true, true, false, true);
emit ChainOwnerExitWithdrawal(recipient, Constants.ETHER, 0);

vm.prank(address(chainOwner));
portal.chainOwnerExitPortalNetworkToken(recipient);

assertEq(recipient.balance, recipientBalanceBefore);
}

function test_chainOwnerExitPortal_zeroBalance_ERC20() public {
// Portal has no tokens
assertEq(token.balanceOf(address(portal)), 0);

uint256 recipientBalanceBefore = token.balanceOf(recipient);

// Admin withdraws (should succeed with 0 amount)
vm.expectEmit(true, true, false, true);
emit ChainOwnerExitWithdrawal(recipient, address(token), 0);

vm.prank(address(chainOwner));
portal.chainOwnerExitPortal(address(token), recipient);

assertEq(token.balanceOf(recipient), recipientBalanceBefore);
}

// function test_version() public view {
// assertEq(portal.version(), "1.1.0");
// }
}
Loading