<aside> ⚠️ This is a work-in-progress, a highly experimental contract, this should NOT be used in production or treated as a “valid” contract by any means.

</aside>

We added the following to the ideas from PR-71:

pragma solidity 0.8.23;

contract OptimismSuperchainERC20 is ERC20 {
  // TODO: discuss if order is the most clean/optimal
  // fix naming and ordering of variables to match ERC20 standard (owner > spender > recipient)
  mapping(address account => mapping(address spender => mapping(uint256 chainId => uint256 amount))) private
    _xAllowances;

  event xApproval(address indexed owner, address indexed spender, uint256 indexed chainId, uint256 amount);
  event Relay(address indexed owner, address indexed recipient, address indexed spender, uint256 chainId, bytes data);
  event RelayAndExecute(
    address indexed owner, address indexed recipient, address indexed spender, uint256 originChainId, uint256 spenderChainId, uint256 amount, bytes data
  );
  event RelayTransferFrom(
    address indexed owner,
    address indexed recipient,
    address indexed spender,
    uint256 originChainId,
    uint256 executeChainId,
    uint256 amount,
    bytes data
  );

  constructor(string memory _name, string memory _symbol, uint8 _decimals) ERC20(_name, _symbol, _decimals) {}

  /*//////////////////////////////////////////////////
    Functions for when funds are in initiating chain
  ////////////////////////////////////////////////////*/

  function xTransfer(uint256 _amount, uint256 _chainId) external returns (bool success) {
    success = xTransferTo(msg.sender, _amount, _chainId);
  }

  function xTransferTo(address _to, uint256 _amount, uint256 _chainId) public returns (bool) {
    _burn(msg.sender, _amount);
    bytes memory _message = abi.encodeCall(this.xReceive, (_to, _amount));
    _sendMessage(_chainId, _message);
    return true;
  }
  
  // xApproval should ONLY be called LOCALLY to be used on relayTransferFrom
  // xApproval should be on the domain where the assets live.
  function xApprove(address _spender, uint256 _chainId, uint256 _amount) external returns (bool) {
    // TODO [research] _chainId wildcard as type(uint256).max (this is very risky)
    _xAllowances[msg.sender][_spender][_chainId] = _amount;
    emit xApproval(msg.sender, _spender, _chainId, _amount);
    return true;
  }
  
  function allowance(address _owner, address _spender) external view returns (uint256) {
    return allowance(_owner, _spender, block.chainid);
  }

  function allowance(address _owner, address _spender, uint256 _chainId) external view returns (uint256) {
    if (_chainId == block.chainId) return _allowance[_owner][_spender];
    return _xAllowance[_owner][_spender][_chainId];
  }

  /*/////////////////////////////////////////////
    Functions for when funds are in remote chain
  /////////////////////////////////////////////*/

  function remoteTransferFrom(
    address _owner,
    address _recipient,
    uint256 _spendChainId,
    uint256 _executeChainId,
    uint256 _amount,
    bytes memory _data
  ) external returns (bool) {
    bytes memory _message =
      abi.encodeCall(this.relayTransferFrom, (_owner, msg.sender, _recipient, block.chainid, _executionChainId, _amount, _data));
    _sendMessage(_spendChainId, _message);
    return true;
  }

  /*/////////////////////////////////////////////
    Functions to handle executing messages
  /////////////////////////////////////////////*/
  function xReceive(address _to, uint256 _amount) external OnlyPredeploys {
    _mint(_to, _amount);
  }

  function relayTransferFrom(
    address _owner,
    address _spender,
    address _recipient,
    uint256 _originChainId,
    uint256 _executionChainId,
    uint256 _amount,
    bytes memory _data
  ) external OnlyPredeploys {
    uint256 allowed = _xAllowances[_owner][_spender][_originChainId];

    if (allowed != type(uint256).max) {
      _xAllowances[_owner][_spender][_originChainId] = allowed - _amount;
    }

    _burn(_owner, _amount);
    
    if (block.chain == _executionChainId){
      _relayAndExecute(_owner, _recipient, _spender, _originChainId, block.chainid, _amount, _data);
    } else {
      bytes memory _message =
      abi.encodeCall(this.relayAndExecute, (_owner, _recipient, _spender, _originChainId, block.chainid, _amount, _data));
      _sendMessage(_executionChainId, _message);
    }
    
    emit RelayTransferFrom(_owner, _recipient, _spender, _originChainId, _executeChainId, _amount, _data);
  }

  function relayAndExecute(
    address _owner,
    address _recipient,
    address _spender,
    uint256 _originChainId,
    uint256 _spenderChainId,
    uint256 _amount,
    bytes memory _data
  ) external OnlyPredeploys {
    _relayAndExecute(_owner, _recipient, _spender, _originChainId, _spenderChainId, _amount, _data);
  }
  
  function _relayAndExecute(
    address _owner,
    address _recipient,
    address _spender,
    uint256 _originChainId,
    uint256 _spenderChainId,
    uint256 _amount,
    bytes memory _data
  ) external {
    _mint(_recipient, _amount);
    if (_data.length > 0) {
      IxCallback(_spender).xCallback(_owner, _recipient, _spender, _originChainId, _spenderChainId, _amount, _data);
    }
    emit RelayAndExecute(_owner, _recipient, _spender, _originChainId, _spenderChainId, _amount, _data);
  }

  function _sendMessage(uint256 _destination, bytes memory _data) internal virtual {
    // this assumes the token is deployed in the same address
    // in every chain.
    // TODO: check if this is possible
    L2ToL2CrossDomainMessenger.sendMessage(_destination, address(this), _data);
  }

  // TODO: think of a better name for this
  modifier OnlyPredeploys() virtual {
    require(msg.sender == address(L2ToL2CrossChainMessenger));
    // this matches above assumption, so TODO: check.
    require(L2ToL2CrossChainMessenger.crossDomainMessageSender() == address(this));
    _;
  }
}