Skip to content

Commit

Permalink
improve(OVM_SpokePool): Use bridgeERC20To to bridge native L2 tokens …
Browse files Browse the repository at this point in the history
…if admin decides to support them

bridgeERC20To can be used to bridge native L2 tokens (without L1 counterparts and specifically does not contain a `l1Token()` function in the L2 erc20 contract) if an L1 remote token is identified.

This call require the spoke pool to set an allowance on the standard bridge before calling `bridgeERC20To`
  • Loading branch information
nicholaspai committed Jun 13, 2024
1 parent 417c595 commit 3d09986
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 7 deletions.
45 changes: 39 additions & 6 deletions contracts/Ovm_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface IL2ERC20Bridge {
* @notice OVM specific SpokePool. Uses OVM cross-domain-enabled logic to implement admin only access to functions. * Optimism, Base, and Boba each implement this spoke pool and set their chain specific contract addresses for l2Eth and l2Weth.
*/
contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter {
using SafeERC20 for IERC20;
// "l1Gas" parameter used in call to bridge tokens from this contract back to L1 via IL2ERC20Bridge. Currently
// unused by bridge but included for future compatibility.
uint32 public l1Gas;
Expand All @@ -54,8 +55,14 @@ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter {
// to support non-standard ERC20 tokens on Optimism, such as DIA and SNX which both use custom bridges.
mapping(address => address) public tokenBridges;

// Stores mapping of L2 tokens to L1 equivalent tokens. If a mapping is defined for a given L2 token, then
// the mapped L1 token can be used in _bridgeTokensToHubPool which can then call bridgeERC20To, which
// requires specfiying an L1 token.
mapping(address => address) public remoteL1Tokens;

event SetL1Gas(uint32 indexed newL1Gas);
event SetL2TokenBridge(address indexed l2Token, address indexed tokenBridge);
event SetRemoteL1Token(address indexed l2Token, address indexed l1Token);

error NotCrossDomainAdmin();

Expand Down Expand Up @@ -105,6 +112,11 @@ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter {
emit SetL1Gas(newl1Gas);
}

function setRemoteL1Token(address l2Token, address l1Token) public onlyAdmin nonReentrant {
remoteL1Tokens[l2Token] = l1Token;
emit SetRemoteL1Token(l2Token, l1Token);
}

/**
* @notice Set bridge contract for L2 token used to withdraw back to L1.
* @dev If this mapping isn't set for an L2 token, then the standard bridge will be used to bridge this token.
Expand Down Expand Up @@ -154,22 +166,43 @@ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter {
else if (_isCCTPEnabled() && l2TokenAddress == address(usdcToken)) {
_transferUsdc(hubPool, amountToReturn);
}
// Note we'll always use withdrawTo instead of bridgeERC20To here because we can assume
// Note we'll default use withdrawTo instead of bridgeERC20To here because we can assume
// we'll only bridge back L2 tokens that are "non-native", i.e. they have a canonical L1 token
// that maps to this L2. If we wanted to bridge "native L2" tokens we'd need to call
// bridgeERC20To and give allowance to the tokenBridge to spend l2Token from this contract.
else
IL2ERC20Bridge(
// bridgeERC20To and give allowance to the tokenBridge to spend l2Token from this contract. We'd
// also need to know the L1 equivalent token address.
else {
IL2ERC20Bridge tokenBridge = IL2ERC20Bridge(
tokenBridges[l2TokenAddress] == address(0)
? Lib_PredeployAddresses.L2_STANDARD_BRIDGE
: tokenBridges[l2TokenAddress]
).withdrawTo(
);
if (remoteL1Tokens[l2TokenAddress] != address(0)) {
// If there is a mapping for this L2 token to an L1 token, then use the L1 token address and
// call bridgeERC20To.
IERC20(l2TokenAddress).safeIncreaseAllowance(address(tokenBridge), amountToReturn);
address remoteL1Token = remoteL1Tokens[l2TokenAddress];
tokenBridge.bridgeERC20To(
l2TokenAddress, // _l2Token. Address of the L2 token to bridge over.
remoteL1Token, // Remote token to be received on l1 side. If the
// remoteL1Token on the other chain does not recognize the local token as the correct
// pair token, the ERC20 bridge will fail and the tokens will be returned to sender on
// this chain.
hubPool, // _to
amountToReturn, // _amount
l1Gas, // _l1Gas
"" // _data
);
} else {
tokenBridge.withdrawTo(
l2TokenAddress, // _l2Token. Address of the L2 token to bridge over.
hubPool, // _to. Withdraw, over the bridge, to the l1 pool contract.
amountToReturn, // _amount.
l1Gas, // _l1Gas. Unused, but included for potential forward compatibility considerations
"" // _data. We don't need to send any data for the bridging action.
);
}
}
}

// Apply OVM-specific transformation to cross domain admin address on L1.
Expand All @@ -180,5 +213,5 @@ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter {
// Reserve storage slots for future versions of this base contract to add state variables without
// affecting the storage layout of child contracts. Decrement the size of __gap whenever state variables
// are added. This is at bottom of contract to make sure its always at the end of storage.
uint256[1000] private __gap;
uint256[999] private __gap;
}
2 changes: 2 additions & 0 deletions contracts/test/MockBedrockStandardBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ contract MockBedrockL2StandardBridge is IL2ERC20Bridge {
uint256 _minGasLimit,
bytes calldata _extraData
) external {
// Check that caller has approved this contract to pull funds, mirroring mainnet's behavior
IERC20(_localToken).transferFrom(msg.sender, address(this), _amount);
// do nothing
}
}
34 changes: 33 additions & 1 deletion test/chain-specific-spokepools/Optimism_SpokePool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mockTreeRoot, amountToReturn, amountHeldByPool } from "../constants";
import { mockTreeRoot, amountToReturn, amountHeldByPool, zeroAddress } from "../constants";
import {
ethers,
expect,
Expand Down Expand Up @@ -129,6 +129,15 @@ describe("Optimism Spoke Pool", function () {
expect((await optimismSpokePool.rootBundles(0)).relayerRefundRoot).to.equal(ethers.utils.hexZeroPad("0x0", 32));
});

it("Only owner can set a remote L1 token", async function () {
expect(await optimismSpokePool.remoteL1Tokens(l2Dai)).to.equal(zeroAddress);
await expect(optimismSpokePool.setRemoteL1Token(l2Dai, rando.address)).to.be.reverted;
crossDomainMessenger.xDomainMessageSender.returns(owner.address);
await expect(optimismSpokePool.connect(crossDomainMessenger.wallet).setRemoteL1Token(l2Dai, rando.address)).to.not
.be.reverted;
expect(await optimismSpokePool.remoteL1Tokens(l2Dai)).to.equal(rando.address);
});

it("Bridge tokens to hub pool correctly calls the Standard L2 Bridge for ERC20", async function () {
const { leaves, tree } = await constructSingleRelayerRefundTree(
l2Dai,
Expand All @@ -142,6 +151,29 @@ describe("Optimism Spoke Pool", function () {
expect(l2StandardBridge.withdrawTo).to.have.been.calledOnce;
expect(l2StandardBridge.withdrawTo).to.have.been.calledWith(l2Dai, hubPool.address, amountToReturn, 5000000, "0x");
});
it("If remote L1 token is set for native L2 token, then bridge calls bridgeERC20To instead of withdrawTo", async function () {
const { leaves, tree } = await constructSingleRelayerRefundTree(
dai.address,
await optimismSpokePool.callStatic.chainId()
);
crossDomainMessenger.xDomainMessageSender.returns(owner.address);

// If we set a remote L1 token for the native L2 token, then the bridge should call bridgeERC20To instead of withdrawTo
await optimismSpokePool.connect(crossDomainMessenger.wallet).setRemoteL1Token(dai.address, rando.address);
await optimismSpokePool.connect(crossDomainMessenger.wallet).relayRootBundle(tree.getHexRoot(), mockTreeRoot);
await optimismSpokePool.connect(relayer).executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]));

// This should have sent tokens back to L1. Check the correct methods on the gateway are correctly called.
expect(l2StandardBridge.bridgeERC20To).to.have.been.calledOnce;
expect(l2StandardBridge.bridgeERC20To).to.have.been.calledWith(
dai.address,
rando.address,
hubPool.address,
amountToReturn,
5000000,
"0x"
);
});
it("Bridge tokens to hub pool correctly calls an alternative L2 Gateway router", async function () {
const { leaves, tree } = await constructSingleRelayerRefundTree(
l2Dai,
Expand Down

0 comments on commit 3d09986

Please sign in to comment.