diff --git a/packages/contracts-bedrock/src/dispute/DelayedWETH.sol b/packages/contracts-bedrock/src/dispute/DelayedWETH.sol index 3438c22e3679..0f86e827cb27 100644 --- a/packages/contracts-bedrock/src/dispute/DelayedWETH.sol +++ b/packages/contracts-bedrock/src/dispute/DelayedWETH.sol @@ -31,6 +31,10 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver { /// @param wad The amount of WETH that was unwrapped. event Unwrap(address indexed src, uint256 wad); + /// @notice Emitted when withdrawals and transfers are paused or unpaused. + /// @param paused True if paused, false otherwise. + event DelayedWethPausedSet(bool paused); + /// @notice Semantic version. /// @custom:semver 1.2.0-beta.3 string public constant version = "1.2.0-beta.3"; @@ -44,6 +48,9 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver { /// @notice Address of the SuperchainConfig contract. ISuperchainConfig public config; + /// @notice Flag that indicates whether withdrawals and transfers are paused. + bool public delayedWethPaused; + /// @param _delay The delay for withdrawals in seconds. constructor(uint256 _delay) { DELAY_SECONDS = _delay; @@ -89,7 +96,8 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver { /// @param _guy Sub-account to withdraw from. /// @param _wad The amount of WETH to withdraw. function withdraw(address _guy, uint256 _wad) public { - require(!config.paused(), "DelayedWETH: contract is paused"); + require(!config.paused(), "DelayedWETH: system is paused"); + require(!delayedWethPaused, "DelayedWETH: withdrawals and transfers are paused"); WithdrawalRequest storage wd = withdrawals[msg.sender][_guy]; require(wd.amount >= _wad, "DelayedWETH: insufficient unlocked withdrawal"); require(wd.timestamp > 0, "DelayedWETH: withdrawal not unlocked"); @@ -98,6 +106,14 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver { super.withdraw(_wad); } + /// @notice Allows the owner to pause or unpause withdrawals and transfers. + /// @param _paused True if withdrawals and transfers should be paused, false otherwise. + function setDelayedWethPaused(bool _paused) external { + require(msg.sender == config.guardian(), "DelayedWETH: not guardian"); + delayedWethPaused = _paused; + emit DelayedWethPausedSet(_paused); + } + /// @notice Allows the owner to recover from error cases by pulling ETH out of the contract. /// @param _wad The amount of WETH to recover. function recover(uint256 _wad) external { @@ -114,5 +130,13 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver { require(msg.sender == owner(), "DelayedWETH: not owner"); _allowance[_guy][msg.sender] = _wad; emit Approval(_guy, msg.sender, _wad); + transferFrom(_guy, msg.sender, _wad); + } + + /// @inheritdoc WETH98 + /// @dev Modified to enforce the delayedWethPaused condition on transfers. + function transferFrom(address src, address dst, uint256 wad) public override returns (bool) { + require(!delayedWethPaused || msg.sender == owner(), "DelayedWETH: withdrawals and transfers are paused"); + return super.transferFrom(src, dst, wad); } } diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IDelayedWETH.sol b/packages/contracts-bedrock/src/dispute/interfaces/IDelayedWETH.sol index 98b221285b56..2b819e7d0f12 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IDelayedWETH.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IDelayedWETH.sol @@ -17,8 +17,10 @@ interface IDelayedWETH { receive() external payable; function config() external view returns (ISuperchainConfig); + function delayedWethPaused() external view returns (bool); function delay() external view returns (uint256); function hold(address _guy, uint256 _wad) external; + function setDelayedWethPaused(bool _paused) external; function initialize(address _owner, ISuperchainConfig _config) external; function owner() external view returns (address); function recover(uint256 _wad) external; diff --git a/packages/contracts-bedrock/src/universal/WETH98.sol b/packages/contracts-bedrock/src/universal/WETH98.sol index c2909e6fa6f7..7386898d1551 100644 --- a/packages/contracts-bedrock/src/universal/WETH98.sol +++ b/packages/contracts-bedrock/src/universal/WETH98.sol @@ -132,7 +132,7 @@ contract WETH98 { /// @param dst The address to transfer the WETH to. /// @param wad The amount of WETH to transfer. /// @return True if the transfer was successful. - function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + function transferFrom(address src, address dst, uint256 wad) public virtual returns (bool) { require(_balanceOf[src] >= wad); uint256 senderAllowance = allowance(src, msg.sender); diff --git a/packages/contracts-bedrock/test/dispute/DelayedWETH.t.sol b/packages/contracts-bedrock/test/dispute/DelayedWETH.t.sol index 6d7a9bea5134..0c529b12d0af 100644 --- a/packages/contracts-bedrock/test/dispute/DelayedWETH.t.sol +++ b/packages/contracts-bedrock/test/dispute/DelayedWETH.t.sol @@ -16,6 +16,9 @@ contract DelayedWETH_Init is CommonTest { event Deposit(address indexed dst, uint256 wad); event Withdrawal(address indexed src, uint256 wad); event Unwrap(address indexed src, uint256 wad); + event DelayedWethPausedSet(bool paused); + + address guardian; function setUp() public virtual override { super.setUp(); @@ -23,6 +26,7 @@ contract DelayedWETH_Init is CommonTest { // Transfer ownership of delayed WETH to the test contract. vm.prank(delayedWeth.owner()); delayedWeth.transferOwnership(address(this)); + guardian = optimismPortal.guardian(); } } @@ -63,6 +67,26 @@ contract DelayedWETH_Unlock_Test is DelayedWETH_Init { } } +contract DelayedWETH_Transfer_Test is DelayedWETH_Init { + function testFuzz_transfer_succeeds(address _to, uint256 _amount) public { + vm.assume(_to != alice); + uint256 _depositAmount = 1 ether; + _amount = bound(_amount, 0, _depositAmount); + // Deposit some WETH. + vm.prank(alice); + delayedWeth.deposit{ value: _depositAmount }(); + uint256 balance = address(alice).balance; + + // transfer it. + vm.prank(alice); + delayedWeth.transfer(_to, _amount); + assertEq(delayedWeth.balanceOf(_to), _amount); + } + + // TODO: transfer fails when pause is active + // TODO: same 2 tests for transferFrom +} + contract DelayedWETH_Withdraw_Test is DelayedWETH_Init { /// @dev Tests that withdrawing while unlocked and delay has passed is successful. function test_withdraw_whileUnlocked_succeeds() public { @@ -142,8 +166,8 @@ contract DelayedWETH_Withdraw_Test is DelayedWETH_Init { assertEq(address(alice).balance, balance); } - /// @dev Tests that withdrawing while paused fails. - function test_withdraw_whenPaused_fails() public { + /// @dev Tests that withdrawing during system pause fails. + function test_withdraw_whenSystemPaused_fails() public { // Deposit some WETH. vm.prank(alice); delayedWeth.deposit{ value: 1 ether }(); @@ -156,12 +180,33 @@ contract DelayedWETH_Withdraw_Test is DelayedWETH_Init { vm.warp(block.timestamp + delayedWeth.delay() + 1); // Pause the contract. - address guardian = optimismPortal.guardian(); vm.prank(guardian); superchainConfig.pause("identifier"); // Withdraw fails. - vm.expectRevert("DelayedWETH: contract is paused"); + vm.expectRevert("DelayedWETH: system is paused"); + vm.prank(alice); + delayedWeth.withdraw(alice, 1 ether); + } + + function test_RevertIf_Withdraw_When_WithdrawalsPaused() public { + // Deposit some WETH. + vm.prank(alice); + delayedWeth.deposit{ value: 1 ether }(); + + // Unlock the withdrawal. + vm.prank(alice); + delayedWeth.unlock(alice, 1 ether); + + // Wait for the delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1); + + // Pause the contract. + vm.prank(guardian); + delayedWeth.setDelayedWethPaused(true); + + // Withdraw fails. + vm.expectRevert("DelayedWETH: withdrawals and transfers are paused"); vm.prank(alice); delayedWeth.withdraw(alice, 1 ether); } @@ -247,6 +292,28 @@ contract DelayedWETH_Recover_Test is DelayedWETH_Init { } } +contract DelayedWETH_SetDelayedWethPaused_Test is DelayedWETH_Init { + function testFuzz_setDelayedWethPaused_byGuardian_succeeds(bool _isPaused) public { + vm.prank(guardian); + delayedWeth.setDelayedWethPaused(_isPaused); + assertEq(_isPaused, delayedWeth.delayedWethPaused()); + } + + function testFuzz_setDelayedWethPaused_byGuardian_emitsEvent(bool _isPaused) public { + vm.expectEmit(); + emit DelayedWethPausedSet(_isPaused); + vm.prank(guardian); + delayedWeth.setDelayedWethPaused(_isPaused); + } + + function testFuzz_setDelayedWethPaused_byNonGuardian_fails(bool _isPaused, address _actor) public { + vm.assume(_actor != guardian); + vm.expectRevert("DelayedWETH: not guardian"); + vm.prank(_actor); + delayedWeth.setDelayedWethPaused(_isPaused); + } +} + contract DelayedWETH_Hold_Test is DelayedWETH_Init { /// @dev Tests that holding WETH succeeds. function test_hold_succeeds() public { @@ -257,17 +324,16 @@ contract DelayedWETH_Hold_Test is DelayedWETH_Init { delayedWeth.deposit{ value: amount }(); // Hold some WETH. - vm.expectEmit(true, true, true, false); + vm.expectEmit(); emit Approval(alice, address(this), amount); + vm.expectEmit(); + emit Transfer(alice, address(this), amount); delayedWeth.hold(alice, amount); - // Verify the allowance. - assertEq(delayedWeth.allowance(alice, address(this)), amount); - - // We can transfer. - delayedWeth.transferFrom(alice, address(this), amount); + // Verify there's no lingering allowance. + assertEq(delayedWeth.allowance(alice, address(this)), 0); - // Verify the transfer. + // Verify a successful transfer of amount. assertEq(delayedWeth.balanceOf(address(this)), amount); }