diff --git a/contracts/optimism/DepositDataCodec.sol b/contracts/optimism/DepositDataCodec.sol index 9da4265b..a7bc919a 100644 --- a/contracts/optimism/DepositDataCodec.sol +++ b/contracts/optimism/DepositDataCodec.sol @@ -27,8 +27,8 @@ contract DepositDataCodec { } DepositData memory depositData; - depositData.rate = uint256(bytes32(buffer[0:31])); - depositData.time = uint256(bytes32(buffer[32:63])); + depositData.rate = uint256(bytes32(buffer[0:32])); + depositData.time = uint256(bytes32(buffer[32:64])); depositData.data = buffer[64:]; return depositData; diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 96f18a7f..1353325f 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -68,22 +68,7 @@ contract L1ERC20TokenBridge is revert ErrorSenderNotEOA(); } - DepositData memory depositData; - depositData.rate = IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(); - depositData.time = block.timestamp; - depositData.data = data_; - - bytes memory encodedDepositData = encodeDepositData(depositData); - - if (isRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); - IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_); - uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); - _initiateERC20Deposit(l1TokenRebasable, l2TokenRebasable, msg.sender, msg.sender, wstETHAmount, l2Gas_, encodedDepositData); - } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); - _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, msg.sender, amount_, l2Gas_, encodedDepositData); - } + _depositERC20To(l1Token_, l2Token_, msg.sender, amount_, l2Gas_, data_); } /// @inheritdoc IL1ERC20Bridge @@ -101,7 +86,7 @@ contract L1ERC20TokenBridge is onlySupportedL1Token(l1Token_) onlySupportedL2Token(l2Token_) { - _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, data_); + _depositERC20To(l1Token_, l2Token_, to_, amount_, l2Gas_, data_); } /// @inheritdoc IL1ERC20Bridge @@ -123,7 +108,7 @@ contract L1ERC20TokenBridge is uint256 stETHAmount = IERC20Wrapable(l1TokenNonRebasable).unwrap(amount_); IERC20(l1TokenRebasable).safeTransfer(to_, stETHAmount); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(l1Token_).safeTransfer(to_, amount_); + IERC20(l1TokenNonRebasable).safeTransfer(to_, amount_); } emit ERC20WithdrawalFinalized( @@ -136,6 +121,38 @@ contract L1ERC20TokenBridge is ); } + function _depositERC20To( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) internal { + + DepositData memory depositData; + depositData.rate = IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(); + depositData.time = block.timestamp; + depositData.data = data_; + + bytes memory encodedDepositData = encodeDepositData(depositData); + + if (amount_ == 0) { + _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, encodedDepositData); + return; + } + + if (isRebasableTokenFlow(l1Token_, l2Token_)) { + IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); + IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_); + uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); + _initiateERC20Deposit(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, wstETHAmount, l2Gas_, encodedDepositData); + } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { + IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); + _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l2Gas_, encodedDepositData); + } + } + /// @dev Performs the logic for deposits by informing the L2 token bridge contract /// of the deposit and calling safeTransferFrom to lock the L1 funds. /// @param from_ Account to pull the deposit from on L1 diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 595d1bf1..68f2bceb 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -39,8 +39,6 @@ contract L2ERC20TokenBridge is address public immutable tokensRateOracle; - - /// @param messenger_ L2 messenger address being used for cross-chain communications /// @param l1TokenBridge_ Address of the corresponding L1 bridge /// @param l1TokenNonRebasable_ Address of the bridged token in the L1 chain @@ -62,20 +60,12 @@ contract L2ERC20TokenBridge is /// @inheritdoc IL2ERC20Bridge function withdraw( - address l1Token_, address l2Token_, uint256 amount_, uint32 l1Gas_, bytes calldata data_ ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - if(l2Token_ == l2TokenRebasable) { - uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); - ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); - _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, shares, l1Gas_, data_); - } else { - IERC20Bridged(l2TokenNonRebasable).bridgeBurn(msg.sender, amount_); - _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l1Gas_, data_); - } + _withdrawTo(l2Token_, msg.sender, amount_, l1Gas_, data_); } /// @inheritdoc IL2ERC20Bridge @@ -85,8 +75,28 @@ contract L2ERC20TokenBridge is uint256 amount_, uint32 l1Gas_, bytes calldata data_ - ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - _initiateWithdrawal(l1TokenNonRebasable, l2Token_, msg.sender, to_, amount_, l1Gas_, data_); + ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { + _withdrawTo(l2Token_, to_, amount_, l1Gas_, data_); + } + + function _withdrawTo( + address l2Token_, + address to_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) internal { + if (l2Token_ == l2TokenRebasable) { + + uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); + ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); + _initiateWithdrawal(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, shares, l1Gas_, data_); + + } else if (l2Token_ == l2TokenNonRebasable) { + + IERC20Bridged(l2TokenNonRebasable).bridgeBurn(msg.sender, amount_); + _initiateWithdrawal(l2TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l1Gas_, data_); + } } /// @inheritdoc IL2ERC20Bridge @@ -112,6 +122,7 @@ contract L2ERC20TokenBridge is } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20Bridged(l2TokenNonRebasable).bridgeMint(to_, amount_); } + emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } diff --git a/contracts/optimism/interfaces/IL2ERC20Bridge.sol b/contracts/optimism/interfaces/IL2ERC20Bridge.sol index 04c27baf..448dfb8b 100644 --- a/contracts/optimism/interfaces/IL2ERC20Bridge.sol +++ b/contracts/optimism/interfaces/IL2ERC20Bridge.sol @@ -46,7 +46,6 @@ interface IL2ERC20Bridge { /// solely as a convenience for external contracts. Aside from enforcing a maximum /// length, these contracts provide no guarantees about its content. function withdraw( - address l1Token_, address l2Token_, uint256 amount_, uint32 l1Gas_, diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol index b8e35599..f3c7f33a 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -15,13 +15,12 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { IERC20 public stETH; address public bridge; - uint256 public tokensRate; /// wst/st + uint256 public tokensRate; constructor(IERC20 stETH_, string memory name_, string memory symbol_) ERC20(name_, symbol_) { stETH = stETH_; - console.log("constructor wrap stETH=",address(stETH)); tokensRate = 2 * 10 **18; _mint(msg.sender, 1000000 * 10**18); diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 6b4ab756..e0822643 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -4,8 +4,8 @@ import env from "../../utils/env"; import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; import testing, { scenario } from "../../utils/testing"; -import hre, { ethers } from "hardhat"; -import { BigNumber, FixedNumber } from "ethers"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; scenario("Optimism :: Bridging integration test", ctxFactory) .after(async (ctx) => { @@ -71,12 +71,169 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.isTrue(await l2ERC20TokenBridge.isWithdrawalsEnabled()); }) + .step("L1 -> L2 deposit zero tokens via depositERC20() method", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1ERC20TokenBridge, + l2TokenRebasable, + l1CrossDomainMessenger, + l2ERC20TokenBridge, + tokensRateOracle, + l1Provider + } = ctx; + + const { accountA: tokenHolderA } = ctx.accounts; + const tokensPerStEth = await l1Token.tokensPerStEth(); + + await l1TokenRebasable + .connect(tokenHolderA.l1Signer) + .approve(l1ERC20TokenBridge.address, 0); + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + + const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + l1ERC20TokenBridge.address + ); + + const tx = await l1ERC20TokenBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1TokenRebasable.address, + l2TokenRebasable.address, + 0, + 200_000, + "0x" + ); + + const blockNumber = await l1Provider.getBlockNumber(); + const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) + const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) + const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToSend, + ]); + + const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToSend, + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20TokenBridge.address, + l1ERC20TokenBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore + ); + }) + + .step("Finalize deposit zero tokens on L2", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l2TokenRebasable, + l1ERC20TokenBridge, + l2CrossDomainMessenger, + l2ERC20TokenBridge, + tokensRateOracle, + l2Provider + } = ctx; + + const tokensPerStEth = await l1Token.tokensPerStEth(); + const blockNumber = await l2Provider.getBlockNumber(); + const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) + const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) + const dataToReceive = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + + const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = + ctx.accounts; + + + const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( + tokenHolderA.address + ); + + const l2TokenRebasableTotalSupplyBefore = await l2TokenRebasable.totalSupply(); + + const tx = await l2CrossDomainMessenger + .connect(l1CrossDomainMessengerAliased) + .relayMessage( + 1, + l1ERC20TokenBridge.address, + l2ERC20TokenBridge.address, + 0, + 300_000, + l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToReceive, + ]), + { gasLimit: 5_000_000 } + ); + + + const [,tokensRate,,updatedAt,] = await tokensRateOracle.latestRoundData(); + assert.equalBN(tokensPerStEth, tokensRate); + assert.equalBN(blockTimestamp, updatedAt); + + await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + "0x", + ]); + + assert.equalBN( + await l2TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore + ); + assert.equalBN( + await l2TokenRebasable.totalSupply(), + l2TokenRebasableTotalSupplyBefore + ); + }) + .step("L1 -> L2 deposit via depositERC20() method", async (ctx) => { const { - l1Token, // wstETH - l1TokenRebasable, // stETH + l1Token, + l1TokenRebasable, l1ERC20TokenBridge, - l2Token, l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, @@ -116,7 +273,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToSend = tokensPerStEthStr + blockTimestampStr.slice(2); + const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, @@ -164,7 +322,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1TokenRebasable, - l2Token, l2TokenRebasable, l1ERC20TokenBridge, l2CrossDomainMessenger, @@ -181,7 +338,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToReceive = tokensPerStEthStr + blockTimestampStr.slice(2); + const dataToReceive = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = @@ -214,10 +371,9 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); - const [,tokensRate,,,] = await tokensRateOracle.latestRoundData(); - console.log("tokensPerStEth=",tokensPerStEth); - console.log("tokensRate=",tokensRate); + const [,tokensRate,,updatedAt,] = await tokensRateOracle.latestRoundData(); assert.equalBN(tokensPerStEth, tokensRate); + assert.equalBN(blockTimestamp, updatedAt); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ l1TokenRebasable.address, @@ -241,9 +397,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("L2 -> L1 withdrawal via withdraw()", async (ctx) => { const { accountA: tokenHolderA } = ctx.accounts; const { withdrawalAmount: withdrawalAmountInRebasableTokens } = ctx.common; - const { - l1Token, - l2Token, + const { l1TokenRebasable, l2TokenRebasable, l2ERC20TokenBridge @@ -263,7 +417,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tx = await l2ERC20TokenBridge .connect(tokenHolderA.l2Signer) .withdraw( - l1TokenRebasable.address, l2TokenRebasable.address, withdrawalAmountInRebasableTokens, 0, @@ -278,7 +431,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) withdrawalAmount, "0x", ]); - assert.equalBN( await l2TokenRebasable.balanceOf(tokenHolderA.address), @@ -292,7 +444,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("Finalize withdrawal on L1", async (ctx) => { const { - l1Token, + l1Token, l1TokenRebasable, l1CrossDomainMessenger, l1ERC20TokenBridge, @@ -354,6 +506,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); }) + // .step("L1 -> L2 deposit via depositERC20To()", async (ctx) => { // const { // l1Token,