Skip to content

feat: upgrade bridge to allow for transfer of all token assets#103

Open
0xClouds wants to merge 9 commits intobase:mainfrom
0xClouds:portal_upgrade
Open

feat: upgrade bridge to allow for transfer of all token assets#103
0xClouds wants to merge 9 commits intobase:mainfrom
0xClouds:portal_upgrade

Conversation

@0xClouds
Copy link

@0xClouds 0xClouds commented Feb 4, 2026

Upgrade the optimism portal contract to support withdrawal of all locked funds.

amount = IERC20(token).balanceOf(address(this));
if (amount > 0) {
(address gasToken,) = gasPayingToken();
if (token == gasToken) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably a nit but seems like we should be doing this after the safe transfer?

Cant recall if safeTransfer reverts or returns a bool as well

Copy link
Author

Choose a reason for hiding this comment

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

if safe transfer reverts the entire transaction reverts

/// @return Whether or not the finalization period has elapsed.
function _isFinalizationPeriodElapsed(uint256 _timestamp) internal view returns (bool) {
return block.timestamp > _timestamp;
}

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

// 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(admin));
portal.emergencyWithdraw(recipient);

Choose a reason for hiding this comment

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

I don't think the test suit is testing anything at the moment. We reference the emergencyWithdraw() function here, but the function we want to test is called chainOwnerExitPortal()

if (token == Constants.ETHER) {
amount = address(this).balance;
if (amount > 0) {
(bool success,) = _recipient.call{value: amount}("");

Choose a reason for hiding this comment

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

nit: we give external call + execution control to recipient address here. However, its assumed the recipient is a trusted address and wouldn't try to do something malicious like reenter somewhere else in the code

/// @notice Reverts if caller is not the proxy admin.
modifier onlyAdmin() {
address admin;
bytes32 adminSlot = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

Choose a reason for hiding this comment

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

double checked, admin slot matches the one found in constants.sol https://github.com/ethereum-optimism/optimism/blob/3056348b21a07e584225310bbc27f1adce5ca2a9/packages/contracts-bedrock/src/libraries/Constants.sol#L29-L31

follows transparent proxy pattern, where admin was already set on proxy itself, looks good

/// @notice Allows owner to withdraw the gas paying token held by the portal.
/// Can be called regardless of pause state.
/// @param _recipient The address to receive the withdrawn funds.
function chainOwnerExitPortal(address _recipient) external onlyAdmin {

Choose a reason for hiding this comment

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

nit: function selectors clashing not a problem, but do we want to name this something more explicit?

maybe something like chainOwnerExitPortalGasToken()

/// Can be called regardless of pause state.
/// @param _asset The token address to withdraw, or address(0) for the gas paying token.
/// @param _recipient The address to receive the withdrawn funds.
function chainOwnerExitPortal(address _asset, address _recipient) public onlyAdmin {

Choose a reason for hiding this comment

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

nit: assumption is that once this is called, the chain is effectively "dead". Just need to make sure users who still have funds on the l3 is made aware and at that point, it'd be up to the recipient to properly distribute funds

Copy link
Author

Choose a reason for hiding this comment

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

Agreed, it is on the onus of the team to ensure their users are aware they are taking control and will distribute the funds properly

);
}

function setChainOwner(address _chainOwner) external onlyAdmin {
Copy link
Author

Choose a reason for hiding this comment

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

New chain owner setter.

Created a setter rather than setting on initialize for simplicity since the proxy will have already initialized the other fields.

Copy link
Author

Choose a reason for hiding this comment

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

Only Admin can call setChainOwner. The expected pattern is to call when using ProxyAdmin's upgradeAndCall method

uint256 internal _balance;

// Owner of the current chain
address public chainOwner;
Copy link
Author

Choose a reason for hiding this comment

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

New chainOwner variable.

The ProxyAdmin contract only have two methods upgrade and upgradeAndCall.
Since ResolvingProxy will not delegate any calls from the ProxyAdmin you have to reset the implementation contract each time if the chain owner had multiple assets it wished to exit.


/// @notice Emitted when a chain owner is set
/// @param chainOwner address of the chain owner
event ChainOwnerSet(address chainOwner);
Copy link
Author

Choose a reason for hiding this comment

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

New chain owner set event

/// @param recipient The address that received the funds.
/// @param token The token address (Constants.ETHER for native ETH).
/// @param amount The amount withdrawn.
event ChainOwnerExitWithdrawal(address indexed recipient, address indexed token, uint256 amount);
Copy link
Author

Choose a reason for hiding this comment

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

renamed chainOwnerExitWithdrawal event

_;
}

modifier onlyChainOwner() {
Copy link
Author

Choose a reason for hiding this comment

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

new onlyChainOwner modifier

Comment on lines +530 to +540
function chainOwnerExitPortalNetworkToken(address _recipient) external onlyChainOwner {
chainOwnerExitPortal(address(0), _recipient);
}

/// @notice Allows owner to withdraw all tokens held by the portal.
/// Can be called regardless of pause state.
/// @param _asset The token address to withdraw, or address(0) for the gas paying token.
/// @param _recipient The address to receive the withdrawn funds.
function chainOwnerExitPortal(address _asset, address _recipient) public onlyChainOwner {
require(_recipient != address(0), "Portal: zero recipient");

Copy link
Author

Choose a reason for hiding this comment

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

Both functions now role restricted via modifier to chain owner

uint256 internal _balance;

// Owner of the current chain
address public chainOwner;
Copy link
Author

Choose a reason for hiding this comment

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

New chainOwner Variable


/// @notice Emitted when a chain owner is set
/// @param chainOwner address of the chain owner
event ChainOwnerSet(address chainOwner);
Copy link
Author

Choose a reason for hiding this comment

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

New ChainOwnerEvent

);
}

function setChainOwner(address _chainOwner) external onlyAdmin {
Copy link
Author

Choose a reason for hiding this comment

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

new chainOwner Setter

Copy link
Author

Choose a reason for hiding this comment

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

modifier only by ProxyAdmin expectation is called during implementation upgrade via upgradeAndCall

Copy link
Author

Choose a reason for hiding this comment

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

change this to allow owner to update as well

Choose a reason for hiding this comment

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

Can we add a check here for address(0) ?

The odds of a mistake here are super low, but since it could lead to loss of funds I think it would be worth it to add here

Comment on lines +530 to +541
function chainOwnerExitPortalNetworkToken(address _recipient) external onlyChainOwner {
chainOwnerExitPortal(address(0), _recipient);
}

/// @notice Allows owner to withdraw all tokens held by the portal.
/// Can be called regardless of pause state.
/// @param _asset The token address to withdraw, or address(0) for the gas paying token.
/// @param _recipient The address to receive the withdrawn funds.
function chainOwnerExitPortal(address _asset, address _recipient) public onlyChainOwner {
require(_recipient != address(0), "Portal: zero recipient");

address token;
Copy link
Author

Choose a reason for hiding this comment

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

Modifier changed to onlyChainOwner

_;
}

modifier onlyChainOwner() {
Copy link
Author

Choose a reason for hiding this comment

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

new chainOwnerModifier

token = new MockERC20();

// Deploy Portal implementation
portalImpl = new PortalWithChainExit();
Copy link
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants