Commit e435facb authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat: introduce delayed WETH (#9666)

Introduces a DelayedWETH contract so that bonds have a backup
mechanism in place.
parent b36eb551
......@@ -26,6 +26,7 @@
"WETH9",
"DeployerWhitelist",
"L1BlockNumber",
"DelayedWETH",
"DisputeGameFactory",
"FaultDisputeGame",
"AlphabetVM",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -213,6 +213,8 @@ type DeployConfig struct {
FaultGameGenesisOutputRoot common.Hash `json:"faultGameGenesisOutputRoot"`
// FaultGameSplitDepth is the depth at which the fault dispute game splits from output roots to execution trace claims.
FaultGameSplitDepth uint64 `json:"faultGameSplitDepth"`
// FaultGameWithdrawalDelay is the number of seconds that users must wait before withdrawing ETH from a fault game.
FaultGameWithdrawalDelay uint64 `json:"faultGameWithdrawalDelay"`
// PreimageOracleMinProposalSize is the minimum number of bytes that a large preimage oracle proposal can be.
PreimageOracleMinProposalSize uint64 `json:"preimageOracleMinProposalSize"`
// PreimageOracleChallengePeriod is the number of seconds that challengers have to challenge a large preimage proposal.
......
......@@ -71,6 +71,7 @@
"faultGameGenesisBlock": 0,
"faultGameGenesisOutputRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
"faultGameSplitDepth": 0,
"faultGameWithdrawalDelay": 604800,
"preimageOracleMinProposalSize": 1800000,
"preimageOracleChallengePeriod": 86400,
"preimageOracleCancunActivationTimestamp": 0,
......
......@@ -178,8 +178,8 @@ func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, l2Node string
h.require.NoError(err, "create fault dispute game")
rcpt, err := wait.ForReceiptOK(ctx, h.client, tx.Hash())
h.require.NoError(err, "wait for create fault dispute game receipt to be OK")
h.require.Len(rcpt.Logs, 1, "should have emitted a single DisputeGameCreated event")
createdEvent, err := h.factory.ParseDisputeGameCreated(*rcpt.Logs[0])
h.require.Len(rcpt.Logs, 2, "should have emitted a single DisputeGameCreated event")
createdEvent, err := h.factory.ParseDisputeGameCreated(*rcpt.Logs[1])
h.require.NoError(err)
game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client)
h.require.NoError(err)
......@@ -244,8 +244,8 @@ func (h *FactoryHelper) StartOutputAlphabetGame(ctx context.Context, l2Node stri
h.require.NoError(err, "create output bisection game")
rcpt, err := wait.ForReceiptOK(ctx, h.client, tx.Hash())
h.require.NoError(err, "wait for create output bisection game receipt to be OK")
h.require.Len(rcpt.Logs, 1, "should have emitted a single DisputeGameCreated event")
createdEvent, err := h.factory.ParseDisputeGameCreated(*rcpt.Logs[0])
h.require.Len(rcpt.Logs, 2, "should have emitted a single DisputeGameCreated event")
createdEvent, err := h.factory.ParseDisputeGameCreated(*rcpt.Logs[1])
h.require.NoError(err)
game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client)
h.require.NoError(err)
......
......@@ -5,7 +5,7 @@ GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4061
GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 450308)
GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3496057)
GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 59803)
GasBenchMark_L2OutputOracle:test_proposeL2Output_benchmark() (gas: 92951)
GasBenchMark_L2OutputOracle:test_proposeL2Output_benchmark() (gas: 92930)
GasBenchMark_OptimismPortal:test_depositTransaction_benchmark() (gas: 68360)
GasBenchMark_OptimismPortal:test_depositTransaction_benchmark_1() (gas: 69013)
GasBenchMark_OptimismPortal:test_proveWithdrawalTransaction_benchmark() (gas: 155553)
\ No newline at end of file
GasBenchMark_OptimismPortal:test_proveWithdrawalTransaction_benchmark() (gas: 155556)
\ No newline at end of file
......@@ -55,6 +55,7 @@
"faultGameGenesisBlock": 0,
"faultGameGenesisOutputRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
"faultGameSplitDepth": 14,
"faultGameWithdrawalDelay": 604800,
"preimageOracleMinProposalSize": 10000,
"preimageOracleChallengePeriod": 120,
"preimageOracleCancunActivationTimestamp": 0,
......
......@@ -49,6 +49,7 @@
"faultGameGenesisBlock": 4061224,
"faultGameGenesisOutputRoot": "0xd08055c58b2c5149565c636b44fad2c25b5ccddef1385a2cb721529d7480b242",
"faultGameSplitDepth": 32,
"faultGameWithdrawalDelay": 604800,
"preimageOracleMinProposalSize": 1800000,
"preimageOracleChallengePeriod": 86400,
"preimageOracleCancunActivationTimestamp": 1705473120,
......
......@@ -49,6 +49,7 @@
"faultGameGenesisBlock": 0,
"faultGameGenesisOutputRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
"faultGameSplitDepth": 4,
"faultGameWithdrawalDelay": 604800,
"preimageOracleMinProposalSize": 10000,
"preimageOracleChallengePeriod": 120,
"preimageOracleCancunActivationTimestamp": 0,
......
......@@ -73,6 +73,7 @@
"faultGameMaxDuration": 86400,
"faultGameGenesisBlock": 0,
"faultGameSplitDepth": 0,
"faultGameWithdrawalDelay": 604800,
"fundDevAccounts": false,
"requiredProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000",
"recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000"
......
......@@ -49,6 +49,7 @@
"faultGameGenesisBlock": 4061224,
"faultGameGenesisOutputRoot": "0xd08055c58b2c5149565c636b44fad2c25b5ccddef1385a2cb721529d7480b242",
"faultGameSplitDepth": 32,
"faultGameWithdrawalDelay": 604800,
"preimageOracleMinProposalSize": 1800000,
"preimageOracleChallengePeriod": 86400,
"preimageOracleCancunActivationTimestamp": 1706655072,
......
# `FaultDisputeGame` Invariants
## FaultDisputeGame always returns all ETH on total resolution
**Test:** [`FaultDisputeGame.t.sol#L38`](../test/invariants/FaultDisputeGame.t.sol#L38)
**Test:** [`FaultDisputeGame.t.sol#L39`](../test/invariants/FaultDisputeGame.t.sol#L39)
The FaultDisputeGame contract should always return all ETH in the contract to the correct recipients upon resolution of all outstanding claims. There may never be any ETH left in the contract after a full resolution.
\ No newline at end of file
# `OptimismPortal2` Invariants
## Deposits of any value should always succeed unless `_to` = `address(0)` or `_isCreation` = `true`.
**Test:** [`OptimismPortal2.t.sol#L158`](../test/invariants/OptimismPortal2.t.sol#L158)
**Test:** [`OptimismPortal2.t.sol#L160`](../test/invariants/OptimismPortal2.t.sol#L160)
All deposits, barring creation transactions and transactions sent to `address(0)`, should always succeed.
## `finalizeWithdrawalTransaction` should revert if the proof maturity period has not elapsed.
**Test:** [`OptimismPortal2.t.sol#L180`](../test/invariants/OptimismPortal2.t.sol#L180)
**Test:** [`OptimismPortal2.t.sol#L182`](../test/invariants/OptimismPortal2.t.sol#L182)
A withdrawal that has been proven should not be able to be finalized until after the proof maturity period has elapsed.
## `finalizeWithdrawalTransaction` should revert if the withdrawal has already been finalized.
**Test:** [`OptimismPortal2.t.sol#L209`](../test/invariants/OptimismPortal2.t.sol#L209)
**Test:** [`OptimismPortal2.t.sol#L211`](../test/invariants/OptimismPortal2.t.sol#L211)
Ensures that there is no chain of calls that can be made that allows a withdrawal to be finalized twice.
## A withdrawal should **always** be able to be finalized `PROOF_MATURITY_DELAY_SECONDS` after it was successfully proven, if the game has resolved and passed the air-gap.
**Test:** [`OptimismPortal2.t.sol#L237`](../test/invariants/OptimismPortal2.t.sol#L237)
**Test:** [`OptimismPortal2.t.sol#L239`](../test/invariants/OptimismPortal2.t.sol#L239)
This invariant asserts that there is no chain of calls that can be made that will prevent a withdrawal from being finalized exactly `PROOF_MATURITY_DELAY_SECONDS` after it was successfully proven and the game has resolved and passed the air-gap.
\ No newline at end of file
......@@ -10,6 +10,7 @@ import { Constants } from "src/libraries/Constants.sol";
import { L1StandardBridge } from "src/L1/L1StandardBridge.sol";
import { L2OutputOracle } from "src/L1/L2OutputOracle.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { DelayedWETH } from "src/dispute/weth/DelayedWETH.sol";
import { ProtocolVersion, ProtocolVersions } from "src/L1/ProtocolVersions.sol";
import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
import { OptimismPortal } from "src/L1/OptimismPortal.sol";
......@@ -178,6 +179,32 @@ library ChainAssertions {
require(factory.owner() == _expectedOwner);
}
/// @notice Asserts that the DelayedWETH is setup correctly
function checkDelayedWETH(
Types.ContractSet memory _contracts,
DeployConfig _cfg,
bool _isProxy,
address _expectedOwner
)
internal
view
{
console.log("Running chain assertions on the DelayedWETH");
DelayedWETH weth = DelayedWETH(payable(_contracts.DelayedWETH));
// Check that the contract is initialized
assertSlotValueIsOne({ _contractAddress: address(weth), _slot: 0, _offset: 0 });
if (_isProxy) {
require(weth.owner() == _expectedOwner);
require(weth.delay() == _cfg.faultGameWithdrawalDelay());
require(weth.config() == SuperchainConfig(_contracts.SuperchainConfig));
} else {
require(weth.owner() == _expectedOwner);
require(weth.delay() == _cfg.faultGameWithdrawalDelay());
}
}
/// @notice Asserts that the L2OutputOracle is setup correctly
function checkL2OutputOracle(
Types.ContractSet memory _contracts,
......
......@@ -9,7 +9,7 @@ import { Chains } from "scripts/Chains.sol";
// Global constant for the `useFaultProofs` slot in the DeployConfig contract, which can be overridden in the testing
// environment.
bytes32 constant USE_FAULT_PROOFS_SLOT = bytes32(uint256(63));
bytes32 constant USE_FAULT_PROOFS_SLOT = bytes32(uint256(64));
/// @title DeployConfig
/// @notice Represents the configuration required to deploy the system. It is expected
......@@ -60,6 +60,7 @@ contract DeployConfig is Script {
uint256 public faultGameMaxDepth;
uint256 public faultGameSplitDepth;
uint256 public faultGameMaxDuration;
uint256 public faultGameWithdrawalDelay;
uint256 public preimageOracleMinProposalSize;
uint256 public preimageOracleChallengePeriod;
uint256 public preimageOracleCancunActivationTimestamp;
......@@ -136,6 +137,7 @@ contract DeployConfig is Script {
faultGameMaxDuration = stdJson.readUint(_json, "$.faultGameMaxDuration");
faultGameGenesisBlock = stdJson.readUint(_json, "$.faultGameGenesisBlock");
faultGameGenesisOutputRoot = stdJson.readBytes32(_json, "$.faultGameGenesisOutputRoot");
faultGameWithdrawalDelay = stdJson.readUint(_json, "$.faultGameWithdrawalDelay");
preimageOracleMinProposalSize = stdJson.readUint(_json, "$.preimageOracleMinProposalSize");
preimageOracleChallengePeriod = stdJson.readUint(_json, "$.preimageOracleChallengePeriod");
......
......@@ -49,7 +49,7 @@ contract FaultDisputeGameViz is Script, FaultDisputeGame_Init {
* @dev Entry point
*/
function remote(address _addr) public {
gameProxy = FaultDisputeGame(_addr);
gameProxy = FaultDisputeGame(payable(_addr));
buildGraph();
console.log("Saved graph to `./dispute_game.svg");
}
......
......@@ -8,6 +8,7 @@ library Types {
address L1StandardBridge;
address L2OutputOracle;
address DisputeGameFactory;
address DelayedWETH;
address OptimismMintableERC20Factory;
address OptimismPortal;
address OptimismPortal2;
......
......@@ -18,19 +18,22 @@ contract FPACOPS is Deploy, StdAssertions {
prankDeployment("ProxyAdmin", msg.sender);
prankDeployment("SystemOwnerSafe", msg.sender);
// Deploy the DisputeGameFactoryProxy.
// Deploy the proxies.
deployERC1967Proxy("DisputeGameFactoryProxy");
deployERC1967Proxy("DelayedWETHProxy");
// Deploy implementations.
deployDisputeGameFactory();
deployDelayedWETH();
deployPreimageOracle();
deployMips();
// Deploy the new `OptimismPortal` implementation.
deployOptimismPortal2();
// Initialize the DisputeGameFactoryProxy.
// Initialize the proxies.
initializeDisputeGameFactoryProxy();
initializeDelayedWETHProxy();
// Deploy the Cannon Fault game implementation and set it as game ID = 0.
setCannonFaultGameImplementation({ _allowUpgrade: false });
......@@ -40,6 +43,7 @@ contract FPACOPS is Deploy, StdAssertions {
// Transfer ownership of the DisputeGameFactory to the SystemOwnerSafe, and transfer the administrative rights
// of the DisputeGameFactoryProxy to the ProxyAdmin.
transferDGFOwnershipFinal({ _proxyAdmin: _proxyAdmin, _systemOwnerSafe: _systemOwnerSafe });
transferWethOwnershipFinal({ _proxyAdmin: _proxyAdmin, _systemOwnerSafe: _systemOwnerSafe });
// Run post-deployment assertions.
postDeployAssertions({ _proxyAdmin: _proxyAdmin, _systemOwnerSafe: _systemOwnerSafe });
......@@ -62,6 +66,17 @@ contract FPACOPS is Deploy, StdAssertions {
);
}
function initializeDelayedWETHProxy() internal broadcast {
console.log("Initializing DelayedWETHProxy with DelayedWETH.");
address wethProxy = mustGetAddress("DelayedWETHProxy");
address systemConfigProxy = mustGetAddress("SystemConfigProxy");
Proxy(payable(wethProxy)).upgradeToAndCall(
mustGetAddress("DelayedWETH"),
abi.encodeWithSignature("initialize(address,address)", msg.sender, systemConfigProxy)
);
}
/// @notice Transfers admin rights of the `DisputeGameFactoryProxy` to the `ProxyAdmin` and sets the
/// `DisputeGameFactory` owner to the `SystemOwnerSafe`.
function transferDGFOwnershipFinal(address _proxyAdmin, address _systemOwnerSafe) internal broadcast {
......@@ -75,6 +90,19 @@ contract FPACOPS is Deploy, StdAssertions {
prox.changeAdmin(_proxyAdmin);
}
/// @notice Transfers admin rights of the `DelayedWETHProxy` to the `ProxyAdmin` and sets the
/// `DelayedWETH` owner to the `SystemOwnerSafe`.
function transferWethOwnershipFinal(address _proxyAdmin, address _systemOwnerSafe) internal broadcast {
DelayedWETH weth = DelayedWETH(mustGetAddress("DelayedWETHProxy"));
// Transfer the ownership of the DelayedWETH to the SystemOwnerSafe.
weth.transferOwnership(_systemOwnerSafe);
// Transfer the admin rights of the DelayedWETHProxy to the ProxyAdmin.
Proxy prox = Proxy(payable(address(weth)));
prox.changeAdmin(_proxyAdmin);
}
/// @notice Checks that the deployed system is configured correctly.
function postDeployAssertions(address _proxyAdmin, address _systemOwnerSafe) internal {
Types.ContractSet memory contracts = _proxiesUnstrict();
......@@ -88,6 +116,9 @@ contract FPACOPS is Deploy, StdAssertions {
DisputeGameFactory dgfProxy = DisputeGameFactory(dgfProxyAddr);
assertEq(address(uint160(uint256(vm.load(dgfProxyAddr, Constants.PROXY_OWNER_ADDRESS)))), _proxyAdmin);
ChainAssertions.checkDisputeGameFactory(contracts, _systemOwnerSafe);
address wethProxyAddr = mustGetAddress("DelayedWETHProxy");
assertEq(address(uint160(uint256(vm.load(wethProxyAddr, Constants.PROXY_OWNER_ADDRESS)))), _proxyAdmin);
ChainAssertions.checkDelayedWETH(contracts, cfg, true, _systemOwnerSafe);
// Check the config elements in the deployed contracts.
ChainAssertions.checkOptimismPortal2(contracts, cfg, false);
......@@ -100,7 +131,7 @@ contract FPACOPS is Deploy, StdAssertions {
assertEq(address(mips.oracle()), address(oracle));
// Check the FaultDisputeGame configuration.
FaultDisputeGame gameImpl = FaultDisputeGame(address(dgfProxy.gameImpls(GameTypes.CANNON)));
FaultDisputeGame gameImpl = FaultDisputeGame(payable(address(dgfProxy.gameImpls(GameTypes.CANNON))));
assertEq(gameImpl.maxGameDepth(), cfg.faultGameMaxDepth());
assertEq(gameImpl.splitDepth(), cfg.faultGameSplitDepth());
assertEq(gameImpl.gameDuration().raw(), cfg.faultGameMaxDuration());
......@@ -110,7 +141,7 @@ contract FPACOPS is Deploy, StdAssertions {
// Check the security override yoke configuration.
PermissionedDisputeGame soyGameImpl =
PermissionedDisputeGame(address(dgfProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)));
PermissionedDisputeGame(payable(address(dgfProxy.gameImpls(GameTypes.PERMISSIONED_CANNON))));
assertEq(soyGameImpl.maxGameDepth(), cfg.faultGameMaxDepth());
assertEq(soyGameImpl.splitDepth(), cfg.faultGameSplitDepth());
assertEq(soyGameImpl.gameDuration().raw(), cfg.faultGameMaxDuration());
......
......@@ -100,8 +100,12 @@
"sourceCodeHash": "0x1e5a6deded88804971fc1847c9eac65921771bff353437c0b29ed2f55513b984"
},
"src/dispute/FaultDisputeGame.sol": {
"initCodeHash": "0x73600519fd00cda111fdc811ec4e1712d599300b2fbc53c70620702653e3be3a",
"sourceCodeHash": "0x7fd6726ba22f848e6177e9413bdb2e74dacbb9ae87d57cb0c4cbd497b9a318d3"
"initCodeHash": "0x1db101f0c3613d5e3f7e4f5f73e3f4e50917aef72abd7c28571d9be6cd76e4ad",
"sourceCodeHash": "0x7bea42037f03604a2781c238426af34d46bd85a6520cb884045dd13b33139b34"
},
"src/dispute/weth/DelayedWETH.sol": {
"initCodeHash": "0x41e274b12dc48658d073dfea67ef694c5cce3963757911ee4cecc9f4c312e4bb",
"sourceCodeHash": "0xb357a0d4b815ea9528cfb6d83aaa1e62cd0352223432d4f3e23bc09ae62691b8"
},
"src/legacy/DeployerWhitelist.sol": {
"initCodeHash": "0x8de80fb23b26dd9d849f6328e56ea7c173cd9e9ce1f05c9beea559d1720deb3d",
......
This diff is collapsed.
......@@ -40,11 +40,24 @@
"internalType": "contract IBigStepper",
"name": "_vm",
"type": "address"
},
{
"internalType": "contract IDelayedWETH",
"name": "_weth",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"stateMutability": "payable",
"type": "fallback"
},
{
"stateMutability": "payable",
"type": "receive"
},
{
"inputs": [],
"name": "absolutePrestate",
......@@ -545,6 +558,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IDelayedWETH",
"name": "weth_",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
......@@ -663,6 +689,11 @@
"name": "InvalidSplitDepth",
"type": "error"
},
{
"inputs": [],
"name": "NoCreditToClaim",
"type": "error"
},
{
"inputs": [],
"name": "OutOfOrderResolution",
......
......@@ -41,6 +41,11 @@
"name": "_vm",
"type": "address"
},
{
"internalType": "contract IDelayedWETH",
"name": "_weth",
"type": "address"
},
{
"internalType": "address",
"name": "_proposer",
......@@ -55,6 +60,14 @@
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"stateMutability": "payable",
"type": "fallback"
},
{
"stateMutability": "payable",
"type": "receive"
},
{
"inputs": [],
"name": "absolutePrestate",
......@@ -555,6 +568,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IDelayedWETH",
"name": "weth_",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
......@@ -678,6 +704,11 @@
"name": "InvalidSplitDepth",
"type": "error"
},
{
"inputs": [],
"name": "NoCreditToClaim",
"type": "error"
},
{
"inputs": [],
"name": "OutOfOrderResolution",
......
[
{
"stateMutability": "payable",
"type": "receive"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "guy",
"type": "address"
},
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "deposit",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "src",
"type": "address"
},
{
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "src",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "guy",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "src",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "src",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Withdrawal",
"type": "event"
}
]
\ No newline at end of file
......@@ -1954,7 +1954,7 @@
"newValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"reverted": false,
"slot": "0x000000000000000000000000000000000000000000000000000000000000003a"
"slot": "0x000000000000000000000000000000000000000000000000000000000000003b"
}
],
"value": 0
......@@ -1980,7 +1980,7 @@
"newValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"reverted": false,
"slot": "0x000000000000000000000000000000000000000000000000000000000000003b"
"slot": "0x000000000000000000000000000000000000000000000000000000000000003c"
}
],
"value": 0
......@@ -2441,7 +2441,7 @@
"newValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"reverted": false,
"slot": "0x000000000000000000000000000000000000000000000000000000000000003a"
"slot": "0x000000000000000000000000000000000000000000000000000000000000003b"
}
],
"value": 0
......@@ -2519,7 +2519,7 @@
"newValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"reverted": false,
"slot": "0x000000000000000000000000000000000000000000000000000000000000003b"
"slot": "0x000000000000000000000000000000000000000000000000000000000000003c"
}
],
"value": 0
......@@ -7162,7 +7162,7 @@
"newValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
"reverted": false,
"slot": "0x0000000000000000000000000000000000000000000000000000000000000039"
"slot": "0x000000000000000000000000000000000000000000000000000000000000003a"
}
],
"value": 0
......
[
{
"bytes": "1",
"label": "_initialized",
"offset": 0,
"slot": "0",
"type": "uint8"
},
{
"bytes": "1",
"label": "_initializing",
"offset": 1,
"slot": "0",
"type": "bool"
},
{
"bytes": "1600",
"label": "__gap",
"offset": 0,
"slot": "1",
"type": "uint256[50]"
},
{
"bytes": "20",
"label": "_owner",
"offset": 0,
"slot": "51",
"type": "address"
},
{
"bytes": "1568",
"label": "__gap",
"offset": 0,
"slot": "52",
"type": "uint256[49]"
},
{
"bytes": "32",
"label": "balanceOf",
"offset": 0,
"slot": "101",
"type": "mapping(address => uint256)"
},
{
"bytes": "32",
"label": "allowance",
"offset": 0,
"slot": "102",
"type": "mapping(address => mapping(address => uint256))"
},
{
"bytes": "32",
"label": "withdrawals",
"offset": 0,
"slot": "103",
"type": "mapping(address => mapping(address => struct IDelayedWETH.WithdrawalRequest))"
},
{
"bytes": "20",
"label": "config",
"offset": 0,
"slot": "104",
"type": "contract SuperchainConfig"
}
]
\ No newline at end of file
[
{
"bytes": "32",
"label": "balanceOf",
"offset": 0,
"slot": "0",
"type": "mapping(address => uint256)"
},
{
"bytes": "32",
"label": "allowance",
"offset": 0,
"slot": "1",
"type": "mapping(address => mapping(address => uint256))"
}
]
\ No newline at end of file
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { IDelayedWETH } from "src/dispute/interfaces/IDelayedWETH.sol";
import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol";
import { IFaultDisputeGame } from "src/dispute/interfaces/IFaultDisputeGame.sol";
import { IInitializable } from "src/dispute/interfaces/IInitializable.sol";
......@@ -47,6 +48,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
/// @notice The game type ID
GameType internal immutable GAME_TYPE;
/// @notice WETH contract for holding ETH
IDelayedWETH internal immutable WETH;
/// @notice The global root claim's position is always at gindex 1.
Position internal constant ROOT_POSITION = Position.wrap(1);
......@@ -91,8 +95,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
/// @param _maxGameDepth The maximum depth of bisection.
/// @param _splitDepth The final depth of the output bisection portion of the game.
/// @param _gameDuration The duration of the game.
/// @param _vm An onchain VM that performs single instruction steps on a fault proof program
/// trace.
/// @param _vm An onchain VM that performs single instruction steps on an FPP trace.
/// @param _weth WETH contract for holding ETH.
constructor(
GameType _gameType,
Claim _absolutePrestate,
......@@ -101,7 +105,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
uint256 _maxGameDepth,
uint256 _splitDepth,
Duration _gameDuration,
IBigStepper _vm
IBigStepper _vm,
IDelayedWETH _weth
) {
// The split depth cannot be greater than or equal to the max game depth.
if (_splitDepth >= _maxGameDepth) revert InvalidSplitDepth();
......@@ -114,8 +119,15 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
SPLIT_DEPTH = _splitDepth;
GAME_DURATION = _gameDuration;
VM = _vm;
WETH = _weth;
}
/// @notice Receive function to allow the contract to receive ETH.
receive() external payable { }
/// @notice Fallback function to allow the contract to receive ETH.
fallback() external payable { }
////////////////////////////////////////////////////////////////
// `IFaultDisputeGame` impl //
////////////////////////////////////////////////////////////////
......@@ -292,6 +304,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
// Update the subgame rooted at the parent claim.
subgames[_challengeIndex].push(claimData.length - 1);
// Deposit the bond.
WETH.deposit{ value: msg.value }();
// Emit the appropriate event for the attack or defense.
emit Move(_challengeIndex, _claim, msg.sender);
}
......@@ -519,6 +534,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
})
);
// Deposit the bond.
WETH.deposit{ value: msg.value }();
// Set the game's starting timestamp
createdAt = Timestamp.wrap(uint64(block.timestamp));
......@@ -547,6 +565,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
uint256 recipientCredit = credit[_recipient];
credit[_recipient] = 0;
// Revert if the recipient has no credit to claim.
if (recipientCredit == 0) {
revert NoCreditToClaim();
}
// Try to withdraw the WETH amount so it can be used here.
WETH.withdraw(_recipient, recipientCredit);
// Transfer the credit to the recipient.
(bool success,) = _recipient.call{ value: recipientCredit }(hex"");
if (!success) revert BondTransferFailed();
......@@ -597,6 +623,11 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
genesisOutputRoot_ = GENESIS_OUTPUT_ROOT;
}
/// @notice Returns the WETH contract for holding ETH.
function weth() external view returns (IDelayedWETH weth_) {
weth_ = WETH;
}
////////////////////////////////////////////////////////////////
// HELPERS //
////////////////////////////////////////////////////////////////
......@@ -612,6 +643,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
// Increase the recipient's credit.
credit[_recipient] += bond;
// Unlock the bond.
WETH.unlock(_recipient, bond);
}
/// @notice Verifies the integrity of an execution bisection subgame's root claim. Reverts if the claim
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { IDelayedWETH } from "src/dispute/interfaces/IDelayedWETH.sol";
import { FaultDisputeGame, IFaultDisputeGame, IBigStepper, IInitializable } from "src/dispute/FaultDisputeGame.sol";
import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol";
......@@ -44,6 +45,7 @@ contract PermissionedDisputeGame is FaultDisputeGame {
uint256 _splitDepth,
Duration _gameDuration,
IBigStepper _vm,
IDelayedWETH _weth,
address _proposer,
address _challenger
)
......@@ -55,7 +57,8 @@ contract PermissionedDisputeGame is FaultDisputeGame {
_maxGameDepth,
_splitDepth,
_gameDuration,
_vm
_vm,
_weth
)
{
PROPOSER = _proposer;
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { IWETH } from "src/dispute/interfaces/IWETH.sol";
/// @title IDelayedWETH
/// @notice Interface for the DelayedWETH contract.
interface IDelayedWETH is IWETH {
/// @notice Represents a withdrawal request.
struct WithdrawalRequest {
uint256 amount;
uint256 timestamp;
}
/// @notice Emitted when an unwrap is started.
/// @param src The address that started the unwrap.
/// @param wad The amount of WETH that was unwrapped.
event Unwrap(address indexed src, uint256 wad);
/// @notice Returns the withdrawal delay in seconds.
/// @return The withdrawal delay in seconds.
function delay() external view returns (uint256);
/// @notice Returns a withdrawal request for the given address.
/// @param _owner The address to query the withdrawal request of.
/// @param _guy Sub-account to query the withdrawal request of.
/// @return The withdrawal request for the given address-subaccount pair.
function withdrawals(address _owner, address _guy) external view returns (uint256, uint256);
/// @notice Unlocks withdrawals for the sender's account, after a time delay.
/// @param _guy Sub-account to unlock.
/// @param _wad The amount of WETH to unlock.
function unlock(address _guy, uint256 _wad) external;
/// @notice Extension to withdrawal, must provide a sub-account to withdraw from.
/// @param _guy Sub-account to withdraw from.
/// @param _wad The amount of WETH to withdraw.
function withdraw(address _guy, uint256 _wad) external;
/// @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;
/// @notice Allows the owner to recover from error cases by pulling ETH from a specific owner.
/// @param _guy The address to recover the WETH from.
/// @param _wad The amount of WETH to recover.
function hold(address _guy, uint256 _wad) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
/// @title IWETH
/// @notice Interface for WETH9.
interface IWETH {
/// @notice Emitted when an approval is made.
/// @param src The address that approved the transfer.
/// @param guy The address that was approved to transfer.
/// @param wad The amount that was approved to transfer.
event Approval(address indexed src, address indexed guy, uint256 wad);
/// @notice Emitted when a transfer is made.
/// @param src The address that transferred the WETH.
/// @param dst The address that received the WETH.
/// @param wad The amount of WETH that was transferred.
event Transfer(address indexed src, address indexed dst, uint256 wad);
/// @notice Emitted when a deposit is made.
/// @param dst The address that deposited the WETH.
/// @param wad The amount of WETH that was deposited.
event Deposit(address indexed dst, uint256 wad);
/// @notice Emitted when a withdrawal is made.
/// @param src The address that withdrew the WETH.
/// @param wad The amount of WETH that was withdrawn.
event Withdrawal(address indexed src, uint256 wad);
/// @notice Returns the name of the token.
/// @return The name of the token.
function name() external pure returns (string memory);
/// @notice Returns the symbol of the token.
/// @return The symbol of the token.
function symbol() external pure returns (string memory);
/// @notice Returns the number of decimals the token uses.
/// @return The number of decimals the token uses.
function decimals() external pure returns (uint8);
/// @notice Returns the balance of the given address.
/// @param owner The address to query the balance of.
/// @return The balance of the given address.
function balanceOf(address owner) external view returns (uint256);
/// @notice Returns the amount of WETH that the spender can transfer on behalf of the owner.
/// @param owner The address that owns the WETH.
/// @param spender The address that is approved to transfer the WETH.
/// @return The amount of WETH that the spender can transfer on behalf of the owner.
function allowance(address owner, address spender) external view returns (uint256);
/// @notice Allows WETH to be deposited by sending ether to the contract.
function deposit() external payable;
/// @notice Withdraws an amount of ETH.
/// @param wad The amount of ETH to withdraw.
function withdraw(uint256 wad) external;
/// @notice Returns the total supply of WETH.
/// @return The total supply of WETH.
function totalSupply() external view returns (uint256);
/// @notice Approves the given address to transfer the WETH on behalf of the caller.
/// @param guy The address that is approved to transfer the WETH.
/// @param wad The amount that is approved to transfer.
/// @return True if the approval was successful.
function approve(address guy, uint256 wad) external returns (bool);
/// @notice Transfers the given amount of WETH to the given address.
/// @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 transfer(address dst, uint256 wad) external returns (bool);
/// @notice Transfers the given amount of WETH from the given address to the given address.
/// @param src The address to transfer the WETH from.
/// @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) external returns (bool);
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { ISemver } from "src/universal/ISemver.sol";
import { IDelayedWETH } from "src/dispute/interfaces/IDelayedWETH.sol";
import { IWETH } from "src/dispute/interfaces/IWETH.sol";
import { WETH98 } from "src/dispute/weth/WETH98.sol";
import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
/// @title DelayedWETH
/// @notice DelayedWETH is an extension to WETH9 that allows for delayed withdrawals. Accounts must
/// trigger an unlock function before they can withdraw WETH. Accounts must trigger unlock
/// by specifying a sub-account and an amount of WETH to unlock. Accounts can trigger the
/// unlock function at any time, but must wait a delay period before they can withdraw
/// after the unlock function is triggered. DelayedWETH is designed to be used by the
/// DisputeGame contracts where unlock will only be triggered after a dispute is resolved.
/// DelayedWETH is meant to sit behind a proxy contract and has an owner address that can
/// pull WETH from any account and can recover ETH from the contract itself. Variable and
/// function naming vaguely follows the vibe of WETH9. Not the prettiest contract in the
/// world, but it gets the job done.
contract DelayedWETH is OwnableUpgradeable, WETH98, IDelayedWETH, ISemver {
/// @notice Semantic version.
/// @custom:semver 0.2.0
string public constant version = "0.2.0";
/// @inheritdoc IDelayedWETH
mapping(address => mapping(address => WithdrawalRequest)) public withdrawals;
/// @notice Withdrawal delay in seconds.
uint256 internal immutable DELAY_SECONDS;
/// @notice Address of the SuperchainConfig contract.
SuperchainConfig public config;
/// @param _delay The delay for withdrawals in seconds.
constructor(uint256 _delay) {
DELAY_SECONDS = _delay;
initialize({ _owner: address(0), _config: SuperchainConfig(address(0)) });
}
/// @notice Initializes the contract.
/// @param _owner The address of the owner.
/// @param _config Address of the SuperchainConfig contract.
function initialize(address _owner, SuperchainConfig _config) public initializer {
__Ownable_init();
_transferOwnership(_owner);
config = _config;
}
/// @inheritdoc IDelayedWETH
function delay() external view returns (uint256) {
return DELAY_SECONDS;
}
/// @inheritdoc IDelayedWETH
function unlock(address _guy, uint256 _wad) external {
// Note that the unlock function can be called by any address, but the actual unlocking
// capability still only gives the msg.sender the ability to withdraw from the account.
// As long as the unlock and withdraw functions are called with the proper recipient
// addresses, this will be safe. Could be made safer by having external accounts execute
// withdrawals themselves but that would have added extra complexity and made DelayedWETH
// a leaky abstraction, so we chose this instead.
require(!config.paused(), "DelayedWETH: contract is paused");
WithdrawalRequest storage wd = withdrawals[msg.sender][_guy];
wd.timestamp = block.timestamp;
wd.amount += _wad;
}
/// @inheritdoc IWETH
function withdraw(uint256 _wad) public override(WETH98, IWETH) {
withdraw(msg.sender, _wad);
}
/// @inheritdoc IDelayedWETH
function withdraw(address _guy, uint256 _wad) public {
require(!config.paused(), "DelayedWETH: contract is paused");
WithdrawalRequest storage wd = withdrawals[msg.sender][_guy];
require(wd.amount >= _wad, "DelayedWETH: insufficient unlocked withdrawal");
require(wd.timestamp > 0, "DelayedWETH: withdrawal not unlocked");
require(wd.timestamp + DELAY_SECONDS <= block.timestamp, "DelayedWETH: withdrawal delay not met");
wd.amount -= _wad;
super.withdraw(_wad);
}
/// @inheritdoc IDelayedWETH
function recover(uint256 _wad) external {
require(msg.sender == owner(), "DelayedWETH: not owner");
require(address(this).balance >= _wad, "DelayedWETH: insufficient balance");
payable(msg.sender).transfer(_wad);
}
/// @inheritdoc IDelayedWETH
function hold(address _guy, uint256 _wad) external {
require(msg.sender == owner(), "DelayedWETH: not owner");
allowance[msg.sender][_guy] = _wad;
emit Approval(msg.sender, _guy, _wad);
}
}
// Copyright (C) 2015, 2016, 2017 Dapphub
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Based on WETH9 by Dapphub.
// Modified by OP Labs.
pragma solidity 0.8.15;
import { IWETH } from "src/dispute/interfaces/IWETH.sol";
/// @title WETH98
/// @notice WETH98 is a version of WETH9 upgraded for Solidity 0.8.x.
contract WETH98 is IWETH {
string public constant name = "Wrapped Ether";
string public constant symbol = "WETH";
uint8 public constant decimals = 18;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/// @notice Pipes to deposit.
receive() external payable {
deposit();
}
/// @inheritdoc IWETH
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
/// @inheritdoc IWETH
function withdraw(uint256 wad) public virtual {
require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] -= wad;
payable(msg.sender).transfer(wad);
emit Withdrawal(msg.sender, wad);
}
/// @inheritdoc IWETH
function totalSupply() external view returns (uint256) {
return address(this).balance;
}
/// @inheritdoc IWETH
function approve(address guy, uint256 wad) external returns (bool) {
allowance[msg.sender][guy] = wad;
emit Approval(msg.sender, guy, wad);
return true;
}
/// @inheritdoc IWETH
function transfer(address dst, uint256 wad) external returns (bool) {
return transferFrom(msg.sender, dst, wad);
}
/// @inheritdoc IWETH
function transferFrom(address src, address dst, uint256 wad) public returns (bool) {
require(balanceOf[src] >= wad);
if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) {
require(allowance[src][msg.sender] >= wad);
allowance[src][msg.sender] -= wad;
}
balanceOf[src] -= wad;
balanceOf[dst] += wad;
emit Transfer(src, dst, wad);
return true;
}
}
......@@ -30,6 +30,9 @@ error AlreadyInitialized();
/// @notice Thrown when a supplied bond is too low to cover the cost of the interaction.
error InsufficientBond();
/// @notice Thrown when a credit claim is attempted for a value of 0.
error NoCreditToClaim();
/// @notice Thrown when the transfer of credit to a recipient account reverts.
error BondTransferFailed();
......
......@@ -334,11 +334,13 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
function setUp() public override {
_proposedBlockNumber = 0xFF;
game = FaultDisputeGame(
payable(
address(
disputeGameFactory.create(
optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber)
)
)
)
);
_proposedGameIndex = disputeGameFactory.gameCount() - 1;
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { console } from "forge-std/console.sol";
// Testing utilities
import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol";
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol";
import { Test } from "forge-std/Test.sol";
import { DisputeGameFactory, IDisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol";
import { DelayedWETH } from "src/dispute/weth/DelayedWETH.sol";
import { Proxy } from "src/universal/Proxy.sol";
import { CommonTest } from "test/setup/CommonTest.sol";
contract DelayedWETH_Init is CommonTest {
event Approval(address indexed src, address indexed guy, uint256 wad);
event Transfer(address indexed src, address indexed dst, uint256 wad);
event Deposit(address indexed dst, uint256 wad);
event Withdrawal(address indexed src, uint256 wad);
event Unwrap(address indexed src, uint256 wad);
function setUp() public virtual override {
super.enableFaultProofs();
super.setUp();
// Transfer ownership of delayed WETH to the test contract.
vm.prank(deploy.mustGetAddress("SystemOwnerSafe"));
delayedWeth.transferOwnership(address(this));
}
}
contract DelayedWETH_Initialize_Test is DelayedWETH_Init {
/// @dev Tests that initialization is successful.
function test_initialize_succeeds() public {
assertEq(delayedWeth.owner(), address(this));
assertEq(address(delayedWeth.config()), address(superchainConfig));
}
}
contract DelayedWETH_Unlock_Test is DelayedWETH_Init {
/// @dev Tests that unlocking once is successful.
function test_unlock_once_succeeds() public {
delayedWeth.unlock(alice, 1 ether);
(uint256 amount, uint256 timestamp) = delayedWeth.withdrawals(address(this), alice);
assertEq(amount, 1 ether);
assertEq(timestamp, block.timestamp);
}
/// @dev TEsts that unlocking twice is successful and timestamp/amount is updated.
function test_unlock_twice_succeeds() public {
// Unlock once.
uint256 ts = block.timestamp;
delayedWeth.unlock(alice, 1 ether);
(uint256 amount1, uint256 timestamp1) = delayedWeth.withdrawals(address(this), alice);
assertEq(amount1, 1 ether);
assertEq(timestamp1, ts);
// Go forward in time.
vm.warp(ts + 1);
// Unlock again works.
delayedWeth.unlock(alice, 1 ether);
(uint256 amount2, uint256 timestamp2) = delayedWeth.withdrawals(address(this), alice);
assertEq(amount2, 2 ether);
assertEq(timestamp2, ts + 1);
}
/// @dev Tests that unlocking while paused fails.
function test_unlock_whilePaused_fails() public {
// Pause the contract.
address guardian = optimismPortal.GUARDIAN();
vm.prank(guardian);
superchainConfig.pause("identifier");
// Unlock fails.
vm.expectRevert("DelayedWETH: contract is paused");
delayedWeth.unlock(alice, 1 ether);
}
}
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 {
// Deposit some WETH.
vm.prank(alice);
delayedWeth.deposit{ value: 1 ether }();
uint256 balance = address(alice).balance;
// Unlock the withdrawal.
vm.prank(alice);
delayedWeth.unlock(alice, 1 ether);
// Wait for the delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1);
// Withdraw the WETH.
vm.expectEmit(true, true, false, false);
emit Withdrawal(address(alice), 1 ether);
vm.prank(alice);
delayedWeth.withdraw(alice, 1 ether);
assertEq(address(alice).balance, balance + 1 ether);
}
/// @dev Tests that withdrawing when unlock was not called fails.
function test_withdraw_whileLocked_fails() public {
// Deposit some WETH.
vm.prank(alice);
delayedWeth.deposit{ value: 1 ether }();
uint256 balance = address(alice).balance;
// Withdraw fails when unlock not called.
vm.expectRevert("DelayedWETH: withdrawal not unlocked");
vm.prank(alice);
delayedWeth.withdraw(alice, 0 ether);
assertEq(address(alice).balance, balance);
}
/// @dev Tests that withdrawing while locked and delay has not passed fails.
function test_withdraw_whileLockedNotLongEnough_fails() public {
// Deposit some WETH.
vm.prank(alice);
delayedWeth.deposit{ value: 1 ether }();
uint256 balance = address(alice).balance;
// Call unlock.
vm.prank(alice);
delayedWeth.unlock(alice, 1 ether);
// Wait for the delay, but not long enough.
vm.warp(block.timestamp + delayedWeth.delay() - 1);
// Withdraw fails when delay not met.
vm.expectRevert("DelayedWETH: withdrawal delay not met");
vm.prank(alice);
delayedWeth.withdraw(alice, 1 ether);
assertEq(address(alice).balance, balance);
}
/// @dev Tests that withdrawing more than unlocked amount fails.
function test_withdraw_tooMuch_fails() public {
// Deposit some WETH.
vm.prank(alice);
delayedWeth.deposit{ value: 1 ether }();
uint256 balance = address(alice).balance;
// Unlock the withdrawal.
vm.prank(alice);
delayedWeth.unlock(alice, 1 ether);
// Wait for the delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1);
// Withdraw too much fails.
vm.expectRevert("DelayedWETH: insufficient unlocked withdrawal");
vm.prank(alice);
delayedWeth.withdraw(alice, 2 ether);
assertEq(address(alice).balance, balance);
}
/// @dev Tests that withdrawing while paused fails.
function test_withdraw_whenPaused_fails() 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.
address guardian = optimismPortal.GUARDIAN();
vm.prank(guardian);
superchainConfig.pause("identifier");
// Withdraw fails.
vm.expectRevert("DelayedWETH: contract is paused");
vm.prank(alice);
delayedWeth.withdraw(alice, 1 ether);
}
}
contract DelayedWETH_Recover_Test is DelayedWETH_Init {
/// @dev Tests that recovering WETH succeeds.
function test_recover_succeeds() public {
delayedWeth.transferOwnership(alice);
// Give the contract some WETH to recover.
vm.deal(address(delayedWeth), 1 ether);
// Record the initial balance.
uint256 initialBalance = address(alice).balance;
// Recover the WETH.
vm.prank(alice);
delayedWeth.recover(1 ether);
// Verify the WETH was recovered.
assertEq(address(delayedWeth).balance, 0);
assertEq(address(alice).balance, initialBalance + 1 ether);
}
/// @dev Tests that recovering WETH by non-owner fails.
function test_recover_byNonOwner_fails() public {
vm.prank(alice);
vm.expectRevert("DelayedWETH: not owner");
delayedWeth.recover(1 ether);
}
/// @dev Tests that recovering more than the balance fails.
function test_recover_moreThanBalance_fails() public {
vm.deal(address(delayedWeth), 0.5 ether);
vm.expectRevert("DelayedWETH: insufficient balance");
delayedWeth.recover(1 ether);
}
}
contract DelayedWETH_Hold_Test is DelayedWETH_Init {
/// @dev Tests that holding WETH succeeds.
function test_hold_succeeds() public {
uint256 amount = 1 ether;
vm.expectEmit(true, true, true, false);
emit Approval(address(this), alice, amount);
delayedWeth.hold(alice, amount);
assertEq(delayedWeth.allowance(address(this), alice), amount);
}
/// @dev Tests that holding WETH by non-owner fails.
function test_hold_byNonOwner_fails() public {
vm.prank(alice);
vm.expectRevert("DelayedWETH: not owner");
delayedWeth.hold(bob, 1 ether);
}
}
......@@ -6,6 +6,7 @@ import { Vm } from "forge-std/Vm.sol";
import { DisputeGameFactory_Init } from "test/dispute/DisputeGameFactory.t.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { PermissionedDisputeGame } from "src/dispute/PermissionedDisputeGame.sol";
import { DelayedWETH } from "src/dispute/weth/DelayedWETH.sol";
import { L2OutputOracle } from "src/L1/L2OutputOracle.sol";
import { PreimageOracle } from "src/cannon/PreimageOracle.sol";
import { PreimageKeyLib } from "src/cannon/PreimageKeyLib.sol";
......@@ -55,6 +56,9 @@ contract PermissionedDisputeGame_Init is DisputeGameFactory_Init {
AlphabetVM _vm = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0, 0));
// Use a 7 day delayed WETH to simulate withdrawals.
DelayedWETH _weth = new DelayedWETH(7 days);
// Deploy an implementation of the fault game
gameImpl = new PermissionedDisputeGame({
_gameType: GAME_TYPE,
......@@ -65,6 +69,7 @@ contract PermissionedDisputeGame_Init is DisputeGameFactory_Init {
_splitDepth: 2 ** 2,
_gameDuration: Duration.wrap(7 days),
_vm: _vm,
_weth: _weth,
_proposer: PROPOSER,
_challenger: CHALLENGER
});
......@@ -72,7 +77,8 @@ contract PermissionedDisputeGame_Init is DisputeGameFactory_Init {
disputeGameFactory.setImplementation(GAME_TYPE, gameImpl);
// Create a new game.
vm.prank(PROPOSER, PROPOSER);
gameProxy = PermissionedDisputeGame(address(disputeGameFactory.create(GAME_TYPE, rootClaim, extraData)));
gameProxy =
PermissionedDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, rootClaim, extraData))));
// Check immutables
assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw());
......
......@@ -7,6 +7,7 @@ import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol";
import { FaultDisputeGame_Init } from "test/dispute/FaultDisputeGame.t.sol";
import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol";
contract FaultDisputeGame_Solvency_Invariant is FaultDisputeGame_Init {
Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32(uint256(10)));
......@@ -46,8 +47,22 @@ contract FaultDisputeGame_Solvency_Invariant is FaultDisputeGame_Init {
}
gameProxy.resolve();
// Wait for the withdrawal delay.
vm.warp(block.timestamp + 7 days + 1 seconds);
if (gameProxy.credit(address(this)) == 0) {
vm.expectRevert(NoCreditToClaim.selector);
gameProxy.claimCredit(address(this));
} else {
gameProxy.claimCredit(address(this));
}
if (gameProxy.credit(address(actor)) == 0) {
vm.expectRevert(NoCreditToClaim.selector);
gameProxy.claimCredit(address(actor));
} else {
gameProxy.claimCredit(address(actor));
}
if (gameProxy.status() == GameStatus.DEFENDER_WINS) {
assertEq(address(this).balance, type(uint96).max);
......
......@@ -116,11 +116,13 @@ contract OptimismPortal2_Invariant_Harness is CommonTest {
// Create a dispute game with the output root we've proposed.
_proposedBlockNumber = 0xFF;
FaultDisputeGame game = FaultDisputeGame(
payable(
address(
disputeGameFactory.create(
optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber)
)
)
)
);
_proposedGameIndex = disputeGameFactory.gameCount() - 1;
......
......@@ -19,6 +19,7 @@ import { FeeVault } from "src/universal/FeeVault.sol";
import { OptimismPortal } from "src/L1/OptimismPortal.sol";
import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { DelayedWETH } from "src/dispute/weth/DelayedWETH.sol";
import { L1CrossDomainMessenger } from "src/L1/L1CrossDomainMessenger.sol";
import { DeployConfig } from "scripts/DeployConfig.s.sol";
import { Deploy } from "scripts/Deploy.s.sol";
......@@ -50,6 +51,7 @@ contract Setup {
OptimismPortal optimismPortal;
OptimismPortal2 optimismPortal2;
DisputeGameFactory disputeGameFactory;
DelayedWETH delayedWeth;
L2OutputOracle l2OutputOracle;
SystemConfig systemConfig;
L1StandardBridge l1StandardBridge;
......@@ -101,6 +103,7 @@ contract Setup {
optimismPortal = OptimismPortal(deploy.mustGetAddress("OptimismPortalProxy"));
optimismPortal2 = OptimismPortal2(deploy.mustGetAddress("OptimismPortalProxy"));
disputeGameFactory = DisputeGameFactory(deploy.mustGetAddress("DisputeGameFactoryProxy"));
delayedWeth = DelayedWETH(deploy.mustGetAddress("DelayedWETHProxy"));
l2OutputOracle = L2OutputOracle(deploy.mustGetAddress("L2OutputOracleProxy"));
systemConfig = SystemConfig(deploy.mustGetAddress("SystemConfigProxy"));
l1StandardBridge = L1StandardBridge(deploy.mustGetAddress("L1StandardBridgeProxy"));
......@@ -118,6 +121,8 @@ contract Setup {
vm.label(deploy.mustGetAddress("OptimismPortalProxy"), "OptimismPortalProxy");
vm.label(address(disputeGameFactory), "DisputeGameFactory");
vm.label(deploy.mustGetAddress("DisputeGameFactoryProxy"), "DisputeGameFactoryProxy");
vm.label(address(delayedWeth), "DelayedWETH");
vm.label(deploy.mustGetAddress("DelayedWETHProxy"), "DelayedWETHProxy");
vm.label(address(systemConfig), "SystemConfig");
vm.label(deploy.mustGetAddress("SystemConfigProxy"), "SystemConfigProxy");
vm.label(address(l1StandardBridge), "L1StandardBridge");
......
......@@ -6,6 +6,7 @@ import { Executables } from "scripts/Executables.sol";
import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol";
import { L2OutputOracle } from "src/L1/L2OutputOracle.sol";
import { SystemConfig } from "src/L1/SystemConfig.sol";
import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { OptimismPortal } from "src/L1/OptimismPortal.sol";
import "src/L1/ProtocolVersions.sol";
......@@ -85,6 +86,22 @@ contract Initializer_Test is Bridge_Initializer {
initializedSlotVal: deploy.loadInitializedSlot("DisputeGameFactoryProxy")
})
);
// DelayedWETHImpl
contracts.push(
InitializeableContract({
target: deploy.mustGetAddress("DelayedWETH"),
initCalldata: abi.encodeCall(delayedWeth.initialize, (address(0), SuperchainConfig(address(0)))),
initializedSlotVal: deploy.loadInitializedSlot("DelayedWETH")
})
);
// DelayedWETHProxy
contracts.push(
InitializeableContract({
target: address(delayedWeth),
initCalldata: abi.encodeCall(delayedWeth.initialize, (address(0), SuperchainConfig(address(0)))),
initializedSlotVal: deploy.loadInitializedSlot("DelayedWETHProxy")
})
);
// L2OutputOracleImpl
contracts.push(
InitializeableContract({
......@@ -314,9 +331,9 @@ contract Initializer_Test is Bridge_Initializer {
/// 3. The `initialize()` function of each contract cannot be called more than once.
function test_cannotReinitialize_succeeds() public {
// Ensure that all L1, L2 `Initializable` contracts are accounted for, in addition to
// OptimismMintableERC20FactoryImpl, OptimismMintableERC20FactoryProxy, OptimismPortal2, DisputeGameFactoryImpl
// and DisputeGameFactoryProxy
assertEq(_getNumInitializable() + 3, contracts.length);
// OptimismMintableERC20FactoryImpl, OptimismMintableERC20FactoryProxy, OptimismPortal2,
// DisputeGameFactoryImpl, DisputeGameFactoryProxy, DelayedWETHImpl, DelayedWETHProxy.
assertEq(_getNumInitializable() + 5, contracts.length);
// Attempt to re-initialize all contracts within the `contracts` array.
for (uint256 i; i < contracts.length; i++) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment