Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow rollup update #83

Merged
merged 17 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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
5 changes: 5 additions & 0 deletions src/bridge/AbsBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ abstract contract AbsBridge is Initializable, DelegateCallAware, IBridge {
_;
}

/// @notice Allows the rollup owner to set another rollup address
function updateRollupAddress(IOwnable _rollup) external onlyRollupOrOwner {
rollup = _rollup;
}

/// @dev returns the address of current active Outbox, or zero if no outbox is active
function activeOutbox() public view returns (address) {
address outbox = _activeOutbox;
Expand Down
7 changes: 7 additions & 0 deletions src/bridge/AbsOutbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ abstract contract AbsOutbox is DelegateCallAware, IOutbox {
rollup = address(_bridge.rollup());
}

/// @notice Allows the rollup owner to sync the rollup address
function updateRollupAddress() external {
if (msg.sender != IOwnable(rollup).owner())
revert NotOwner(msg.sender, IOwnable(rollup).owner());
rollup = address(bridge.rollup());
}

function updateSendRoot(bytes32 root, bytes32 l2BlockHash) external {
if (msg.sender != rollup) revert NotRollup(msg.sender, rollup);
roots[root] = l2BlockHash;
Expand Down
2 changes: 2 additions & 0 deletions src/bridge/IBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,6 @@ interface IBridge {
function setDelayedInbox(address inbox, bool enabled) external;

function setOutbox(address inbox, bool enabled) external;

function updateRollupAddress(IOwnable _rollup) external;
}
2 changes: 2 additions & 0 deletions src/bridge/IOutbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ interface IOutbox {

function updateSendRoot(bytes32 sendRoot, bytes32 l2BlockHash) external;

function updateRollupAddress() external;

/// @notice When l2ToL1Sender returns a nonzero address, the message was originated by an L2 account
/// When the return value is zero, that means this is a system message
/// @dev the l2ToL1Sender behaves as the tx.origin, the msg.sender should be validated to protect against reentrancies
Expand Down
2 changes: 2 additions & 0 deletions src/bridge/ISequencerInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,6 @@ interface ISequencerInbox is IDelayedMessageProvider {
// ---------- initializer ----------

function initialize(IBridge bridge_, MaxTimeVariation calldata maxTimeVariation_) external;

function updateRollupAddress() external;
}
7 changes: 7 additions & 0 deletions src/bridge/SequencerInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ contract SequencerInbox is DelegateCallAware, GasRefundEnabled, ISequencerInbox
maxTimeVariation = maxTimeVariation_;
}

/// @notice Allows the rollup owner to sync the rollup address
function updateRollupAddress() external {
if (msg.sender != IOwnable(rollup).owner())
revert NotOwner(msg.sender, IOwnable(rollup).owner());
rollup = bridge.rollup();
}

function getTimeBounds() internal view virtual returns (TimeBounds memory) {
TimeBounds memory bounds;
if (block.timestamp > maxTimeVariation.delaySeconds) {
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/Error.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pragma solidity ^0.8.4;
/// @dev Init was already called
error AlreadyInit();

/// Init was called with param set to zero that must be nonzero
/// @dev Init was called with param set to zero that must be nonzero
error HadZeroInit();

/// @dev Thrown when non owner tries to access an only-owner function
Expand Down
4 changes: 4 additions & 0 deletions src/mocks/BridgeStub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ contract BridgeStub is IBridge, IEthBridge {
revert("NOT_IMPLEMENTED");
}

function updateRollupAddress(IOwnable) external pure {
revert("NOT_IMPLEMENTED");
}

function enqueueDelayedMessage(
uint8 kind,
address sender,
Expand Down
7 changes: 7 additions & 0 deletions src/rollup/AbsRollupEventInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ abstract contract AbsRollupEventInbox is
rollup = address(_bridge.rollup());
}

/// @notice Allows the rollup owner to sync the rollup address
function updateRollupAddress() external {
if (msg.sender != IOwnable(rollup).owner())
revert NotOwner(msg.sender, IOwnable(rollup).owner());
rollup = address(bridge.rollup());
}

function rollupInitialized(uint256 chainId, string calldata chainConfig)
external
override
Expand Down
2 changes: 2 additions & 0 deletions src/rollup/IRollupEventInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ interface IRollupEventInbox {

function rollup() external view returns (address);

function updateRollupAddress() external;

function rollupInitialized(uint256 chainId, string calldata chainConfig) external;
}
30 changes: 26 additions & 4 deletions src/rollup/RollupUserLogic.sol
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,24 @@ abstract contract AbsRollupUserLogic is
stakeOnNode(msg.sender, latestNodeCreated());
}

modifier whenNotPausedOrDeprecated() {
gzeoneth marked this conversation as resolved.
Show resolved Hide resolved
require(!paused() || address(bridge.rollup()) != address(this), "PAUSED_AND_ACTIVE");
_;
}

/**
* @notice Refund a staker that is currently staked on or before the latest confirmed node
* @dev Since a staker is initially placed in the latest confirmed node, if they don't move it
* a griefer can remove their stake. It is recomended to batch together the txs to place a stake
* and move it to the desired node.
* @param stakerAddress Address of the staker whose stake is refunded
*/
function returnOldDeposit(address stakerAddress) external override onlyValidator whenNotPaused {
function returnOldDeposit(address stakerAddress)
external
override
onlyValidator
whenNotPausedOrDeprecated
{
require(latestStakedNode(stakerAddress) <= latestConfirmed(), "TOO_RECENT");
requireUnchallengedStaker(stakerAddress);
withdrawStaker(stakerAddress);
Expand All @@ -258,7 +268,7 @@ abstract contract AbsRollupUserLogic is
* @notice Reduce the amount staked for the sender (difference between initial amount staked and target is creditted back to the sender).
* @param target Target amount of stake for the staker. If this is below the current minimum, it will be set to minimum instead
*/
function reduceDeposit(uint256 target) external onlyValidator whenNotPaused {
function reduceDeposit(uint256 target) external onlyValidator whenNotPausedOrDeprecated {
requireUnchallengedStaker(msg.sender);
uint256 currentRequired = currentRequiredStake();
if (target < currentRequired) {
Expand Down Expand Up @@ -659,7 +669,13 @@ contract RollupUserLogic is AbsRollupUserLogic, IRollupUser {
/**
* @notice Withdraw uncommitted funds owned by sender from the rollup chain
*/
function withdrawStakerFunds() external override onlyValidator whenNotPaused returns (uint256) {
function withdrawStakerFunds()
external
override
onlyValidator
whenNotPausedOrDeprecated
returns (uint256)
{
uint256 amount = withdrawFunds(msg.sender);
// This is safe because it occurs after all checks and effects
// solhint-disable-next-line avoid-low-level-calls
Expand Down Expand Up @@ -731,7 +747,13 @@ contract ERC20RollupUserLogic is AbsRollupUserLogic, IRollupUserERC20 {
/**
* @notice Withdraw uncommitted funds owned by sender from the rollup chain
*/
function withdrawStakerFunds() external override onlyValidator whenNotPaused returns (uint256) {
function withdrawStakerFunds()
external
override
onlyValidator
whenNotPausedOrDeprecated
returns (uint256)
{
uint256 amount = withdrawFunds(msg.sender);
// This is safe because it occurs after all checks and effects
require(IERC20Upgradeable(stakeToken).transfer(msg.sender, amount), "TRANSFER_FAILED");
Expand Down
4 changes: 4 additions & 0 deletions src/test-helpers/BridgeTester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ contract BridgeTester is Initializable, DelegateCallAware, IBridge, IEthBridge {
rollup = rollup_;
}

function updateRollupAddress(IOwnable _rollup) external {
rollup = _rollup;
}

function activeOutbox() public view returns (address) {
if (_activeOutbox == EMPTY_ACTIVEOUTBOX) return address(uint160(0));
return _activeOutbox;
Expand Down
4 changes: 4 additions & 0 deletions src/test-helpers/OutboxWithoutOptTester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ contract OutboxWithoutOptTester is DelegateCallAware, IOutbox {
emit SendRootUpdated(root, l2BlockHash);
}

function updateRollupAddress() external onlyDelegated onlyProxyOwner {
rollup = address(bridge.rollup());
}

/// @notice When l2ToL1Sender returns a nonzero address, the message was originated by an L2 account
/// When the return value is zero, that means this is a system message
/// @dev the l2ToL1Sender behaves as the tx.origin, the msg.sender should be validated to protect against reentrancies
Expand Down
53 changes: 52 additions & 1 deletion test/contract/arbRollup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
RollupUserLogic__factory,
SequencerInbox,
SequencerInbox__factory,
Bridge,
} from '../../build/types'
import {
abi as UpgradeExecutorABI,
Expand Down Expand Up @@ -86,13 +87,15 @@
let rollup: RollupContract
let rollupUser: RollupUserLogic
let rollupAdmin: RollupAdminLogic
let bridge: Bridge
let accounts: Signer[]
let validators: Signer[]
let sequencerInbox: SequencerInbox
let admin: Signer
let sequencer: Signer
let challengeManager: ChallengeManager
let upgradeExecutor: string
let adminproxy: string

Check warning on line 98 in test/contract/arbRollup.spec.ts

View workflow job for this annotation

GitHub Actions / Contract tests

'adminproxy' is assigned a value but never used. Allowed unused vars must match /^_/u

async function getDefaultConfig(
_confirmPeriodBlocks = confirmationPeriodBlocks
Expand Down Expand Up @@ -275,6 +278,7 @@
await val1.getAddress(),
await val2.getAddress(),
await val3.getAddress(),
await val4.getAddress(),
],
maxDataSize: 117964,
nativeToken: ethers.constants.AddressZero,
Expand All @@ -298,6 +302,7 @@
const rollupUser = rollupUserLogicFac
.attach(rollupCreatedEvent.rollupAddress)
.connect(user)
const bridge = ethBridgeFac.attach(rollupCreatedEvent.bridge).connect(user)

sequencerInbox = (
(await ethers.getContractFactory(
Expand Down Expand Up @@ -328,8 +333,9 @@
sequencerInbox: rollupCreatedEvent.sequencerInbox,
delayedBridge: rollupCreatedEvent.bridge,
delayedInbox: rollupCreatedEvent.inboxAddress,
bridge: rollupCreatedEvent.bridge,
bridge,
upgradeExecutorAddress: rollupCreatedEvent.upgradeExecutor,
adminproxy: rollupCreatedEvent.adminProxy,
}
}

Expand Down Expand Up @@ -509,15 +515,19 @@
const {
rollupAdmin: rollupAdminContract,
rollupUser: rollupUserContract,
bridge: bridgeContract,
admin: adminI,
validators: validatorsI,
upgradeExecutorAddress,
adminproxy: adminproxyAddress,
} = await setup()
rollupAdmin = rollupAdminContract
rollupUser = rollupUserContract
bridge = bridgeContract
admin = adminI
validators = validatorsI
upgradeExecutor = upgradeExecutorAddress
adminproxy = adminproxyAddress
rollup = new RollupContract(rollupUser.connect(validators[0]))
})

Expand Down Expand Up @@ -580,6 +590,9 @@
await rollupUser
.connect(validators[2])
.newStakeOnExistingNode(1, prevNode.nodeHash, { value: stake })
await rollupUser
.connect(validators[3])
.newStakeOnExistingNode(1, prevNode.nodeHash, { value: stake })
})

it('should move stake to a new node', async function () {
Expand Down Expand Up @@ -789,6 +802,44 @@
await rollup.confirmNextNode(challengerNode)
})

it('allow force refund staker with pending node', async function () {
await rollupAdmin.connect(await impersonateAccount(upgradeExecutor)).pause()
Copy link
Contributor

Choose a reason for hiding this comment

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

missing .wait() - and on some other transactions below

Copy link
Member Author

Choose a reason for hiding this comment

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

that should not be necessary right? otherwise the test should be failing

Copy link
Contributor

Choose a reason for hiding this comment

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

could just be getting lucky with timing. You might get intermittent failures with this

await (
await rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.forceRefundStaker([await validators[3].getAddress()])
).wait()

await expect(
rollup.rollup.connect(validators[3]).withdrawStakerFunds()
).to.be.revertedWith('PAUSED_AND_ACTIVE')
// staker can only withdraw if rollup address changed when paused
await bridge
.connect(await impersonateAccount(rollup.rollup.address))
.updateRollupAddress(ethers.constants.AddressZero)

await (
await rollup.rollup.connect(validators[3]).withdrawStakerFunds()
).wait()
await rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.resume()

// restore rollup address
await bridge
.connect(await impersonateAccount(ethers.constants.AddressZero))
.updateRollupAddress(rollupUser.address)

const postWithdrawablefunds = await rollup.rollup.withdrawableFunds(
await validators[3].getAddress()
)
expect(postWithdrawablefunds, 'withdrawable funds').to.equal(0)
const stake = await rollup.rollup.amountStaked(
await validators[3].getAddress()
)
expect(stake, 'amount staked').to.equal(0)
})

it('should add and remove stakes correctly', async function () {
/*
RollupUser functions that alter stake and their respective Core logic
Expand Down
Loading