Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/usdc bridge #1

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
Open

Feat/usdc bridge #1

wants to merge 11 commits into from

Conversation

sander2
Copy link
Collaborator

@sander2 sander2 commented Apr 8, 2024

A bridge for USDC that complies with the bridged USDC standard

Overview

We use a modified version of the optimism StandardBridge. The StandardBridge was taken from version 1.7.1, which is newer than what we will use at launch. I tried packporting my changes to the version we'll actually be running, but the newer version actually seems to handle proxy support better (older version uses semver that is stored in variables that are marked immutable, which is problematic in code used through a proxy).

The changes compared to stock, are:

  • remove the IOptimismMintableERC20 requirement. Instead, the contract is initialized with a concrete pair of L1Token and L2Token addresses.
  • Unlike the standard bridge, this bridge will only work with one token (no other erc20s, no eth)
  • Both the L1 and L2 bridge contracts are now Ownable. The owner will be a multisig controlled by a set of signers from different legal entities.
    • The owner can pause bridging functionality. Unlike the standard bridge, we pause the sending side rather than the receiving side.
    • The L1Bridge owner can burn USDC held by the bridge.
  • removed superchain config dependency since we won't have it at launch.

We deploy USDC from Circle's stablecoin-evm token, at commit 657375471bff72afa5f625083bbab8003eb5f8c9. This includes the MasterMinter contract.

We will deploy a UsdcManager contract that will be set as the MasterMinter owner, the USDC implementation contract owner, and the USDC proxy contract admin. This owner of this contract can whitelist an address that is owned by Circle, after which this whitelisted account can call transferUSDCRoles to transfer the proxy and implementation ownership to a specified account. This call also removes minting priviledges from the bridge.

Role summary

  • (1) L1 and L2 Proxy admin will be a conduit-bob multisig
  • (2) L1 and L2 bridge owner will be Bob. This role can pause bridging. Additionally, it can burn USDC on the L1 if given permission by Circle as part of the migration to native minting.
  • (3) USDC implementation owner, USDC proxy admin and MasterMinter will be set to the UsdcManager contract
  • (4) The owner of UsdcManager will be Bob, which can whitelist one address that all UsdcManager roles can be transfered to. Once an address is whitelisted, it is locked in (i.e. it can not be changed later on), and the roles can then only be transferred to that address .

Copy link

changeset-bot bot commented Apr 8, 2024

⚠️ No Changeset found

Latest commit: 4a83c90

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

/// @notice Reserve extra slots (to a total of 50) in the storage layout for future upgrades.
/// A gap size of 43 was chosen here, so that the first slot used in a child contract
/// would be a multiple of 50.
uint256[43] private __gap;

Choose a reason for hiding this comment

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

Why did you change this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are 2 new storage slots being used before this. By reducing the gap, the multiple is kept to 50 as it was saying in the original comment. I don't think it matters much though

Choose a reason for hiding this comment

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

The was actually a necessary change. If we left it at 45, then we would have caused a storage shift for other contracts that are serialized after the USDC bridge in the inheritance hierarchy (I haven't checked if there were other contracts after the USDC bridge, but changing it to 43 was the safe choice. )

From:
https://docs.openzeppelin.com/contracts/3.x/upgradeable#:~:text=You%20may%20notice%20that%20every,storage%20compatibility%20with%20existing%20deployments.

It isn’t safe to simply add a state variable because it "shifts down" all of the state variables below in the inheritance chain.

Choose a reason for hiding this comment

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

It is only important to contract changes to contracts that are already deployed though, so Sander is right that if this is the first deployment of the contract, it is not necessary. ⬆️

Comment on lines +286 to +288
// usdc has no burnFrom, so first transfer to the contract and then burn.
IERC20(_localToken).safeTransferFrom(_from, address(this), _amount);
IPartialUsdc(_localToken).burn(_amount);

Choose a reason for hiding this comment

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

Just for my understanding this is executed on the L2? And just to check _localToken is Circle's FiatToken? That can only by burned by the owner right? Which is this contract?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just for my understanding this is executed on the L2?

yes

And just to check _localToken is Circle's FiatToken?

yes

That can only by burned by the owner right?

Can only be burned by addresses that have been assigned the minter role, as is the case for the bridge.

Which is this contract?

https://github.com/circlefin/stablecoin-evm/blob/657375471bff72afa5f625083bbab8003eb5f8c9/contracts/v1/FiatTokenV1.sol#L360

Copy link

@ferencdg ferencdg May 17, 2024

Choose a reason for hiding this comment

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

IGNORE, turned out to be false:

Hmmm, I don't think the code is correct.

the function starts with:
require(_isCorrectUsdcTokenPair(_localToken, _remoteToken), "Invalid token pair");
and if you look into _isCorrectUsdcTokenPair for L2UsdcBridge

function _isCorrectUsdcTokenPair(address _localToken, address _remoteToken) internal view override returns (bool) {
        return _isL2Usdc(_localToken) && _isL1Usdc(_remoteToken);
    }

So if the require doesn't fail, then at this point I know that
_isL2Usdc(_localToken) && _isL1Usdc(_remoteToken)
is true, so why do we have this if statement?
(_isL1Usdc(_localToken) && _isL2Usdc(_remoteToken)) when it will always be false?

..................................

Let's just think about this USDCBridge part of the L2UsdcBridge. If someone calls bridgeERC20To in our case, then someone want's to initiate an L2->L1 transfer. If someone initiated an L1->L2 transfer, then finalizeBridgeERC20 would have been called on the L2UsdcBridge and not the bridgeERC20To function.
I think similar problem exists with the code in the finalizeBridgeERC20 method, but haven't checked.

..................................

The if/else statement in the original Optimum code is used to differentiate between different token implementations I think, for example:
_initiateBridgeERC20 case

both branches initiate an L2->L1 transfer, but the first one works with 'burneable' tokens while the other token implementation might not be able to burn. For this second implementation we just transfer the local token from the user to the bridge and do a little bit of accounting in the deposit array to know how much of that non-burneable token is still there.

if (_isOptimismMintableERC20(_localToken)) {
          require(
              _isCorrectTokenPair(_localToken, _remoteToken),
              "StandardBridge: wrong remote token for Optimism Mintable ERC20 local token"
          );

          OptimismMintableERC20(_localToken).burn(_from, _amount);
      } else {
          IERC20(_localToken).safeTransferFrom(_from, address(this), _amount);
          deposits[_localToken][_remoteToken] = deposits[_localToken][_remoteToken] + _amount;
      }
```~~

Comment on lines 65 to 77
function transferUSDCRoles(address owner) external {
require(msg.sender == whitelistedTakeoverOrigin, "Unauthorized transfer");
require(owner != address(0), "Can not transfer ownership to the zero address");

// Change proxy admin
IUsdcProxy(tokenProxyAddress).changeAdmin(owner);

// remove minter
IMasterMinter(masterMinterAddress).removeMinter();

// Transfer implementation owner
IUsdcImpl(tokenProxyAddress).transferOwnership(owner);
}

Check warning

Code scanning / Slither

Unused return Medium

@@ -0,0 +1,119 @@
// SPDX-License-Identifier: MIT

Choose a reason for hiding this comment

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

Why not just use the Pausable from the openzeppelin package? How is this different from the original Pausable contract?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants