Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "contracts/lib/interop-lib"]
path = contracts/lib/interop-lib
url = https://github.com/ethereum-optimism/interop-lib
[submodule "contracts/lib/v4-template"]
path = contracts/lib/v4-template
url = https://github.com/uniswapfoundation/v4-template
6 changes: 5 additions & 1 deletion contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ remappings = [
"@solady-v0.0.245/=lib/optimism/packages/contracts-bedrock/lib/solady-v0.0.245/src/",
"@solady/=lib/optimism/packages/contracts-bedrock/lib/solady/src/",
"@openzeppelin/contracts=lib/optimism/packages/contracts-bedrock/lib/openzeppelin-contracts/contracts",
"@interop-lib/=lib/interop-lib/src/"
"@interop-lib/=lib/interop-lib/src/",
"@uniswap-v4-template=lib/v4-template",
"@uniswap-v4-core=lib/v4-template/lib/v4-core",
"@uniswap-v4-periphery=lib/v4-template/lib/v4-periphery",
"@uniswap-v4-permit2=lib/v4-template/lib/v4-periphery/lib/permit2",
]

[rpc_endpoints]
Expand Down
1 change: 1 addition & 0 deletions contracts/lib/v4-template
Submodule v4-template added at 634b96
138 changes: 138 additions & 0 deletions contracts/src/ERC20Reference.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";

import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock-interfaces/L2/IL2ToL2CrossDomainMessenger.sol";

/**
* @title ERC20Reference
* @notice An ERC20 that is a remote reference of a native ERC20 on its home chain. This can be thought
* about in the traditional sense of a pointer reference. On the home chain, any user can create
* an acquireable reference to their ERC20 for a given spender, acquired via `transferFrom()`.
* However, all `transfer()` calls incur a "dereference" back to the home chain to natively
* transfer the ERC20 to the desired recipient, backed by Superchain Message Passing. As a result,
* an ERC20 can be remotely controlled by an account without fungible wrapped representations.
*/
contract ERC20Reference is ERC20 {
/// @dev The address of deployer available on all optimism chains used to create the remote representation
address constant _CREATE2_DEPLOYER = address(0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2);

/// @dev The dertministic address of the Permit2 contract
address constant _PERMIT2 = address(0x000000000022D473030F116dDEE9F6B43aC78BA3);

/// @notice the ERC20 token of this remote representation
IERC20 public erc20;

/// @notice the chain the ERC20 lives on
uint256 public homeChainId;

/// @notice the remote chain that can hold a lock on this ERC20
uint256 public remoteChainId;

/// @notice the account allowed to acquire this lock from the user via `transferFrom()`
address public spender;

/// @dev The messenger predeploy to handle message passing
IL2ToL2CrossDomainMessenger internal _messenger =
IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);

/// @notice The constructor.
/// @param _homeChainId The chain the ERC20 lives on
/// @param _erc20 The ERC20 token this remote representation is based on
/// @param _remoteChainId The chain this erc20 is controlled by
/// @param _spender The account allowed to acquire this lock from the user via `transferFrom()`
constructor(uint256 _homeChainId, IERC20 _erc20, uint256 _remoteChainId, address _spender)
ERC20("ERC20Reference", "ERC20Ref")
{
// By asserting the deployer is used, we obtain good safety that
// 1. This contract was deterministically created based on the constructor args
// 2. `approve()` & `transfer()` only works on the correctly erc20 address (constructor arg)
require(msg.sender == _CREATE2_DEPLOYER);

homeChainId = _homeChainId;
erc20 = _erc20;
remoteChainId = _remoteChainId;
spender = _spender;
}

/// @notice Approve a spender on the remote to pull an amount of ERC20Reference
/// @param _spender The address of the spender on the remote chain
/// @param _amount The amount to approve
/// @return success True if the approval was successful
function approve(address _spender, uint256 _amount) public override returns (bool) {
require(block.chainid == homeChainId);
require(_spender == spender);

// (1) Escrow the ERC20
erc20.transferFrom(msg.sender, address(this), _amount);

// (2) Send a message to approve the spender over the new reference
bytes memory call = abi.encodeCall(this.handleApproval, (msg.sender, _spender, _amount));
_messenger.sendMessage(remoteChainId, address(this), call);
return true;
}

/// @notice Handle an approval, creating a reference to the new amount, approved for the spender.
/// @param _owner The owner of the ERC20Reference
/// @param _spender The spender of the ERC20Reference
/// @param _amount The amount to approve
function handleApproval(address _owner, address _spender, uint256 _amount) external {
require(block.chainid == remoteChainId);
require(msg.sender == address(_messenger));

// (1) Call must have come from the this RemoteERC20
address sender = _messenger.crossDomainMessageSender();
require(sender == address(this));

// (2) Mint the ERC20 to the original owner (re-used _sender argument)
super._mint(_owner, _amount);

// (3) Manually set the allowance over the lock for the spender.
// For contracts that pull token via Permit2, we also approve
// the Permit2 contract with the same amount
super._approve(_owner, _spender, _amount);
super._approve(_owner, _PERMIT2, _amount);
}

