diff --git a/Dockerfile-opstack b/Dockerfile-opstack index 8aa8e8157a6c2..541cd0bbb23b1 100644 --- a/Dockerfile-opstack +++ b/Dockerfile-opstack @@ -43,7 +43,7 @@ RUN --mount=type=cache,target=/root/.cache \ make build-go-no-submodules # Copy foundry tools and artifacts from contracts image -COPY --from=op-contracts:v1.13.4 /usr/local/bin/forge /usr/local/bin/cast /usr/local/bin/anvil /usr/local/bin/ +COPY --from=op-contracts:latest /usr/local/bin/forge /usr/local/bin/cast /usr/local/bin/anvil /usr/local/bin/ # Verify installations RUN echo "🔍 Verifying installations:" && \ diff --git a/packages/contracts-bedrock/scripts/SetupCustomGasToken.s.sol b/packages/contracts-bedrock/scripts/SetupCustomGasToken.s.sol index 29662ae4d3695..b7f57f9b1aef8 100644 --- a/packages/contracts-bedrock/scripts/SetupCustomGasToken.s.sol +++ b/packages/contracts-bedrock/scripts/SetupCustomGasToken.s.sol @@ -9,7 +9,6 @@ import { stdJson } from "forge-std/StdJson.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { DepositedOKBAdapter } from "src/L1/DepositedOKBAdapter.sol"; -import { OKBBurner } from "src/L1/OKBBurner.sol"; // Interfaces import { IOKB } from "interfaces/L1/IOKB.sol"; @@ -28,8 +27,8 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; /// @notice Foundry script to set up and verify custom gas token configuration /// @dev This script: /// 1. Reads OKB token address from environment variable -/// 2. Deploys OKBBurner implementation contract for minimal proxy pattern -/// 3. Deploys DepositedOKBAdapter with burner implementation reference +/// 2. Deploys DepositedOKBAdapter that handles OKB burning internally +/// 3. Adds deployer address to whitelist for deposits /// 4. Sets gas paying token in SystemConfig storage /// 5. Verifies all configurations on L1 contract SetupCustomGasToken is Script { @@ -43,7 +42,6 @@ contract SetupCustomGasToken is Script { // Deployed contracts IOKB okbToken; - OKBBurner burnerImplementation; DepositedOKBAdapter adapter; function setUp() public { @@ -71,10 +69,10 @@ contract SetupCustomGasToken is Script { vm.startBroadcast(msg.sender); - deployBurnerImplementation(); - deployAdapter(); + setupWhitelist(); + setGasPayingToken(); vm.stopBroadcast(); @@ -100,15 +98,18 @@ contract SetupCustomGasToken is Script { require(tokenAddr == Constants.ETHER, "FAILED: GasPayingToken already set"); } - /// @notice Deploy OKBBurner implementation contract - function deployBurnerImplementation() internal { - burnerImplementation = new OKBBurner(okbTokenAddress); // adapter address will be set later - console.log(" OKBBurner Implementation deployed at:", address(burnerImplementation)); + /// @notice Set up whitelist for authorized depositors + function setupWhitelist() internal { + console.log(" Adding deployer to whitelist..."); + address[] memory addresses = new address[](1); + addresses[0] = deployerAddress; + adapter.addToWhitelistBatch(addresses); + console.log(" Deployer whitelisted successfully:", deployerAddress); } /// @notice Deploy DepositedOKBAdapter function deployAdapter() internal { - adapter = new DepositedOKBAdapter(okbTokenAddress, payable(optimismPortalProxy), address(burnerImplementation)); + adapter = new DepositedOKBAdapter(okbTokenAddress, payable(optimismPortalProxy), deployerAddress); console.log(" DepositedOKBAdapter deployed at:", address(adapter)); } @@ -151,19 +152,23 @@ contract SetupCustomGasToken is Script { // Check DepositedOKBAdapter configuration require(address(adapter.OKB()) == okbTokenAddress, "FAILED: Adapter OKB mismatch"); require(address(adapter.PORTAL()) == optimismPortalProxy, "FAILED: Adapter portal mismatch"); + require(adapter.owner() == deployerAddress, "FAILED: Adapter owner mismatch"); - // Check OKBBurner Implementation configuration - require(address(burnerImplementation.OKB()) == okbTokenAddress, "FAILED: Burner OKB mismatch"); + // Check adapter has preminted total supply + uint256 adapterBalance = adapter.balanceOf(address(adapter)); + uint256 expectedBalance = okbToken.totalSupply(); + console.log(" [CHECK 6] Adapter balance:", adapterBalance); + console.log(" [CHECK 6] Expected balance (OKB total supply):", expectedBalance); + require(adapterBalance == expectedBalance, "FAILED: Adapter balance should equal OKB total supply"); - // Check Adapter burner implementation reference - require( - adapter.BURNER_IMPLEMENTATION() == address(burnerImplementation), - "FAILED: Adapter burner implementation mismatch" - ); + // Check whitelist configuration + console.log(" [CHECK 7] Verifying deployer whitelist..."); + require(adapter.whitelist(deployerAddress), "FAILED: Deployer address not whitelisted"); + console.log(" [CHECK 7] Deployer whitelist verified:", deployerAddress); - // Check Adapter approval to portal + // Check Adapter approval to portal (should be zero initially) uint256 allowance = adapter.allowance(address(adapter), optimismPortalProxy); - console.log(" [CHECK 7] Adapter approval to Portal:", allowance); + console.log(" [CHECK 8] Adapter approval to Portal:", allowance); require(allowance == 0, "FAILED: Adapter should not pre-approve portal"); } } diff --git a/packages/contracts-bedrock/src/L1/DepositedOKBAdapter.sol b/packages/contracts-bedrock/src/L1/DepositedOKBAdapter.sol index 00ef78583101f..4ef12f0be244d 100644 --- a/packages/contracts-bedrock/src/L1/DepositedOKBAdapter.sol +++ b/packages/contracts-bedrock/src/L1/DepositedOKBAdapter.sol @@ -1,12 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { OKBBurner } from "./OKBBurner.sol"; - // Contracts import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; -import { OKBBurner } from "src/L1/OKBBurner.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // Interfaces import { IOKB } from "interfaces/L1/IOKB.sol"; @@ -29,24 +27,18 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// - OKB burning to maintain strict supply constraints /// - Seamless integration with existing OP Stack infrastructure /// @dev This token is set as the gasPayingToken on SystemConfig. -contract DepositedOKBAdapter is ERC20 { +contract DepositedOKBAdapter is ERC20, Ownable { /// @notice Address of the OptimismPortal2 contract that this adapter works with. IOptimismPortal2 public immutable PORTAL; /// @notice Address of the OKB token contract. IOKB public immutable OKB; - /// @notice Address of the OKBBurner implementation contract. - address public immutable BURNER_IMPLEMENTATION; - - /// @notice Default gas limit for L2 transactions. + /// @notice Default gas limit for L2 deposit transactions. uint64 public constant DEFAULT_GAS_LIMIT = 100_000; - /// @notice Counter for creating unique burner addresses. - uint256 private _burnerNonce; - - /// @notice Address of the rescue address. - address public rescuer; + /// @notice Mapping of whitelisted addresses allowed to deposit. + mapping(address => bool) public whitelist; /// @notice Emitted when a user deposits OKB and initiates an L2 transaction. /// @param from Address that deposited the OKB. @@ -54,93 +46,102 @@ contract DepositedOKBAdapter is ERC20 { /// @param amount Amount of OKB burned and deposited. event Deposited(address indexed from, address indexed to, uint256 amount); - /// @notice Emitted when the rescuer is set. - /// @param rescuer The new rescuer address. - event RescuerSet(address indexed rescuer); + /// @notice Emitted when an address is added to the whitelist. + /// @param account Address that was added to the whitelist. + event WhitelistAdded(address indexed account); - /// @notice Emitted when ERC20 is rescued. - /// @param token The address of the token rescued. - /// @param to The address to which the token was rescued. - /// @param amount The amount of token rescued. - event ERC20Rescued(address indexed token, address indexed to, uint256 amount); + /// @notice Emitted when an address is removed from the whitelist. + /// @param account Address that was removed from the whitelist. + event WhitelistRemoved(address indexed account); /// @notice Thrown when transfer is attempted outside of portal operations. - /// @param to The attempted recipient address. - /// @param amount The attempted transfer amount. - error TransferNotAllowed(address to, uint256 amount); - - /// @notice Thrown when trying to transfer to an address other than the portal. - error OnlyPortalTransfer(); - - /// @notice Thrown when burner creation fails. - error BurnerCreationFailed(); - - /// @notice Thrown when OKB transfer to burner fails. - error TransferToBurnerFailed(); - - /// @notice Thrown when OKB burn fails. - error BurnFailed(); + error TransferNotAllowed(); /// @notice Thrown when amount is zero. error AmountMustBeGreaterThanZero(); - /// @notice Thrown when OKB address is zero. - error OKBAddressCannotBeZero(); + /// @notice Thrown when balance is insufficient. + error InsufficientBalance(); - /// @notice Thrown when portal address is zero. - error PortalAddressCannotBeZero(); + /// @notice Thrown when transfer fails. + error TransferFailed(); - /// @notice Thrown when rescuer address is zero. - error RescuerAddressCannotBeZero(); + /// @notice Thrown when transfer OKB from user fails. + error TransferFromUserFailed(); - /// @notice Thrown when burner implementation address is zero. - error BurnerImplementationCannotBeZero(); + /// @notice Thrown when OKB balance is not equal to the amount deposited. + error OKBBalanceMismatch(); - /// @notice Thrown when balance is insufficient. - error InsufficientBalance(); + /// @notice Thrown when caller is not whitelisted. + error NotWhitelisted(); - /// @notice Thrown when rescuer is not the rescuer. - error RescuerOnly(); + /// @notice Thrown when address is zero. + error AddressCannotBeZero(); - modifier onlyRescuer() { - if (msg.sender != rescuer) { - revert RescuerOnly(); - } - _; - } + /// @notice Thrown when OKB balance is not zero. + error OKBBalanceNotZeroAfterBurn(); - /// @notice Constructor sets up the adapter with references to OKB, OptimismPortal, and OKBBurner. + /// @notice Constructor sets up the adapter with references to OKB, OptimismPortal, rescuer, + /// and premints the maximum supply to the owner. /// @param _okb Address of the OKB token contract. /// @param _portal Address of the OptimismPortal2 contract. - /// @param _burnerImplementation Address of the OKBBurner implementation contract. - constructor(address _okb, address payable _portal, address _burnerImplementation) ERC20("Deposited OKB", "dOKB") { + /// @param _owner Address of the contract owner. + constructor(address _okb, address payable _portal, address _owner) ERC20("Deposited OKB", "dOKB") { if (_okb == address(0)) { - revert OKBAddressCannotBeZero(); + revert AddressCannotBeZero(); } if (_portal == address(0)) { - revert PortalAddressCannotBeZero(); + revert AddressCannotBeZero(); } - if (_burnerImplementation == address(0)) { - revert BurnerImplementationCannotBeZero(); - } - rescuer = msg.sender; OKB = IOKB(_okb); PORTAL = IOptimismPortal2(_portal); - BURNER_IMPLEMENTATION = _burnerImplementation; - emit RescuerSet(msg.sender); + + // Premint total supply of OKB to this contract to enforce hard cap + _mint(address(this), OKB.totalSupply()); + + // Transfer ownership to the owner + transferOwnership(_owner); + } + + /// @notice Adds multiple addresses to the whitelist in a single transaction. + /// @param _accounts Array of addresses to add to the whitelist. + function addToWhitelistBatch(address[] calldata _accounts) external onlyOwner { + for (uint256 i = 0; i < _accounts.length; i++) { + if (_accounts[i] == address(0)) { + revert AddressCannotBeZero(); + } + whitelist[_accounts[i]] = true; + emit WhitelistAdded(_accounts[i]); + } } - /// @notice Allows users to burn OKB and deposit into L2. + /// @notice Removes multiple addresses from the whitelist in a single transaction. + /// @param _accounts Array of addresses to remove from the whitelist. + function removeFromWhitelistBatch(address[] calldata _accounts) external onlyOwner { + for (uint256 i = 0; i < _accounts.length; i++) { + if (_accounts[i] == address(0)) { + revert AddressCannotBeZero(); + } + whitelist[_accounts[i]] = false; + emit WhitelistRemoved(_accounts[i]); + } + } + + /// @notice Allows whitelisted users to burn OKB and deposit into L2. /// This function: - /// 1. Transfers OKB from the user to this contract - /// 2. Creates a minimal proxy burner contract - /// 3. Transfers the exact amount of OKB to the burner - /// 4. Burns the OKB via the burner (which self-destructs) - /// 5. Mints deposit tokens to this contract - /// 6. Initiates an L2 deposit transaction via the portal + /// 1. Checks if caller is whitelisted + /// 2. Transfers OKB from the user to this contract + /// 3. Creates a minimal proxy burner contract + /// 4. Transfers the exact amount of OKB to the burner + /// 5. Burns the OKB via the burner (which self-destructs) + /// 6. Mints deposit tokens to this contract + /// 7. Initiates an L2 deposit transaction via the portal /// @param _to Target address on L2 to receive the tokens. /// @param _amount Amount of OKB to burn and deposit. function deposit(address _to, uint256 _amount) external { + if (!whitelist[msg.sender]) { + revert NotWhitelisted(); + } if (_amount == 0) { revert AmountMustBeGreaterThanZero(); } @@ -148,29 +149,33 @@ contract DepositedOKBAdapter is ERC20 { revert InsufficientBalance(); } - // Create a unique salt for deterministic burner address - bytes32 salt = keccak256(abi.encode(msg.sender, _amount, block.timestamp, _burnerNonce++)); - - // Create minimal proxy burner contract - address burner = Clones.cloneDeterministic(BURNER_IMPLEMENTATION, salt); - if (burner == address(0)) { - revert BurnerCreationFailed(); + // Transfer any remaining OKB to rescuer. + // If someone mistakenly directly transfer OKB to this contract, transfer it to the owner. + if (OKB.balanceOf(address(this)) > 0) { + bool transferSuccess = OKB.transfer(owner(), OKB.balanceOf(address(this))); + if (!transferSuccess) { + revert TransferFailed(); + } } // Transfer OKB from user to this contract first - bool transferFromUserSuccess = OKB.transferFrom(msg.sender, address(burner), _amount); + bool transferFromUserSuccess = OKB.transferFrom(msg.sender, address(this), _amount); if (!transferFromUserSuccess) { - revert TransferToBurnerFailed(); + revert TransferFromUserFailed(); } - // Burn the OKB via the burner (burner will self-destruct) - OKBBurner(burner).burnAndDestruct(); - uint256 amount = OKB.balanceOf(burner); - if (amount > 0) { - revert BurnFailed(); + // Check invariant: the amount of OKB in this contract should be equal to the amount deposited. + if (OKB.balanceOf(address(this)) != _amount) { + revert OKBBalanceMismatch(); + } + + // Burn all OKB from this contract + OKB.triggerBridge(); + + // Check invariant: the amount of OKB in this contract should be zero after burning. + if (OKB.balanceOf(address(this)) > 0) { + revert OKBBalanceNotZeroAfterBurn(); } - // Mint deposit tokens to this contract - _mint(address(this), _amount); // Approve the portal to pull the deposit tokens _approve(address(this), address(PORTAL), _amount); @@ -181,30 +186,13 @@ contract DepositedOKBAdapter is ERC20 { emit Deposited(msg.sender, _to, _amount); } - /// @notice Returns the current burner nonce for creating unique addresses. - /// @return nonce Current nonce value. - function getBurnerNonce() external view returns (uint256 nonce) { - return _burnerNonce; - } - - /// @notice Predicts the address of the next burner contract. - /// @param _user User address. - /// @param _amount Amount to be burned. - /// @return burner Predicted burner address. - function predictBurnerAddress(address _user, uint256 _amount) external view returns (address burner) { - bytes32 salt = keccak256(abi.encode(_user, _amount, block.timestamp, _burnerNonce)); - return Clones.predictDeterministicAddress(BURNER_IMPLEMENTATION, salt, address(this)); - } - /// @notice Override transfer to disable transfers /// This ensures that deposit tokens can only be used by the portal /// and cannot be transferred or traded elsewhere. - /// @param _to Recipient address. - /// @param _amount Amount to transfer. - /// @return bool True if transfer succeeds. - function transfer(address _to, uint256 _amount) public virtual override returns (bool) { + /// @return bool Always reverts. + function transfer(address /* _to */, uint256 /* _amount */) public virtual override returns (bool) { // Do not allow any transfers - revert TransferNotAllowed(_to, _amount); + revert TransferNotAllowed(); } /// @notice Override transferFrom to disable transfers @@ -220,19 +208,27 @@ contract DepositedOKBAdapter is ERC20 { return super.transferFrom(from, to, amount); } - revert TransferNotAllowed(to, amount); + revert TransferNotAllowed(); } - function rescueERC20(address token, address to, uint256 amount) external onlyRescuer { - ERC20(token).transfer(to, amount); - emit ERC20Rescued(token, to, amount); - } + /// @notice Allows owner to rescue ERC20 tokens sent to this contract. + /// @param _token Address of the ERC20 token to rescue. + /// @param _to Address to send the tokens to. + /// @param _amount Amount of tokens to rescue. + function rescueERC20(address _token, address _to, uint256 _amount) external onlyOwner { + if (_token == address(0)) { + revert AddressCannotBeZero(); + } + if (_to == address(0)) { + revert AddressCannotBeZero(); + } + if (_amount == 0) { + revert AmountMustBeGreaterThanZero(); + } - function setRescuer(address _rescuer) external onlyRescuer { - if (_rescuer == address(0)) { - revert RescuerAddressCannotBeZero(); + bool transferSuccess = IERC20(_token).transfer(_to, _amount); + if (!transferSuccess) { + revert TransferFailed(); } - rescuer = _rescuer; - emit RescuerSet(_rescuer); } } diff --git a/packages/contracts-bedrock/src/L1/OKBBurner.sol b/packages/contracts-bedrock/src/L1/OKBBurner.sol deleted file mode 100644 index 4f213676162c6..0000000000000 --- a/packages/contracts-bedrock/src/L1/OKBBurner.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Interfaces -import { IOKB } from "interfaces/L1/IOKB.sol"; - -/// @title OKBBurner -/// @notice Implementation contract for burning OKB tokens via minimal proxy pattern. -/// This contract is designed to be cloned using CREATE2 for deterministic addresses. -/// Each clone burns exactly the OKB tokens it holds and then self-destructs. -/// @dev This contract should never be used directly - only through minimal proxies. -contract OKBBurner { - /// @notice Address of the OKB token contract. - IOKB public immutable OKB; - - /// @notice Emitted when OKB tokens are burned by this burner. - /// @param amount Amount of OKB tokens burned. - event OKBBurned(uint256 amount); - /// @notice Thrown when OKB address is zero. - - error OKBAddressCannotBeZero(); - - /// @notice Constructor sets the OKB token and adapter addresses. - /// @param _okb Address of the OKB token contract. - constructor(address _okb) { - if (_okb == address(0)) { - revert OKBAddressCannotBeZero(); - } - OKB = IOKB(_okb); - } - - /// @notice Burns all OKB tokens held by this contract and self-destructs. - /// @dev This function: - /// 1. Gets the current OKB balance - /// 2. Calls triggerBridge() to burn all tokens - /// 3. Self-destructs to clean up and refund gas - function burnAndDestruct() external { - // Get balance before burning for event emission - uint256 balance = OKB.balanceOf(address(this)); - - // Burn all OKB tokens held by this contract - if (balance > 0) { - OKB.triggerBridge(); - emit OKBBurned(balance); - } - - // Self-destruct and send any remaining ETH to tx.origin - selfdestruct(payable(tx.origin)); - } -} diff --git a/packages/contracts-bedrock/test/L1/DepositedOKBAdapter.t.sol b/packages/contracts-bedrock/test/L1/DepositedOKBAdapter.t.sol new file mode 100644 index 0000000000000..53418aea0f91a --- /dev/null +++ b/packages/contracts-bedrock/test/L1/DepositedOKBAdapter.t.sol @@ -0,0 +1,672 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; +import { Test } from "forge-std/Test.sol"; + +// Contracts +import { DepositedOKBAdapter } from "src/L1/DepositedOKBAdapter.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Libraries +import { Types } from "src/libraries/Types.sol"; +import { GameType } from "src/dispute/lib/LibUDT.sol"; + +// Interfaces +import { IOKB } from "interfaces/L1/IOKB.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; + +/// @title MockOKB +/// @notice Mock OKB contract for testing +contract MockOKB is ERC20, IOKB { + bool public burnTriggered = false; + + constructor(uint256 _totalSupply) ERC20("OKX Token", "OKB") { + _mint(address(this), _totalSupply); + } + + function mint(address _to, uint256 _amount) external { + _mint(_to, _amount); + } + + function triggerBridge() external override { + burnTriggered = true; + // Burn all tokens held by the caller + _burn(msg.sender, balanceOf(msg.sender)); + } + + function resetBurnTriggered() external { + burnTriggered = false; + } +} + +/// @title MockOptimismPortal2 +/// @notice Mock OptimismPortal2 contract for testing +contract MockOptimismPortal2 is IOptimismPortal2 { + struct DepositCall { + address to; + uint256 mint; + uint256 value; + uint64 gasLimit; + bool isCreation; + bytes data; + } + + DepositCall[] public depositCalls; + address public depositToken; + bool public shouldRevert = false; + + function setDepositToken(address _token) external { + depositToken = _token; + } + + function setShouldRevert(bool _shouldRevert) external { + shouldRevert = _shouldRevert; + } + + function depositERC20Transaction( + address _to, + uint256 _mint, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes memory _data + ) + external + override + { + if (shouldRevert) { + revert("MockPortal: forced revert"); + } + + // Simulate portal pulling tokens from the adapter + if (depositToken != address(0)) { + bool success = IERC20(depositToken).transferFrom(msg.sender, address(this), _mint); + require(success, "MockPortal: token transfer failed"); + } + + depositCalls.push( + DepositCall({ + to: _to, + mint: _mint, + value: _value, + gasLimit: _gasLimit, + isCreation: _isCreation, + data: _data + }) + ); + } + + function getDepositCallsLength() external view returns (uint256) { + return depositCalls.length; + } + + function getLastDepositCall() external view returns (DepositCall memory) { + require(depositCalls.length > 0, "No deposit calls"); + return depositCalls[depositCalls.length - 1]; + } + + // Implement other IOptimismPortal2 functions as no-ops for compilation + receive() external payable { } + + function anchorStateRegistry() external pure override returns (IAnchorStateRegistry) { + return IAnchorStateRegistry(address(0)); + } + + function ethLockbox() external pure override returns (IETHLockbox) { + return IETHLockbox(address(0)); + } + + function checkWithdrawal(bytes32, address) external pure override { } + function depositTransaction(address, uint256, uint64, bool, bytes memory) external payable override { } + + function disputeGameBlacklist(IDisputeGame) external pure override returns (bool) { + return false; + } + + function disputeGameFactory() external pure override returns (IDisputeGameFactory) { + return IDisputeGameFactory(address(0)); + } + + function disputeGameFinalityDelaySeconds() external pure override returns (uint256) { + return 0; + } + + function donateETH() external payable override { } + + function superchainConfig() external pure override returns (ISuperchainConfig) { + return ISuperchainConfig(address(0)); + } + + function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory) external pure override { } + function finalizeWithdrawalTransactionExternalProof( + Types.WithdrawalTransaction memory, + address + ) + external + pure + override + { } + + function finalizedWithdrawals(bytes32) external pure override returns (bool) { + return false; + } + + function guardian() external pure override returns (address) { + return address(0); + } + + function initialize(ISystemConfig, IAnchorStateRegistry) external pure override { } + + function initVersion() external pure override returns (uint8) { + return 0; + } + + function l2Sender() external pure override returns (address) { + return address(0); + } + + function minimumGasLimit(uint64) external pure override returns (uint64) { + return 0; + } + + function numProofSubmitters(bytes32) external pure override returns (uint256) { + return 0; + } + + function params() external pure override returns (uint128, uint64, uint64) { + return (0, 0, 0); + } + + function paused() external pure override returns (bool) { + return false; + } + + function proofMaturityDelaySeconds() external pure override returns (uint256) { + return 0; + } + + function proofSubmitters(bytes32, uint256) external pure override returns (address) { + return address(0); + } + + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory, + uint256, + Types.OutputRootProof memory, + bytes[] memory + ) + external + pure + override + { } + + function provenWithdrawals(bytes32, address) external pure override returns (IDisputeGame, uint64) { + return (IDisputeGame(address(0)), 0); + } + + function respectedGameType() external pure override returns (GameType) { + return GameType.wrap(0); + } + + function respectedGameTypeUpdatedAt() external pure override returns (uint64) { + return 0; + } + + function systemConfig() external pure override returns (ISystemConfig) { + return ISystemConfig(address(0)); + } + + function version() external pure override returns (string memory) { + return "1.0.0"; + } + + function __constructor__(uint256) external pure override { } + + function proxiedInterface() external pure returns (IProxyAdminOwnedBase) { + return IProxyAdminOwnedBase(address(0)); + } + + function proxyAdmin() external pure returns (IProxyAdmin) { + return IProxyAdmin(address(0)); + } + + function proxyAdminOwner() external pure returns (address) { + return address(0); + } +} + +/// @title DepositedOKBAdapter_TestInit +/// @notice Test setup contract for DepositedOKBAdapter tests +contract DepositedOKBAdapter_TestInit is CommonTest { + // Events for testing + event WhitelistAdded(address indexed account); + event WhitelistRemoved(address indexed account); + event Deposited(address indexed from, address indexed to, uint256 amount); + + uint256 constant TOTAL_SUPPLY = 21_000_000e18; // 21 million OKB + uint256 constant TEST_AMOUNT = 1000e18; // 1000 OKB + + MockOKB okb; + MockOptimismPortal2 portal; + DepositedOKBAdapter adapter; + address owner; + address user1; + address user2; + + function setUp() public virtual override { + super.setUp(); + + owner = makeAddr("owner"); + user1 = makeAddr("user1"); + user2 = makeAddr("user2"); + + // Deploy mock contracts + okb = new MockOKB(TOTAL_SUPPLY); + portal = new MockOptimismPortal2(); + + // Deploy the adapter + adapter = new DepositedOKBAdapter(address(okb), payable(address(portal)), owner); + + // Set up the portal to accept the adapter as deposit token + portal.setDepositToken(address(adapter)); + + // Give some OKB to test users + okb.mint(user1, TEST_AMOUNT * 10); + okb.mint(user2, TEST_AMOUNT * 5); + + vm.deal(user1, 10 ether); + vm.deal(user2, 10 ether); + } +} + +/// @title DepositedOKBAdapter_Constructor_Test +/// @notice Test contract for DepositedOKBAdapter constructor +contract DepositedOKBAdapter_Constructor_Test is DepositedOKBAdapter_TestInit { + /// @notice Test successful constructor execution + function test_constructor_succeeds() public { + // Check that the adapter was deployed correctly + assertEq(address(adapter.OKB()), address(okb)); + assertEq(address(adapter.PORTAL()), address(portal)); + assertEq(adapter.owner(), owner); + assertEq(adapter.name(), "Deposited OKB"); + assertEq(adapter.symbol(), "dOKB"); + assertEq(adapter.totalSupply(), TOTAL_SUPPLY); + assertEq(adapter.balanceOf(address(adapter)), TOTAL_SUPPLY); + assertEq(adapter.DEFAULT_GAS_LIMIT(), 100_000); + } + + /// @notice Test constructor reverts with zero OKB address + function test_constructor_zeroOKBAddress_reverts() public { + vm.expectRevert(DepositedOKBAdapter.AddressCannotBeZero.selector); + new DepositedOKBAdapter(address(0), payable(address(portal)), owner); + } + + /// @notice Test constructor reverts with zero portal address + function test_constructor_zeroPortalAddress_reverts() public { + vm.expectRevert(DepositedOKBAdapter.AddressCannotBeZero.selector); + new DepositedOKBAdapter(address(okb), payable(address(0)), owner); + } +} + +/// @title DepositedOKBAdapter_WhitelistManagement_Test +/// @notice Test contract for whitelist management functions +contract DepositedOKBAdapter_WhitelistManagement_Test is DepositedOKBAdapter_TestInit { + /// @notice Test adding single address to whitelist + function test_addToWhitelistBatch_single_succeeds() public { + address[] memory accounts = new address[](1); + accounts[0] = user1; + + vm.expectEmit(true, false, false, false); + emit WhitelistAdded(user1); + + vm.prank(owner); + adapter.addToWhitelistBatch(accounts); + + assertTrue(adapter.whitelist(user1)); + } + + /// @notice Test adding multiple addresses to whitelist + function test_addToWhitelistBatch_multiple_succeeds() public { + address[] memory accounts = new address[](2); + accounts[0] = user1; + accounts[1] = user2; + + vm.expectEmit(true, false, false, false); + emit WhitelistAdded(user1); + vm.expectEmit(true, false, false, false); + emit WhitelistAdded(user2); + + vm.prank(owner); + adapter.addToWhitelistBatch(accounts); + + assertTrue(adapter.whitelist(user1)); + assertTrue(adapter.whitelist(user2)); + } + + /// @notice Test adding zero address to whitelist reverts + function test_addToWhitelistBatch_zeroAddress_reverts() public { + address[] memory accounts = new address[](1); + accounts[0] = address(0); + + vm.expectRevert(DepositedOKBAdapter.AddressCannotBeZero.selector); + vm.prank(owner); + adapter.addToWhitelistBatch(accounts); + } + + /// @notice Test non-owner cannot add to whitelist + function test_addToWhitelistBatch_nonOwner_reverts() public { + address[] memory accounts = new address[](1); + accounts[0] = user1; + + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(user1); + adapter.addToWhitelistBatch(accounts); + } + + /// @notice Test removing address from whitelist + function test_removeFromWhitelistBatch_succeeds() public { + // First add to whitelist + address[] memory accounts = new address[](1); + accounts[0] = user1; + vm.prank(owner); + adapter.addToWhitelistBatch(accounts); + + // Then remove + vm.expectEmit(true, false, false, false); + emit WhitelistRemoved(user1); + + vm.prank(owner); + adapter.removeFromWhitelistBatch(accounts); + + assertFalse(adapter.whitelist(user1)); + } + + /// @notice Test removing zero address from whitelist reverts + function test_removeFromWhitelistBatch_zeroAddress_reverts() public { + address[] memory accounts = new address[](1); + accounts[0] = address(0); + + vm.expectRevert(DepositedOKBAdapter.AddressCannotBeZero.selector); + vm.prank(owner); + adapter.removeFromWhitelistBatch(accounts); + } +} + +/// @title DepositedOKBAdapter_Deposit_Test +/// @notice Test contract for deposit functionality +contract DepositedOKBAdapter_Deposit_Test is DepositedOKBAdapter_TestInit { + function setUp() public override { + super.setUp(); + + // Add user1 to whitelist + address[] memory accounts = new address[](1); + accounts[0] = user1; + vm.prank(owner); + adapter.addToWhitelistBatch(accounts); + + // Approve adapter to spend user1's OKB + vm.prank(user1); + okb.approve(address(adapter), type(uint256).max); + } + + /// @notice Test successful deposit + function test_deposit_succeeds() public { + uint256 depositAmount = TEST_AMOUNT; + address target = makeAddr("target"); + + uint256 userBalanceBefore = okb.balanceOf(user1); + uint256 adapterBalanceBefore = adapter.balanceOf(address(adapter)); + + vm.expectEmit(true, true, false, true); + emit Deposited(user1, target, depositAmount); + + vm.prank(user1); + adapter.deposit(target, depositAmount); + + // Check that OKB was burned + assertTrue(okb.burnTriggered()); + assertEq(okb.balanceOf(user1), userBalanceBefore - depositAmount); + assertEq(okb.balanceOf(address(adapter)), 0); // Should be burned + + // Check that portal was called + MockOptimismPortal2.DepositCall memory lastCall = portal.getLastDepositCall(); + assertEq(lastCall.to, target); + assertEq(lastCall.mint, depositAmount); + assertEq(lastCall.value, depositAmount); + assertEq(lastCall.gasLimit, adapter.DEFAULT_GAS_LIMIT()); + assertFalse(lastCall.isCreation); + assertEq(lastCall.data, ""); + + // Check adapter token balance decreased (tokens were transferred to portal) + assertEq(adapter.balanceOf(address(adapter)), adapterBalanceBefore - depositAmount); + } + + /// @notice Test deposit with non-whitelisted user reverts + function test_deposit_notWhitelisted_reverts() public { + vm.expectRevert(DepositedOKBAdapter.NotWhitelisted.selector); + vm.prank(user2); + adapter.deposit(makeAddr("target"), TEST_AMOUNT); + } + + /// @notice Test deposit with zero amount reverts + function test_deposit_zeroAmount_reverts() public { + vm.expectRevert(DepositedOKBAdapter.AmountMustBeGreaterThanZero.selector); + vm.prank(user1); + adapter.deposit(makeAddr("target"), 0); + } + + /// @notice Test deposit with insufficient balance reverts + function test_deposit_insufficientBalance_reverts() public { + uint256 userBalance = okb.balanceOf(user1); + + vm.expectRevert(DepositedOKBAdapter.InsufficientBalance.selector); + vm.prank(user1); + adapter.deposit(makeAddr("target"), userBalance + 1); + } + + /// @notice Test deposit handles existing OKB balance in contract + function test_deposit_handlesExistingOKBBalance_succeeds() public { + uint256 depositAmount = TEST_AMOUNT; + address target = makeAddr("target"); + + // Send some OKB directly to the adapter (simulating mistaken transfer) + vm.prank(user1); + okb.transfer(address(adapter), 100e18); + + uint256 ownerBalanceBefore = okb.balanceOf(owner); + uint256 adapterOKBBefore = okb.balanceOf(address(adapter)); + + vm.prank(user1); + adapter.deposit(target, depositAmount); + + // Check that the existing OKB was transferred to owner + assertEq(okb.balanceOf(owner), ownerBalanceBefore + adapterOKBBefore); + + // Check that the burn was triggered + assertTrue(okb.burnTriggered()); + } +} + +/// @title DepositedOKBAdapter_Transfer_Test +/// @notice Test contract for transfer restrictions +contract DepositedOKBAdapter_Transfer_Test is DepositedOKBAdapter_TestInit { + /// @notice Test regular transfer is not allowed + function test_transfer_notAllowed_reverts() public { + vm.expectRevert(DepositedOKBAdapter.TransferNotAllowed.selector); + vm.prank(user1); + adapter.transfer(user2, 1000); + } + + /// @notice Test transferFrom is not allowed except from adapter to portal + function test_transferFrom_notAllowed_reverts() public { + vm.expectRevert(DepositedOKBAdapter.TransferNotAllowed.selector); + vm.prank(user1); + adapter.transferFrom(user1, user2, 1000); + } + + /// @notice Test transferFrom from adapter to portal is allowed + function test_transferFrom_adapterToPortal_succeeds() public { + uint256 amount = 1000e18; + + // First approve the portal to spend from the adapter + vm.prank(address(adapter)); + adapter.approve(address(portal), amount); + + // This should succeed (simulating portal pulling tokens) + vm.prank(address(portal)); + bool success = adapter.transferFrom(address(adapter), address(portal), amount); + assertTrue(success); + + assertEq(adapter.balanceOf(address(portal)), amount); + assertEq(adapter.balanceOf(address(adapter)), TOTAL_SUPPLY - amount); + } +} + +/// @title DepositedOKBAdapter_Rescue_Test +/// @notice Test contract for ERC20 rescue functionality +contract DepositedOKBAdapter_Rescue_Test is DepositedOKBAdapter_TestInit { + ERC20 testToken; + + function setUp() public override { + super.setUp(); + testToken = new ERC20("Test Token", "TEST"); + // Mint some tokens to the adapter (simulating accidental transfer) + deal(address(testToken), address(adapter), 1000e18); + } + + /// @notice Test successful ERC20 rescue + function test_rescueERC20_succeeds() public { + uint256 rescueAmount = 500e18; + address rescueTo = makeAddr("rescueTo"); + + uint256 balanceBefore = testToken.balanceOf(rescueTo); + + vm.prank(owner); + adapter.rescueERC20(address(testToken), rescueTo, rescueAmount); + + assertEq(testToken.balanceOf(rescueTo), balanceBefore + rescueAmount); + assertEq(testToken.balanceOf(address(adapter)), 1000e18 - rescueAmount); + } + + /// @notice Test rescue with zero token address reverts + function test_rescueERC20_zeroTokenAddress_reverts() public { + vm.expectRevert(DepositedOKBAdapter.AddressCannotBeZero.selector); + vm.prank(owner); + adapter.rescueERC20(address(0), makeAddr("rescueTo"), 100); + } + + /// @notice Test rescue with zero recipient address reverts + function test_rescueERC20_zeroRecipientAddress_reverts() public { + vm.expectRevert(DepositedOKBAdapter.AddressCannotBeZero.selector); + vm.prank(owner); + adapter.rescueERC20(address(testToken), address(0), 100); + } + + /// @notice Test rescue with zero amount reverts + function test_rescueERC20_zeroAmount_reverts() public { + vm.expectRevert(DepositedOKBAdapter.AmountMustBeGreaterThanZero.selector); + vm.prank(owner); + adapter.rescueERC20(address(testToken), makeAddr("rescueTo"), 0); + } + + /// @notice Test non-owner cannot rescue + function test_rescueERC20_nonOwner_reverts() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(user1); + adapter.rescueERC20(address(testToken), makeAddr("rescueTo"), 100); + } +} + +/// @title DepositedOKBAdapter_Integration_Test +/// @notice Integration tests combining multiple functionalities +contract DepositedOKBAdapter_Integration_Test is DepositedOKBAdapter_TestInit { + function setUp() public override { + super.setUp(); + + // Add users to whitelist + address[] memory accounts = new address[](2); + accounts[0] = user1; + accounts[1] = user2; + vm.prank(owner); + adapter.addToWhitelistBatch(accounts); + + // Approve adapter to spend users' OKB + vm.prank(user1); + okb.approve(address(adapter), type(uint256).max); + vm.prank(user2); + okb.approve(address(adapter), type(uint256).max); + } + + /// @notice Test multiple deposits from different users + function test_multipleDeposits_succeeds() public { + address target1 = makeAddr("target1"); + address target2 = makeAddr("target2"); + uint256 amount1 = TEST_AMOUNT; + uint256 amount2 = TEST_AMOUNT / 2; + + // First deposit + vm.prank(user1); + adapter.deposit(target1, amount1); + + // Reset burn trigger for second deposit + okb.resetBurnTriggered(); + + // Second deposit + vm.prank(user2); + adapter.deposit(target2, amount2); + + // Check both deposits were recorded + assertEq(portal.getDepositCallsLength(), 2); + + // Check that both deposits were recorded + assertTrue(portal.getDepositCallsLength() >= 2); + + // We can't easily access individual array elements, so we'll just verify the last call + MockOptimismPortal2.DepositCall memory lastCall = portal.getLastDepositCall(); + assertEq(lastCall.to, target2); + assertEq(lastCall.mint, amount2); + } + + /// @notice Test whitelist management followed by deposits + function test_whitelistManagementThenDeposit_succeeds() public { + address user3 = makeAddr("user3"); + okb.mint(user3, TEST_AMOUNT); + vm.prank(user3); + okb.approve(address(adapter), type(uint256).max); + + // Initially user3 is not whitelisted + vm.expectRevert(DepositedOKBAdapter.NotWhitelisted.selector); + vm.prank(user3); + adapter.deposit(makeAddr("target"), TEST_AMOUNT); + + // Add user3 to whitelist + address[] memory accounts = new address[](1); + accounts[0] = user3; + vm.prank(owner); + adapter.addToWhitelistBatch(accounts); + + // Now deposit should succeed + vm.prank(user3); + adapter.deposit(makeAddr("target"), TEST_AMOUNT); + + // Remove user3 from whitelist + vm.prank(owner); + adapter.removeFromWhitelistBatch(accounts); + + // Deposit should fail again + vm.expectRevert(DepositedOKBAdapter.NotWhitelisted.selector); + vm.prank(user3); + adapter.deposit(makeAddr("target"), TEST_AMOUNT); + } +}