diff --git a/src/NativeConverter.sol b/src/NativeConverter.sol index 0b606a3..2db1a84 100644 --- a/src/NativeConverter.sol +++ b/src/NativeConverter.sol @@ -21,6 +21,7 @@ contract NativeConverter is CommonAdminOwner { using SafeERC20Upgradeable for IUSDC; event Convert(address indexed from, address indexed to, uint256 amount); + event Deconvert(address indexed from, address indexed to, uint256 amount); event Migrate(uint256 amount); /// @notice the PolygonZkEVMBridge deployed on the zkEVM @@ -106,11 +107,33 @@ contract NativeConverter is CommonAdminOwner { emit Convert(msg.sender, receiver, amount); } + function deconvert( + address receiver, + uint256 amount, + bytes calldata permitData + ) external whenNotPaused { + require(receiver != address(0), "INVALID_RECEIVER"); + require(amount > 0, "INVALID_AMOUNT"); + require(amount <= zkBWUSDC.balanceOf(address(this)), "AMOUNT_TOO_LARGE"); + + if (permitData.length > 0) + LibPermit.permit(address(zkUSDCe), amount, permitData); + + // transfer native usdc from user to the converter, and burn it + zkUSDCe.safeTransferFrom(msg.sender, address(this), amount); + zkUSDCe.burn(amount); + + // and then send bridge wrapped usdc to the user + zkBWUSDC.safeTransfer(receiver, amount); + + emit Deconvert(msg.sender, receiver, amount); + } + /// @notice Migrates L2 BridgeWrappedUSDC USDC to L1 USDC /// @dev Any BridgeWrappedUSDC transfered in by previous calls to /// `convert` will be burned and the corresponding /// L1 USDC will be sent to the L1Escrow via a message to the bridge - function migrate() external whenNotPaused { + function migrate() external onlyOwner whenNotPaused { // Anyone can call migrate() on NativeConverter to // have all zkBridgeWrappedUSDC withdrawn via the PolygonZkEVMBridge // moving the L1_USDC held in the PolygonZkEVMBridge to L1Escrow diff --git a/test/Base.sol b/test/Base.sol index eb433d5..04c3b35 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -32,6 +32,9 @@ library Events { // copy of NativeConverter.Convert event Convert(address indexed from, address indexed to, uint256 amount); + // copy of NativeConverter.Deconvert + event Deconvert(address indexed from, address indexed to, uint256 amount); + // copy of L1Escrow.Deposit event Deposit(address indexed from, address indexed to, uint256 amount); diff --git a/test/integration/ConvertFlows.t.sol b/test/integration/ConvertFlows.t.sol index c2553fe..5a1db21 100644 --- a/test/integration/ConvertFlows.t.sol +++ b/test/integration/ConvertFlows.t.sol @@ -186,4 +186,38 @@ contract ConvertFlows is Base { _assertUsdcSupplyAndBalancesMatch(); } + + function testConverNativeUsdcToWrappedUsdc() public { + vm.selectFork(_l2Fork); + vm.startPrank(_alice); + + // setup: alice converts wrapped to native ("seeding" the nativeconverter) and sends to bob + uint256 amount = _toUSDC(10000); + _erc20L2Wusdc.approve(address(_nativeConverter), amount); + _nativeConverter.convert(_bob, amount, _emptyBytes); + vm.stopPrank(); + + // deconvert + vm.startPrank(_bob); + + // frank has no wrapped + uint256 wrappedBalance1 = _erc20L2Wusdc.balanceOf(_frank); + assertEq(wrappedBalance1, 0); + + // bob converts 8k native to wrapped, with frank as the receiver + uint256 amount2 = _toUSDC(8000); + _erc20L2Usdc.approve(address(_nativeConverter), amount2); + + // check that our convert event is emitted + vm.expectEmit(address(_nativeConverter)); + emit Events.Deconvert(_bob, _frank, amount2); + + _nativeConverter.deconvert(_frank, amount2, _emptyBytes); + + // frank has 8k wrapped + uint256 wrappedBalance2 = _erc20L2Wusdc.balanceOf(_frank); + assertEq(wrappedBalance2, amount2); + + _assertUsdcSupplyAndBalancesMatch(); + } }