Skip to content
Merged
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
49 changes: 40 additions & 9 deletions contracts/PolygonTokenBridger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import "./interfaces/WETH9.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

// Polygon Registry contract that stores their addresses.
interface PolygonRegistry {
function erc20Predicate() external returns (address);
}

// Polygon ERC20Predicate contract that handles Plasma exits (only used for Matic).
interface PolygonERC20Predicate {
function startExitWithBurntTokens(bytes calldata data) external;
}

// ERC20s (on polygon) compatible with polygon's bridge have a withdraw method.
interface PolygonIERC20 is IERC20 {
function withdraw(uint256 amount) external;
Expand Down Expand Up @@ -38,6 +48,9 @@ contract PolygonTokenBridger is Lockable {
// Should be set to HubPool on Ethereum, or unused on Polygon.
address public immutable destination;

// Registry that stores L1 polygon addresses.
PolygonRegistry public immutable l1PolygonRegistry;

// WETH contract on Ethereum.
WETH9 public immutable l1Weth;

Expand All @@ -59,15 +72,21 @@ contract PolygonTokenBridger is Lockable {
/**
* @notice Constructs Token Bridger contract.
* @param _destination Where to send tokens to for this network.
* @param _l1Weth Ethereum WETH address.
* @param _l1PolygonRegistry L1 registry that stores updated addresses of polygon contracts. This should always be
* set to the L1 registry regardless if whether it's deployed on L2 or L1.
* @param _l1Weth L1 WETH address.
* @param _l1ChainId the chain id for the L1 in this environment.
* @param _l2ChainId the chain id for the L2 in this environment.
*/
constructor(
address _destination,
PolygonRegistry _l1PolygonRegistry,
WETH9 _l1Weth,
uint256 _l1ChainId,
uint256 _l2ChainId
) {
destination = _destination;
l1PolygonRegistry = _l1PolygonRegistry;
l1Weth = _l1Weth;
l1ChainId = _l1ChainId;
l2ChainId = _l2ChainId;
Expand Down Expand Up @@ -99,18 +118,30 @@ contract PolygonTokenBridger is Lockable {
* @param token Token to send to destination.
*/
function retrieve(IERC20 token) public nonReentrant onlyChainId(l1ChainId) {
if (address(token) == address(l1Weth)) {
// For WETH, there is a pre-deposit step to ensure any ETH that has been sent to the contract is captured.
l1Weth.deposit{ value: address(this).balance }();
}
token.safeTransfer(destination, token.balanceOf(address(this)));
}

/**
* @notice Called to initiate an l1 exit (withdrawal) of matic tokens that have been sent over the plasma bridge.
* @param data the proof data to trigger the exit. Can be generated using the maticjs-plasma package.
*/
function callExit(bytes memory data) public nonReentrant onlyChainId(l1ChainId) {
PolygonERC20Predicate erc20Predicate = PolygonERC20Predicate(l1PolygonRegistry.erc20Predicate());
erc20Predicate.startExitWithBurntTokens(data);
}

receive() external payable {
if (functionCallStackOriginatesFromOutsideThisContract()) {
// This should only happen on the mainnet side where ETH is sent to the contract directly by the bridge.
_requireChainId(l1ChainId);
l1Weth.deposit{ value: address(this).balance }();
} else {
// This should only happen on the l2 side where matic is unwrapped by this contract.
_requireChainId(l2ChainId);
}
// This method is empty to avoid any gas expendatures that might cause transfers to fail.
// Note: the fact that there is _no_ code in this function means that matic can be erroneously transferred in
// to the contract on the polygon side. These tokens would be locked indefinitely since the receive function
// cannot be called on the polygon side. While this does have some downsides, the lack of any functionality
// in this function means that it has no chance of running out of gas on transfers, which is a much more
// important benefit. This just makes the matic token risk similar to that of ERC20s that are erroneously
// sent to the contract.
}

function _requireChainId(uint256 chainId) internal view {
Expand Down
79 changes: 79 additions & 0 deletions contracts/Polygon_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,80 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool {
require(success, "delegatecall failed");
}

/**
* @notice Allows the caller to trigger the wrapping of any unwrapped matic tokens.
* @dev Matic sends via L1 -> L2 bridging actions don't call into the contract receiving the tokens, so wrapping
* must be done via a separate transaction.
*/
function wrap() public nonReentrant {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should wrap be triggered on any other external functions like processMessageFromRoot for convenience?

Copy link
Contributor Author

@mrice32 mrice32 Apr 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could. Note: the functions that actually would need the tokens aren't triggered by processMessageFromRoot, though. Executing relayer refunds and slow relays are the ones that would require this, but to add it to those, they might need to be overridden.

Will think a little about how to do this with little overhead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this functionality in the last commit: 811ac20. PTAL.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah perfect this is now consistent with how Optimism_SpokePool handles the same logic: https://github.com/across-protocol/contracts-v2/blob/master/contracts/Optimism_SpokePool.sol#L89

HOWEVER, you can probably save some gas by adding a check if (destinationToken == weth) ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really think this extra check is worthwhile because:

  1. We already check address(this).balance > 0, which is extremely cheap. It costs 5 gas according to https://ethereum.org/en/developers/docs/evm/opcodes/ to read the balance. Reading the weth value from the contract, by comparison, would cost 100 - 2100 gas. I think these checks are the only difference between the two, since we will likely run some matic execution step each time matic tokens are sent over to the spoke, meaning the number of wraps shouldn't really differ between the two.
  2. Since this is on polygon, gas costs shouldn't really matter too much.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline, we'll leave this as is.

_wrap();
}

/**
* @notice Executes a relayer refund leaf stored as part of a root bundle. Will send the relayer the amount they
* sent to the recipient plus a relayer fee.
* @dev this is only overridden to wrap any matic the contract holds before running.
* @param rootBundleId Unique ID of root bundle containing relayer refund root that this leaf is contained in.
* @param relayerRefundLeaf Contains all data necessary to reconstruct leaf contained in root bundle and to
* refund relayer. This data structure is explained in detail in the SpokePoolInterface.
* @param proof Inclusion proof for this leaf in relayer refund root in root bundle.
*/
function executeRelayerRefundLeaf(
uint32 rootBundleId,
SpokePoolInterface.RelayerRefundLeaf memory relayerRefundLeaf,
bytes32[] memory proof
) public override nonReentrant {
_wrap();
_executeRelayerRefundLeaf(rootBundleId, relayerRefundLeaf, proof);
}

/**
* @notice Executes a slow relay leaf stored as part of a root bundle. Will send the full amount remaining in the
* relay to the recipient, less fees.
* @dev This function assumes that the relay's destination chain ID is the current chain ID, which prevents
* the caller from executing a slow relay intended for another chain on this chain. This is only overridden to call
* wrap before running the function.
* @param depositor Depositor on origin chain who set this chain as the destination chain.
* @param recipient Specified recipient on this chain.
* @param destinationToken Token to send to recipient. Should be mapped to the origin token, origin chain ID
* and this chain ID via a mapping on the HubPool.
* @param amount Full size of the deposit.
* @param originChainId Chain of SpokePool where deposit originated.
* @param realizedLpFeePct Fee % based on L1 HubPool utilization at deposit quote time. Deterministic based on
* quote time.
* @param relayerFeePct Original fee % to keep as relayer set by depositor.
* @param depositId Unique deposit ID on origin spoke pool.
* @param rootBundleId Unique ID of root bundle containing slow relay root that this leaf is contained in.
* @param proof Inclusion proof for this leaf in slow relay root in root bundle.
*/
function executeSlowRelayLeaf(
address depositor,
address recipient,
address destinationToken,
uint256 amount,
uint256 originChainId,
uint64 realizedLpFeePct,
uint64 relayerFeePct,
uint32 depositId,
uint32 rootBundleId,
bytes32[] memory proof
) public virtual override nonReentrant {
_wrap();
_executeSlowRelayLeaf(
depositor,
recipient,
destinationToken,
amount,
originChainId,
chainId(),
realizedLpFeePct,
relayerFeePct,
depositId,
rootBundleId,
proof
);
}

/**************************************
* INTERNAL FUNCTIONS *
**************************************/
Expand All @@ -147,6 +221,11 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool {
emit PolygonTokensBridged(relayerRefundLeaf.l2TokenAddress, address(this), relayerRefundLeaf.amountToReturn);
}

function _wrap() internal {
uint256 balance = address(this).balance;
if (balance > 0) wrappedNativeToken.deposit{ value: balance }();
}

// @dev: This contract will trigger admin functions internally via the `processMessageFromRoot`, which is why
// the `callValidated` check is made below and why we use the `validateInternalCalls` modifier on
// `processMessageFromRoot`. This prevents calling the admin functions from any other method besides
Expand Down
29 changes: 26 additions & 3 deletions contracts/chain-adapters/Polygon_Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ interface IFxStateSender {
function sendMessageToChild(address _receiver, bytes calldata _data) external;
}

interface DepositManager {
function depositERC20ForUser(
address token,
address user,
uint256 amount
) external;
}

/**
* @notice Sends cross chain messages Polygon L2 network.
* @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be
Expand All @@ -34,21 +42,33 @@ contract Polygon_Adapter is AdapterInterface {
using SafeERC20 for IERC20;
IRootChainManager public immutable rootChainManager;
IFxStateSender public immutable fxStateSender;
DepositManager public immutable depositManager;
address public immutable erc20Predicate;
address public immutable l1Matic;
WETH9 public immutable l1Weth;

/**
* @notice Constructs new Adapter.
* @param _rootChainManager RootChainManager Polygon system helper contract.
* @param _fxStateSender FxStateSender Polygon system helper contract.
* @param _rootChainManager RootChainManager Polygon system contract to deposit tokens over the PoS bridge.
* @param _fxStateSender FxStateSender Polygon system contract to send arbitrary messages to L2.
* @param _depositManager DepositManager Polygon system contract to deposit tokens over the Plasma bridge (Matic).
* @param _erc20Predicate ERC20Predicate Polygon system contract to approve when depositing to the PoS bridge.
* @param _l1Matic matic address on l1.
* @param _l1Weth WETH address on L1.
*/
constructor(
IRootChainManager _rootChainManager,
IFxStateSender _fxStateSender,
DepositManager _depositManager,
address _erc20Predicate,
address _l1Matic,
WETH9 _l1Weth
) {
rootChainManager = _rootChainManager;
fxStateSender = _fxStateSender;
depositManager = _depositManager;
erc20Predicate = _erc20Predicate;
l1Matic = _l1Matic;
l1Weth = _l1Weth;
}

Expand Down Expand Up @@ -80,8 +100,11 @@ contract Polygon_Adapter is AdapterInterface {
if (l1Token == address(l1Weth)) {
l1Weth.withdraw(amount);
rootChainManager.depositEtherFor{ value: amount }(to);
} else if (l1Token == l1Matic) {
IERC20(l1Token).safeIncreaseAllowance(address(depositManager), amount);
depositManager.depositERC20ForUser(l1Token, to, amount);
} else {
IERC20(l1Token).safeIncreaseAllowance(address(rootChainManager), amount);
IERC20(l1Token).safeIncreaseAllowance(erc20Predicate, amount);
rootChainManager.depositFor(to, l1Token, abi.encode(amount));
}
emit TokensRelayed(l1Token, l2Token, amount, to);
Expand Down
30 changes: 29 additions & 1 deletion contracts/test/PolygonMocks.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract RootChainManagerMock {
function depositEtherFor(address user) external payable {} // solhint-disable-line no-empty-blocks
function depositEtherFor(address user) external payable {}

function depositFor(
address user,
Expand All @@ -15,3 +17,29 @@ contract FxStateSenderMock {
// solhint-disable-next-line no-empty-blocks
function sendMessageToChild(address _receiver, bytes calldata _data) external {}
}

contract DepositManagerMock {
function depositERC20ForUser(
address token,
address user,
uint256 amount // solhint-disable-next-line no-empty-blocks
) external {} // solhint-disable-line no-empty-blocks
}

contract PolygonRegistryMock {
// solhint-disable-next-line no-empty-blocks
function erc20Predicate() external returns (address predicate) {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is a mock, but it looks like using the "registry" from polygon's list may give the wrong value. The Registry is 0x33a02E6cC863D393d6Bf231B697b82F6e499cA71, and you can see that function erc20Predicate returns 0x158d5fa3Ef8e4dDA8a5367deCF76b94E7efFCe95. Just based on the tx's i see calling that contract, it's for exiting from Matic, but not bridging/depositing onto matic.

Again since this is a mock, you can have it return whatever value you want. So not a problem. But it might lead to someone thinking that the actual mainnet contract will return the "deposit" predicate, which it doesn't. I believe the deposit predicate is 0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf.

I'm thinking that Polygon_Adapter.sol is deployed on ETH mainnet, for transferring tokens over to Matic. If that's not true... this doesn't apply

}

contract PolygonERC20PredicateMock {
// solhint-disable-next-line no-empty-blocks
function startExitWithBurntTokens(bytes calldata data) external {}
}

contract PolygonERC20Mock is ERC20 {
// solhint-disable-next-line no-empty-blocks
constructor() ERC20("Test ERC20", "TEST") {}

// solhint-disable-next-line no-empty-blocks
function withdraw(uint256 amount) external {}
}
8 changes: 7 additions & 1 deletion deploy/008_deploy_polygon_token_bridger_mainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ const func = async function (hre: HardhatRuntimeEnvironment) {
from: deployer,
log: true,
skipIfAlreadyDeployed: true,
args: [hubPool.address, L1_ADDRESS_MAP[chainId].weth, chainId, POLYGON_CHAIN_IDS[chainId]],
args: [
hubPool.address,
L1_ADDRESS_MAP[chainId].polygonRegistry,
L1_ADDRESS_MAP[chainId].weth,
chainId,
POLYGON_CHAIN_IDS[chainId],
],
deterministicDeployment: "0x1234", // Salt for the create2 call.
});
};
Expand Down
3 changes: 3 additions & 0 deletions deploy/009_deploy_polygon_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const func = async function (hre: HardhatRuntimeEnvironment) {
args: [
L1_ADDRESS_MAP[chainId].polygonRootChainManager,
L1_ADDRESS_MAP[chainId].polygonFxRoot,
L1_ADDRESS_MAP[chainId].polygonDepositManager,
L1_ADDRESS_MAP[chainId].polygonERC20Predicate,
L1_ADDRESS_MAP[chainId].matic,
L1_ADDRESS_MAP[chainId].weth,
],
});
Expand Down
8 changes: 7 additions & 1 deletion deploy/010_deploy_polygon_token_bridger_polygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ const func = async function (hre: HardhatRuntimeEnvironment) {
from: deployer,
log: true,
skipIfAlreadyDeployed: true,
args: [l1HubPool.address, L1_ADDRESS_MAP[l1ChainId].weth, l1ChainId, chainId],
args: [
l1HubPool.address,
L1_ADDRESS_MAP[l1ChainId].polygonRegistry,
L1_ADDRESS_MAP[l1ChainId].weth,
l1ChainId,
chainId,
],
deterministicDeployment: "0x1234", // Salt for the create2 call.
});
};
Expand Down
28 changes: 21 additions & 7 deletions deploy/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,36 @@ export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
finder: "0x40f941E48A552bF496B154Af6bf55725f18D77c3",
polygonRootChainManager: "0xA0c68C638235ee32657e8f720a23ceC1bFc77C77",
polygonFxRoot: "0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2",
polygonERC20Predicate: "0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf",
polygonRegistry: "0x33a02E6cC863D393d6Bf231B697b82F6e499cA71",
polygonDepositManager: "0x401F6c983eA34274ec46f84D70b31C151321188b",
matic: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
},
4: {
weth: "0xc778417E063141139Fce010982780140Aa0cD5Ab",
finder: "0xbb6206fb01fAad31e8aaFc3AD303cEA89D8c8157",
l1ArbitrumInbox: "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e", // Should be listed as "DelayedInbox" here: https://developer.offchainlabs.com/docs/useful_addresses
l1ERC20GatewayRouter: "0x70C143928eCfFaf9F5b406f7f4fC28Dc43d68380", // Should be listed as "L1 ERC20 Gateway Router" here: https://developer.offchainlabs.com/docs/useful_addresses
optimismCrossDomainMessenger: "0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1", // dummy: Optimism's testnet is kovan
optimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", // dummy: Optimism's testnet is kovan
polygonRootChainManager: "0xBbD7cBFA79faee899Eaf900F13C9065bF03B1A74", // dummy: Polygon's testnet is goerli
polygonFxRoot: "0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA", // dummy: Polygon's testnet is goerli
polygonRootChainManager: "0xBbD7cBFA79faee899Eaf900F13C9065bF03B1A74", // Dummy: Polygon's testnet is goerli
polygonFxRoot: "0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA", // Dummy: Polygon's testnet is goerli
polygonERC20Predicate: "0xdD6596F2029e6233DEFfaCa316e6A95217d4Dc34", // Dummy: Polygon's testnet is goerli
polygonRegistry: "0xeE11713Fe713b2BfF2942452517483654078154D", // Dummy: Polygon's testnet is goerli
polygonDepositManager: "0x7850ec290A2e2F40B82Ed962eaf30591bb5f5C96", // Dummy: Polygon's testnet is goerli
matic: "0x499d11E0b6eAC7c0593d8Fb292DCBbF815Fb29Ae", // Dummy: Polygon's testnet is goerli
},
5: {
optimismCrossDomainMessenger: "0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1", // dummy: Optimism's testnet is kovan
weth: "0x60D4dB9b534EF9260a88b0BED6c486fe13E604Fc",
optimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", // dummy: Optimism's testnet is kovan
l1ArbitrumInbox: "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e", // dummy: Arbitrum's testnet is rinkeby
l1ERC20GatewayRouter: "0x70C143928eCfFaf9F5b406f7f4fC28Dc43d68380", // dummy: Arbitrum's testnet is rinkeby
finder: "0xDC6b80D38004F495861E081e249213836a2F3217",
polygonRootChainManager: "0xBbD7cBFA79faee899Eaf900F13C9065bF03B1A74",
polygonFxRoot: "0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA",
weth: "0x60D4dB9b534EF9260a88b0BED6c486fe13E604Fc",
polygonERC20Predicate: "0xdD6596F2029e6233DEFfaCa316e6A95217d4Dc34",
polygonRegistry: "0xeE11713Fe713b2BfF2942452517483654078154D",
polygonDepositManager: "0x7850ec290A2e2F40B82Ed962eaf30591bb5f5C96",
matic: "0x499d11E0b6eAC7c0593d8Fb292DCBbF815Fb29Ae",
},
42: {
l1ArbitrumInbox: "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e", // dummy: Arbitrum's testnet is rinkeby
Expand All @@ -37,8 +47,12 @@ export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
weth: "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
optimismStandardBridge: "0x22F24361D548e5FaAfb36d1437839f080363982B",
finder: "0xeD0169a88d267063184b0853BaAAAe66c3c154B2",
polygonRootChainManager: "0xBbD7cBFA79faee899Eaf900F13C9065bF03B1A74", // dummy: Polygon's testnet is goerli
polygonFxRoot: "0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA", // dummy: Polygon's testnet is goerli
polygonRootChainManager: "0xBbD7cBFA79faee899Eaf900F13C9065bF03B1A74", // Dummy: Polygon's testnet is goerli
polygonFxRoot: "0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA", // Dummy: Polygon's testnet is goerli
polygonERC20Predicate: "0xdD6596F2029e6233DEFfaCa316e6A95217d4Dc34", // Dummy: Polygon's testnet is goerli
polygonRegistry: "0xeE11713Fe713b2BfF2942452517483654078154D", // Dummy: Polygon's testnet is goerli
polygonDepositManager: "0x7850ec290A2e2F40B82Ed962eaf30591bb5f5C96", // Dummy: Polygon's testnet is goerli
matic: "0x499d11E0b6eAC7c0593d8Fb292DCBbF815Fb29Ae", // Dummy: Polygon's testnet is goerli
},
};

Expand Down
Loading