Skip to content

Commit

Permalink
Add contract, state variable, and function comments for the systme.
Browse files Browse the repository at this point in the history
This will help auditors and other readers of the code to understand how each contract and its functions work.
  • Loading branch information
Melvillian committed Aug 29, 2023
1 parent 0e296b0 commit 61e0723
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 41 deletions.
6 changes: 6 additions & 0 deletions scripts/DeployInit.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import "../src/NativeConverter.sol";
import "../src/ZkMinterBurnerProxy.sol";
import "../src/ZkMinterBurner.sol";

/// @title DeployInit
/// @notice A script for deploying, initializing, and setting the access controls
/// @notice for the 3 contracts that comprise the zkUSDCe system:
/// @notice 1) L1Escrow
/// @notice 2) ZkMinterBurner
/// @notice 3) NativeConverter
contract DeployInit is Script {
uint256 l1ForkId = vm.createFork(vm.envString("L1_RPC_URL"));
uint256 l2ForkId = vm.createFork(vm.envString("L2_RPC_URL"));
Expand Down
3 changes: 3 additions & 0 deletions scripts/DeployInitHelpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import "../src/NativeConverter.sol";
import "../src/ZkMinterBurnerProxy.sol";
import "../src/ZkMinterBurner.sol";

/// @title LibDeployInit
/// @dev The contract that actually implements the logic for deploying
/// the zkUSDC-e system contracts
library LibDeployInit {
function deployL1Contracts() internal returns (address l1eProxy) {
// deploy implementation
Expand Down
15 changes: 12 additions & 3 deletions src/CommonAdminOwner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";

/// @title CommonAdminOwner
/// @notice An upgradeable contract that, when inherited from, provides 4 functionalities:
/// @notice 1. The ability to pause and unpause functions with the `whenNotPaused` modifier
/// @notice 2. The ability to transfer ownership (which controls who can pause/unpause)
/// @notice 3. UUPS upgradeability, and the admin role which is allowed to upgrade
/// @notice the implementation contract
/// @notice 4. The ability to change the admin (which controls upgradeability)
contract CommonAdminOwner is
Initializable,
OwnableUpgradeable,
PausableUpgradeable,
UUPSUpgradeable
{
/// @notice The initializer, which must be used instead of the constructor
/// @notice because this is a UUPS contract
function __CommonAdminOwner_init() internal onlyInitializing {
__Ownable_init(); // ATTN: this is overwritten by _transferOwnership
__Pausable_init(); // NOOP
__UUPSUpgradeable_init(); // NOOP
__Ownable_init();
__Pausable_init();
__UUPSUpgradeable_init();
}

modifier onlyAdmin() {
Expand Down
47 changes: 38 additions & 9 deletions src/L1Escrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,31 @@ import {CommonAdminOwner} from "./CommonAdminOwner.sol";
import {IUSDC} from "./interfaces/IUSDC.sol";
import {LibPermit} from "./helpers/LibPermit.sol";

// This contract will receive USDC from users on L1 and trigger BridgeMinter on the zkEVM via LxLy.
// This contract will hold all of the backing for USDC on zkEVM.
/// @title L1Escrow
/// @notice This upgradeable contract receives USDC from users on L1 and uses the PolygonZkEVMBridge
/// to send a message to the ZkMinterBurner contract on the L2 (zkEVM) which
/// then mints USDC-e for users
/// @notice This contract holds all of the L1 USDC that backs the USDC-e on the zkEVM
/// @notice This contract is upgradeable using UUPS, and can have its important functions
/// paused and unpaused
contract L1Escrow is IBridgeMessageReceiver, CommonAdminOwner {
using SafeERC20Upgradeable for IUSDC;

event Deposit(address indexed from, address indexed to, uint256 amount);

/// @notice The singleton bridge contract on both L1 and L2 (zkEVM) that faciliates
/// @notice bridging messages between L1 and L2. It also stores all of the L1 USDC
/// @notice backing the L2 BridgeWrappedUSDC
IPolygonZkEVMBridge public bridge;

/// @notice The ID used internally by the bridge to identify zkEVM messages. Initially
/// @notice set to be `1`
uint32 public zkNetworkId;

/// @notice Address of the L2 ZkMinterBurner, which receives messages from the L1Escrow
address public zkMinterBurner;

/// @notice Address of the L1 USDC token
IUSDC public l1USDC;

constructor() {
Expand All @@ -28,6 +43,8 @@ contract L1Escrow is IBridgeMessageReceiver, CommonAdminOwner {
_transferOwnership(address(1));
}

/// @notice Setup the state variables of the upgradeable L1Escrow contract
/// @notice the owner is the contract that is able to pause and unpause function calls
function initialize(
address owner_,
address admin_,
Expand All @@ -53,12 +70,21 @@ contract L1Escrow is IBridgeMessageReceiver, CommonAdminOwner {
l1USDC = IUSDC(l1Usdc_);
}

/// @notice Bridges L1 USDC to L2 USDC-e
/// @dev The L1Escrow transfers L1 USDC from the caller to itself and
/// @dev calls `bridge.bridgeMessage, which ultimately results in a message
/// @dev received on the L2 ZkMinterBurner which mints USDC-e for the destination
/// @dev address
/// @dev Can be paused
/// @param destinationAddress address that will receive USDC-e on the L2
/// @param amount amount of L1 USDC to bridge
/// @param forceUpdateGlobalExitRoot whether or not to force the bridge to update
function bridgeToken(
address destinationAddress,
uint256 amount,
bool forceUpdateGlobalExitRoot
) public whenNotPaused {
// User calls deposit() on L1Escrow, L1_USDC transferred to L1Escrow
// User calls `bridgeToken` on L1Escrow, L1_USDC is transferred to L1Escrow
// message sent to PolygonZkEvmBridge targeted to L2's zkMinterBurner.

require(destinationAddress != address(0), "INVALID_RECEIVER");
Expand All @@ -78,6 +104,8 @@ contract L1Escrow is IBridgeMessageReceiver, CommonAdminOwner {
emit Deposit(msg.sender, destinationAddress, amount);
}

/// @notice Similar to {L1Escrow-bridgeToken}, but saves an ERC20.approve call
/// @notice by using the EIP-2612 permit function
function bridgeToken(
address destinationAddress,
uint256 amount,
Expand All @@ -90,26 +118,27 @@ contract L1Escrow is IBridgeMessageReceiver, CommonAdminOwner {
bridgeToken(destinationAddress, amount, forceUpdateGlobalExitRoot);
}

/// @dev This function is triggered by the bridge to faciliate the L1 USDC withdrawal process.
/// @dev This function is called by the bridge when a message is sent by the L2
/// @dev ZkMinterBurner communicating that it has burned USDC-e and wants to withdraw the L1 USDC
/// @dev that backs it.
/// @dev This function can only be called by the bridge contract
/// @dev Can be paused
function onMessageReceived(
address originAddress,
uint32 originNetwork,
bytes memory data
) external payable whenNotPaused {
// Function triggered by the bridge once a message is received by the other network

require(msg.sender == address(bridge), "NOT_BRIDGE");
require(zkMinterBurner == originAddress, "NOT_MINTER_BURNER");
require(zkNetworkId == originNetwork, "NOT_ZK_CHAIN");

// decode message data and call withdraw
// decode message data and call transfer
(address l1Receiver, uint256 amount) = abi.decode(
data,
(address, uint256)
);

// Message claimed and sent to L1Escrow,
// which transfers L1_USDC to the correct address.

// kinda redundant - these checks are being done by the caller
require(l1Receiver != address(0), "INVALID_RECEIVER");
require(amount > 0, "INVALID_AMOUNT");
Expand Down
37 changes: 27 additions & 10 deletions src/NativeConverter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import {CommonAdminOwner} from "./CommonAdminOwner.sol";
import {IUSDC} from "./interfaces/IUSDC.sol";
import {LibPermit} from "./helpers/LibPermit.sol";

// This contract will receive BridgeWrappedUSDC on zkEVM and issue USDC.e on zkEVM.
// This contract will hold the minter role giving it the ability to mint USDC.e based on inflows of BridgeWrappedUSDC.
// This contract will also have a permissionless publicly callable function called “migrate” which when called will
// withdraw all BridgedWrappedUSDC to L1 via the LXLY bridge. The beneficiary address will be the L1Escrow,
// thus migrating the supply and settling the balance.
/// @title NativeConverter
/// @notice This contract will receive BridgeWrappedUSDC on zkEVM and issue USDC.e on the zkEVM.
/// @notice This contract will hold the minter role giving it the ability to mint USDC.e based on
/// inflows of BridgeWrappedUSDC. This contract will also have a permissionless publicly
/// callable function called “migrate” which when called will burn all BridgedWrappedUSDC
/// on the L2, and send a message to the bridge that causes all of the corresponding
/// backing L1 USD to be sent to the L1Escrow. This aligns the balance of the L1Escrow
/// contract with the total supply of USDC-e on the zkEVM.
contract NativeConverter is CommonAdminOwner {
using SafeERC20Upgradeable for IUSDC;

Expand All @@ -22,10 +25,17 @@ contract NativeConverter is CommonAdminOwner {

/// @notice the PolygonZkEVMBridge deployed on the zkEVM
IPolygonZkEVMBridge public bridge;

/// @notice The ID used internally by the bridge to identify L1 messages. Initially
/// @notice set to be `0`
uint32 public l1NetworkId;

/// @notice The address of the L1Escrow
address public l1Escrow;

/// @notice The L2 USDC-e deployed on the zkEVM
IUSDC public zkUSDCe;

/// @notice The default L2 USDC TokenWrapped token deployed on the zkEVM
IUSDC public zkBWUSDC;

Expand All @@ -35,6 +45,8 @@ contract NativeConverter is CommonAdminOwner {
_transferOwnership(address(1));
}

/// @notice Setup the state variables of the upgradeable NativeConverter contract
/// @notice The owner is the contract that is able to pause and unpause function calls
function initialize(
address owner_,
address admin_,
Expand Down Expand Up @@ -63,16 +75,17 @@ contract NativeConverter is CommonAdminOwner {
zkBWUSDC = IUSDC(zkBWUSDC_);
}

/// @notice Converts L2 BridgeWrappedUSDC to L2 USDC-e
/// @dev The NativeConverter transfers L2 BridgeWrappedUSDC from the caller to itself and
/// @dev mints L2 USDC-e to the caller
/// @param receiver address that will receive L2 USDC-e on the L2
/// @param amount amount of L2 BridgeWrappedUSDC to convert
/// @param permitData data for the permit call on the L2 BridgeWrappedUSDC
function convert(
address receiver,
uint256 amount,
bytes calldata permitData
) external whenNotPaused {
// User calls convert() on NativeConverter,
// BridgeWrappedUSDC is transferred to NativeConverter
// NativeConverter calls mint() on NativeUSDC which mints
// new supply to the correct address.

require(receiver != address(0), "INVALID_RECEIVER");
require(amount > 0, "INVALID_AMOUNT");

Expand All @@ -86,6 +99,10 @@ contract NativeConverter is CommonAdminOwner {
emit Convert(msg.sender, receiver, amount);
}

/// @notice Migrates L2 BridgeWrappedUSDC USDC to L1 USDC
/// @dev Any BridgeWrappedUSDC transfered in by previous calls to
/// @dev {NativeConverter-convert} will be burned and the corresponding
/// @dev L1 USDC will be sent to the L1Escrow via a message to the bridge
function migrate() external whenNotPaused {
// Anyone can call migrate() on NativeConverter to
// have all zkBridgeWrappedUSDC withdrawn via the PolygonZkEVMBridge
Expand Down
52 changes: 35 additions & 17 deletions src/ZkMinterBurner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,29 @@ import {CommonAdminOwner} from "./CommonAdminOwner.sol";
import {IUSDC} from "./interfaces/IUSDC.sol";
import {LibPermit} from "./helpers/LibPermit.sol";

// - Minter
// This contract will receive messages from the LXLY bridge on zkEVM,
// it will hold the minter role giving it the ability to mint USDC.e
// based on instructions from LXLY from Ethereum only.
// - Burner
// This contract will send messages to LXLY bridge on zkEVM,
// it will hold the burner role giving it the ability to burn USDC.e based on instructions from LXLY,
// triggering a release of assets on L1Escrow.
/// @title ZkMinterBurner
/// @notice This upgradeable L2 contract facilitates 2 actions:
/// 1. Minting USDC-E on the zkEVM backed by L1 USDC held in the L1Escrow
/// 2. Burning USDC-E on the zkEVM and sending a bridge message to unlock the
/// corresponding funds held in the L1Escrow (the reverse of (1) above).
contract ZkMinterBurner is IBridgeMessageReceiver, CommonAdminOwner {
using SafeERC20Upgradeable for IUSDC;

event Withdraw(address indexed from, address indexed to, uint256 amount);

/// @notice The singleton bridge contract on both L1 and L2 (zkEVM) that faciliates
/// @notice bridging messages between L1 and L2. It also stores all of the L1 USDC
/// @notice backing the L2 BridgeWrappedUSDC
IPolygonZkEVMBridge public bridge;

/// @notice The ID used internally by the bridge to identify L1 messages. Initially
/// @notice set to be `0`
uint32 public l1NetworkId;

/// @notice The address of the L1Escrow
address public l1Escrow;

/// @notice The address of the L2 USDC-e ERC20 token
IUSDC public zkUSDCe;

constructor() {
Expand All @@ -34,6 +41,8 @@ contract ZkMinterBurner is IBridgeMessageReceiver, CommonAdminOwner {
_transferOwnership(address(1));
}

/// @notice Setup the state variables of the upgradeable ZkMinterBurner contract
/// @notice the owner is the contract that is able to pause and unpause function calls
function initialize(
address owner_,
address admin_,
Expand All @@ -59,20 +68,25 @@ contract ZkMinterBurner is IBridgeMessageReceiver, CommonAdminOwner {
zkUSDCe = IUSDC(zkUSDCe_);
}

/// @notice Bridges L2 USDC-e to L1 USDC
/// @dev The ZkMinterBurner transfers L2 USDC-e from the caller to itself and
/// @dev burns it, thencalls `bridge.bridgeMessage, which ultimately results in a message
/// @dev received on the L1Escrow which unlocks the corresponding L1 USDC to the
/// @dev destination address
/// @dev Can be paused
/// @param destinationAddress address that will receive L1 USDC on the L1
/// @param amount amount of L2 USDC-e to bridge
/// @param forceUpdateGlobalExitRoot whether or not to force the bridge to update
function bridgeToken(
address destinationAddress,
uint256 amount,
bool forceUpdateGlobalExitRoot
) public whenNotPaused {
// User calls withdraw() on BridgeBurner
// which calls burn() on NativeUSDC burning the supply.
// Message is sent to PolygonZkEVMBridge targeted to L1Escrow.

require(destinationAddress != address(0), "INVALID_RECEIVER");
// this is redundant - the usdc contract does the same validation
// require(amount > 0, "INVALID_AMOUNT");

// transfer the USDC.E from the user, and then burn it
// transfer the USDC-E from the user, and then burn it
zkUSDCe.safeTransferFrom(msg.sender, address(this), amount);
zkUSDCe.burn(amount);

Expand All @@ -88,6 +102,8 @@ contract ZkMinterBurner is IBridgeMessageReceiver, CommonAdminOwner {
emit Withdraw(msg.sender, destinationAddress, amount);
}

/// @notice Similar to {ZkMinterBurnerImpl-bridgeToken}, but saves an ERC20.approve call
/// @notice by using the EIP-2612 permit function
function bridgeToken(
address destinationAddress,
uint256 amount,
Expand All @@ -100,6 +116,12 @@ contract ZkMinterBurner is IBridgeMessageReceiver, CommonAdminOwner {
bridgeToken(destinationAddress, amount, forceUpdateGlobalExitRoot);
}

/// @dev This function is triggered by the bridge to faciliate the USDC-e minting process.
/// @dev This function is called by the bridge when a message is sent by the L1Escrow
/// @dev communicating that it has received L1 USDC and wants the ZkMinterBurner to
/// @dev mint USDC-e.
/// @dev This function can only be called by the bridge contract
/// @dev Can be paused
function onMessageReceived(
address originAddress,
uint32 originNetwork,
Expand All @@ -117,10 +139,6 @@ contract ZkMinterBurner is IBridgeMessageReceiver, CommonAdminOwner {
(address, uint256)
);

// Message claimed and sent to ZkMinterBurner,
// which calls mint() on zkUSDCe
// which mints new supply to the correct address.

// this is redundant - the zkUSDCe contract does the same validations
// require(zkReceiver != address(0), "INVALID_RECEIVER");
// require(amount > 0, "INVALID_AMOUNT");
Expand Down
12 changes: 10 additions & 2 deletions src/helpers/LibPermit.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

/// @title LibPermit
/// @notice Library to call the EIP-2612 permit method on a token
library LibPermit {
error NotValidSelector();
error NotValidOwner();
error NotValidSpender();
error NotValidAmount();

// bytes4(keccak256(bytes("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)")));
/// @dev bytes4(keccak256(bytes("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)")));
bytes4 private constant _PERMIT_SIGNATURE = 0xd505accf;

/// @dev Adapted from PolygonZKEVMBridge.sol's `_permit`
/**
* @notice Function to call token the EIP-2612 permit method on a token
* @dev Adapted from PolygonZKEVMBridge.sol's `_permit`
+ @param token ERC20 token address
* @param amount Quantity that is expected to be allowed
* @param permitData Raw data of the call `permit` of the token
*/
function permit(
address token,
uint256 amount,
Expand Down

0 comments on commit 61e0723

Please sign in to comment.