/// @notice Transfer the approved ERC20Reference to the spender.
/// @param _from The sender
/// @param _to The recipient
/// @param _amount The amount
/// @return success True if the transfer was successful
function transferFrom(address _from, address _to, uint256 _amount) public override returns (bool) {
require(block.chainid == remoteChainId, "Acquirable only on the remote");
return super.transferFrom(_from, _to, _amount);
}

/// @notice Transfer the ERC20Reference. This burns the specified amount of this reference, and natively
/// transfers the ERC20 to the recipient on the home chain
/// @param _to The recipient
/// @param _amount The amount
/// @return success True if the transfer was successful
function transfer(address _to, uint256 _amount) public override returns (bool) {
if (block.chainid == remoteChainId) {
// (1) Burn the held reference (either the original owner or the spender)
// @note: If this is the original owner and not the spender, they will still have the allowance set
// on the sender. However, the user would have to manually approve again for there to be any
// tokens for the spender to pull so it is fine for this allowance to remain.
super._burn(msg.sender, _amount);

// (1) Send a message to transfer the held value
_messenger.sendMessage(homeChainId, address(this), abi.encodeCall(this.transfer, (_to, _amount)));
return true;
} else {
require(msg.sender == address(_messenger));

// (1) Call must have come from the this ERC20Reference
address sender = _messenger.crossDomainMessageSender();
require(sender == address(this));

// (2) Unlock the ERC20 to the recipient
return erc20.transfer(_to, _amount);
}
}
}
116 changes: 116 additions & 0 deletions contracts/test/ERC20Reference.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {Test} from "forge-std/Test.sol";
import {StdUtils} from "forge-std/StdUtils.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";

import {Relayer} from "@interop-lib/test/Relayer.sol";
import {CrossL2Inbox} from "@contracts-bedrock/L2/CrossL2Inbox.sol";
import {L2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/L2ToL2CrossDomainMessenger.sol";

import {ERC20Reference} from "../src/ERC20Reference.sol";

interface ICreate2Deployer {
function deploy(uint256 value, bytes32 salt, bytes memory code) external;
function computeAddress(bytes32 salt, bytes32 codeHash) external view returns (address);
}

contract ERC20ReferenceTest is StdUtils, Test, Relayer {
ICreate2Deployer public deployer = ICreate2Deployer(0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2);

bytes32 public salt = bytes32(0);

ERC20 public erc20;
ERC20Reference public remoteERC20;

// Run against supersim locally so forking is fast
constructor() Relayer("http://127.0.0.1:9545", "http://127.0.0.1:9546") {}

function spender() public virtual returns (address) {
return address(0x1);
}

function setUp() public virtual {
// ERC20 only exists on A
vm.selectFork(chainA);
erc20 = new ERC20("ERC20", "ERC20");

// home chain is A, remotely controlled by the spender on B
bytes memory args = abi.encode(chainIdByForkId[chainA], address(erc20), chainIdByForkId[chainB], spender());
bytes memory remoteERC20CreationCode = abi.encodePacked(type(ERC20Reference).creationCode, args);
remoteERC20 = ERC20Reference(deployer.computeAddress(salt, keccak256(remoteERC20CreationCode)));

// Setup Remote on A
deployer.deploy(0, salt, remoteERC20CreationCode);
vm.etch(Predeploys.CROSS_L2_INBOX, address(new CrossL2Inbox()).code);
vm.etch(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, address(new L2ToL2CrossDomainMessenger()).code);

// Setup Remote on B
vm.selectFork(chainB);
deployer.deploy(0, salt, remoteERC20CreationCode);
vm.etch(Predeploys.CROSS_L2_INBOX, address(new CrossL2Inbox()).code);
vm.etch(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, address(new L2ToL2CrossDomainMessenger()).code);
}

function test_approve() public {
vm.selectFork(chainA);
vm.assume(erc20.balanceOf(address(this)) == 0);

deal(address(erc20), address(this), 1e18);
assertEq(erc20.balanceOf(address(this)), 1e18);

// Approve
vm.startPrank(address(this));
erc20.approve(address(remoteERC20), 1e18);
remoteERC20.approve(spender(), 1e18);
assertEq(erc20.balanceOf(address(this)), 0);

// Check allowance
relayAllMessages();
vm.selectFork(chainB);
assertEq(remoteERC20.balanceOf(address(this)), 1e18);
assertEq(remoteERC20.balanceOf(spender()), 0);
assertEq(remoteERC20.allowance(address(this), spender()), 1e18);

vm.stopPrank();
}

function test_transferFrom() public {
test_approve();

vm.selectFork(chainB);

// Claim approval
vm.startPrank(spender());
remoteERC20.transferFrom(address(this), spender(), 1e18);

assertEq(remoteERC20.balanceOf(address(this)), 0);
assertEq(remoteERC20.balanceOf(spender()), 1e18);
assertEq(remoteERC20.allowance(address(this), spender()), 0);

vm.stopPrank();
}

function test_transfer() public {
test_transferFrom();

vm.selectFork(chainB);

// Transfer back to the holder (remote tokens burned)
vm.startPrank(spender());
remoteERC20.transfer(address(this), 1e18);
assertEq(remoteERC20.balanceOf(spender()), 0);
assertEq(remoteERC20.balanceOf(address(this)), 0);
vm.stopPrank();

// Tokens only transferred on the home chain.
relayAllMessages();
vm.selectFork(chainA);
assertEq(erc20.balanceOf(address(this)), 1e18);
}
}
120 changes: 120 additions & 0 deletions contracts/test/ERC20ReferenceUniswapV4.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";

import {StdUtils} from "forge-std/StdUtils.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {Relayer} from "@interop-lib/test/Relayer.sol";
import {CrossL2Inbox} from "@contracts-bedrock/L2/CrossL2Inbox.sol";
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";
import {L2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/L2ToL2CrossDomainMessenger.sol";

import {Currency, CurrencyLibrary} from "@uniswap-v4-core/src/types/Currency.sol";
import {IHooks} from "@uniswap-v4-core/src/interfaces/IHooks.sol";
import {IPoolManager} from "@uniswap-v4-core/src/interfaces/IPoolManager.sol";
import {IPositionManager} from "@uniswap-v4-periphery/src/interfaces/IPositionManager.sol";
import {PoolId, PoolIdLibrary} from "@uniswap-v4-core/src/types/PoolId.sol";
import {PoolKey} from "@uniswap-v4-core/src/types/PoolKey.sol";
import {TickMath} from "@uniswap-v4-core/src/libraries/TickMath.sol";
import {LiquidityAmounts} from "@uniswap-v4-core/test/utils/LiquidityAmounts.sol";
import {StateLibrary} from "@uniswap-v4-core/src/libraries/StateLibrary.sol";
import {EasyPosm} from "@uniswap-v4-template/test/utils/EasyPosm.sol";

import {ERC20Reference} from "../src/ERC20Reference.sol";

import {UniswapFixtures} from "./UniswapFixtures.t.sol";
import {ERC20ReferenceTest} from "./ERC20Reference.t.sol";

contract ERC20ReferenceUniswapV4Test is Test, ERC20ReferenceTest, UniswapFixtures {
using EasyPosm for IPositionManager;
using PoolIdLibrary for PoolKey;
using CurrencyLibrary for Currency;
using StateLibrary for IPoolManager;

PoolKey poolKey;
PoolId poolId;

// Chain A (Remote), Chain B (Home)
constructor() ERC20ReferenceTest() {}

function spender() public view override returns (address) {
return address(posm);
}

function setUp() public override {
// The v4 pool only exists on the remote chain with no erc20 (B)
vm.selectFork(chainB);

// creates the pool manager, utility routers, and test tokens
deployFreshManagerAndRouters();
deployPosm(manager);

// Setup ERC20Reference. Setup after v4 deployment since
// the POSM (uniswap v4) is going to be the approved spender
super.setUp();

// setup the create eth/erc20 pool
poolKey = PoolKey(Currency.wrap(address(0)), Currency.wrap(address(remoteERC20)), 3000, 60, IHooks(address(0)));
poolId = poolKey.toId();
manager.initialize(poolKey, SQRT_PRICE_1_1);
}

function test_poolSetup() public {
// Provide full-range liquidity to the pool
int24 tickLower = TickMath.minUsableTick(poolKey.tickSpacing);
int24 tickUpper = TickMath.maxUsableTick(poolKey.tickSpacing);

uint128 liquidityAmount = 100e18;
(uint256 amount0Expected, uint256 amount1Expected) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
liquidityAmount
);

// Deal erc20 on the home chain & Approve Uniswap
vm.selectFork(chainA);
deal(address(erc20), address(this), amount1Expected + 1);
erc20.approve(address(remoteERC20), amount1Expected + 1);
remoteERC20.approve(address(posm), amount1Expected + 1);
relayAllMessages();

// On remote, approve permit2 for the posm (could also just be a signature)
vm.selectFork(chainB);
vm.deal(address(this), amount0Expected + 1);
permit2.approve(address(remoteERC20), address(posm), type(uint160).max, type(uint48).max);
posm.mint(
poolKey,
tickLower,
tickUpper,
liquidityAmount,
amount0Expected + 1,
amount1Expected + 1,
address(this),
block.timestamp,
ZERO_BYTES
);
}

function test_swap() public {
test_poolSetup();

// No Balance on the chain with the native ERC20
vm.selectFork(chainA);
assertEq(erc20.balanceOf(address(this)), 0);

// Swap in ETH on chain with the v4 Pool
vm.selectFork(chainB);
vm.deal(address(this), 1 ether);
swap(poolKey, true, -1 ether, ZERO_BYTES);
relayAllMessages();

// THERE IS A NATIVE BALANCE (output of the swap)
vm.selectFork(chainA);
assertGt(erc20.balanceOf(address(this)), 0);
}
}
Loading