diff --git a/contracts/BridgeableTokens.sol b/contracts/BridgeableTokens.sol index a0e85e50..6416d97d 100644 --- a/contracts/BridgeableTokens.sol +++ b/contracts/BridgeableTokens.sol @@ -53,6 +53,14 @@ contract BridgeableTokens { _; } + function isRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { + return l1Token_ == l1TokenRebasable && l2Token_ == l2TokenRebasable; + } + + function isNonRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { + return l1Token_ == l1TokenNonRebasable && l2Token_ == l2TokenNonRebasable; + } + error ErrorUnsupportedL1Token(); error ErrorUnsupportedL2Token(); error ErrorAccountIsZeroAddress(); diff --git a/contracts/optimism/DepositDataCodec.sol b/contracts/optimism/DepositDataCodec.sol new file mode 100644 index 00000000..9da4265b --- /dev/null +++ b/contracts/optimism/DepositDataCodec.sol @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +contract DepositDataCodec { + + struct DepositData { + uint256 rate; + uint256 time; + bytes data; + } + + function encodeDepositData(DepositData memory depositData) internal pure returns (bytes memory) { + bytes memory data = bytes.concat( + abi.encodePacked(depositData.rate), + abi.encodePacked(depositData.time), + abi.encodePacked(depositData.data) + ); + return data; + } + + function decodeDepositData(bytes calldata buffer) internal pure returns (DepositData memory) { + + if (buffer.length < 32 * 2) { + revert ErrorDepositDataLength(); + } + + DepositData memory depositData; + depositData.rate = uint256(bytes32(buffer[0:31])); + depositData.time = uint256(bytes32(buffer[32:63])); + depositData.data = buffer[64:]; + + return depositData; + } + + error ErrorDepositDataLength(); +} \ No newline at end of file diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 93b9535f..96f18a7f 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -13,6 +13,7 @@ import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; import {BridgingManager} from "../BridgingManager.sol"; import {BridgeableTokens} from "../BridgeableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import {DepositDataCodec} from "./DepositDataCodec.sol"; import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; import "hardhat/console.sol"; @@ -25,7 +26,8 @@ contract L1ERC20TokenBridge is IL1ERC20Bridge, BridgingManager, BridgeableTokens, - CrossDomainEnabled + CrossDomainEnabled, + DepositDataCodec { using SafeERC20 for IERC20; @@ -65,16 +67,22 @@ contract L1ERC20TokenBridge is if (Address.isContract(msg.sender)) { revert ErrorSenderNotEOA(); } + + DepositData memory depositData; + depositData.rate = IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(); + depositData.time = block.timestamp; + depositData.data = data_; - if(l1Token_ == l1TokenRebasable) { - bytes memory data = bytes.concat(hex'01', 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(l1Token_, l2Token_, msg.sender, msg.sender, wstETHAmount, l2Gas_, data); - } else { + _initiateERC20Deposit(l1TokenRebasable, l2TokenRebasable, msg.sender, msg.sender, wstETHAmount, l2Gas_, encodedDepositData); + } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); - _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l2Gas_, data_); + _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, msg.sender, amount_, l2Gas_, encodedDepositData); } } @@ -111,10 +119,10 @@ contract L1ERC20TokenBridge is onlySupportedL2Token(l2Token_) onlyFromCrossDomainAccount(l2TokenBridge) { - if (data_.length > 0 && data_[0] == hex'01') { + if (isRebasableTokenFlow(l1Token_, l2Token_)) { uint256 stETHAmount = IERC20Wrapable(l1TokenNonRebasable).unwrap(amount_); IERC20(l1TokenRebasable).safeTransfer(to_, stETHAmount); - } else { + } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(l1Token_).safeTransfer(to_, amount_); } diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index a8f9df01..595d1bf1 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -6,6 +6,7 @@ pragma solidity 0.8.10; import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; +import {ITokensRateOracle} from "../token/interfaces/ITokensRateOracle.sol"; import {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; @@ -14,6 +15,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {BridgingManager} from "../BridgingManager.sol"; import {BridgeableTokens} from "../BridgeableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import {DepositDataCodec} from "./DepositDataCodec.sol"; import { console } from "hardhat/console.sol"; @@ -27,13 +29,18 @@ contract L2ERC20TokenBridge is IL2ERC20Bridge, BridgingManager, BridgeableTokens, - CrossDomainEnabled + CrossDomainEnabled, + DepositDataCodec { using SafeERC20 for IERC20; /// @inheritdoc IL2ERC20Bridge address public immutable l1TokenBridge; + 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 @@ -46,9 +53,11 @@ contract L2ERC20TokenBridge is address l1TokenNonRebasable_, address l1TokenRebasable_, address l2TokenNonRebasable_, - address l2TokenRebasable_ + address l2TokenRebasable_, + address tokensRateOracle_ ) CrossDomainEnabled(messenger_) BridgeableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { l1TokenBridge = l1TokenBridge_; + tokensRateOracle = tokensRateOracle_; } /// @inheritdoc IL2ERC20Bridge @@ -60,10 +69,9 @@ contract L2ERC20TokenBridge is bytes calldata data_ ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { if(l2Token_ == l2TokenRebasable) { - bytes memory data = bytes.concat(hex'01', data_); uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); - _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, shares, l1Gas_, data); + _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_); @@ -96,14 +104,15 @@ contract L2ERC20TokenBridge is onlySupportedL2Token(l2Token_) onlyFromCrossDomainAccount(l1TokenBridge) { - if (data_.length > 0 && data_[0] == hex'01') { + DepositData memory depositData = decodeDepositData(data_); + ITokensRateOracle(tokensRateOracle).updateRate(int256(depositData.rate), depositData.time); + + if (isRebasableTokenFlow(l1Token_, l2Token_)) { ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); - bytes memory data = data_[1:]; - emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data); - } else { - IERC20Bridged(l2Token_).bridgeMint(to_, amount_); - emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); + } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { + IERC20Bridged(l2TokenNonRebasable).bridgeMint(to_, amount_); } + emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } /// @notice Performs the logic for withdrawals by burning the token and informing diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol index 2edf81d1..b8e35599 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -47,4 +47,8 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { return stETHAmount; } + + function tokensPerStEth() external view returns (uint256) { + return tokensRate; + } } diff --git a/contracts/stubs/TokensRateOracleStub.sol b/contracts/stubs/TokensRateOracleStub.sol index 6f397515..4c43af26 100644 --- a/contracts/stubs/TokensRateOracleStub.sol +++ b/contracts/stubs/TokensRateOracleStub.sol @@ -44,4 +44,9 @@ contract TokensRateOracleStub is ITokensRateOracle { ) { return (0,latestRoundDataAnswer,0,latestRoundDataUpdatedAt,0); } + + function updateRate(int256 rate, uint256 updatedAt) external { + latestRoundDataAnswer = rate; + latestRoundDataUpdatedAt = updatedAt; + } } \ No newline at end of file diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 04899582..0501dde8 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -75,6 +75,10 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return sharesAmount; } + function tokensPerStEth() external view returns (uint256) { + return 0; + } + function mintShares(address account_, uint256 amount_) external returns (uint256) { return _mintShares(account_, amount_); } diff --git a/contracts/token/interfaces/IERC20Wrapable.sol b/contracts/token/interfaces/IERC20Wrapable.sol index 15213ccd..de82ee27 100644 --- a/contracts/token/interfaces/IERC20Wrapable.sol +++ b/contracts/token/interfaces/IERC20Wrapable.sol @@ -30,30 +30,9 @@ interface IERC20Wrapable { */ function unwrap(uint256 wrapableTokenAmount_) external returns (uint256); - // TODO: - // /** - // * @notice Get amount of wstETH for a given amount of stETH - // * @param _stETHAmount amount of stETH - // * @return Amount of wstETH for a given stETH amount - // */ - // function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256); - - // /** - // * @notice Get amount of stETH for a given amount of wstETH - // * @param _wstETHAmount amount of wstETH - // * @return Amount of stETH for a given wstETH amount - // */ - // function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256); - - // /** - // * @notice Get amount of stETH for a one wstETH - // * @return Amount of stETH for 1 wstETH - // */ - // function stEthPerToken() external view returns (uint256); - - // /** - // * @notice Get amount of wstETH for a one stETH - // * @return Amount of wstETH for a 1 stETH - // */ - // function tokensPerStEth() external view returns (uint256); + /** + * @notice Get amount of wstETH for a one stETH + * @return Amount of wstETH for a 1 stETH + */ + function tokensPerStEth() external view returns (uint256); } \ No newline at end of file diff --git a/contracts/token/interfaces/ITokensRateOracle.sol b/contracts/token/interfaces/ITokensRateOracle.sol index 843be503..c28cabc9 100644 --- a/contracts/token/interfaces/ITokensRateOracle.sol +++ b/contracts/token/interfaces/ITokensRateOracle.sol @@ -7,6 +7,8 @@ pragma solidity 0.8.10; /// @notice Oracle interface for two tokens rate interface ITokensRateOracle { + function updateRate(int256 rate, uint256 updatedAt) external; + /** * @notice represents the number of decimals the oracle responses represent. */ @@ -25,4 +27,8 @@ interface ITokensRateOracle { uint256 updatedAt, uint80 answeredInRound ); +} + +interface ITokensRateOracleUpdatable { + function updateRate(int256 rate, uint256 updatedAt) external; } \ No newline at end of file diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 71193d4c..6b4ab756 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -80,11 +80,15 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, + tokensRateOracle, + l1Provider } = ctx; const { accountA: tokenHolderA } = ctx.accounts; const { depositAmount: depositAmountInRebasableTokens } = ctx.common; - const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); + const tokensPerStEth = await l1Token.tokensPerStEth(); + + await tokensRateOracle.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); await l1TokenRebasable .connect(tokenHolderA.l1Signer) @@ -108,13 +112,19 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "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 = tokensPerStEthStr + blockTimestampStr.slice(2); + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x01", + dataToSend, ]); const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( @@ -125,7 +135,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x01", + dataToSend, ] ); @@ -152,15 +162,27 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("Finalize deposit on L2", async (ctx) => { const { + l1Token, l1TokenRebasable, l2Token, l2TokenRebasable, l1ERC20TokenBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, + tokensRateOracle, + l2Provider } = ctx; const { depositAmount: depositAmountInRebasableTokens } = ctx.common; const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); + 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 = tokensPerStEthStr + blockTimestampStr.slice(2); + const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = ctx.accounts; @@ -186,12 +208,16 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x01", + dataToReceive, ]), { gasLimit: 5_000_000 } ); - console.log("test2"); + + const [,tokensRate,,,] = await tokensRateOracle.latestRoundData(); + console.log("tokensPerStEth=",tokensPerStEth); + console.log("tokensRate=",tokensRate); + assert.equalBN(tokensPerStEth, tokensRate); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ l1TokenRebasable.address, @@ -250,7 +276,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, withdrawalAmount, - "0x01", + "0x", ]); @@ -302,7 +328,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, withdrawalAmount, - "0x01", + "0x", ] ), 0 @@ -314,7 +340,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, withdrawalAmount, - "0x01", + "0x", ]); assert.equalBN( diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts index 39ce1f7f..4a3625a3 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deployment.ts @@ -174,6 +174,7 @@ export default function deployment( l1TokenRebasable, expectedL2TokenProxyAddress, expectedL2TokenRebasableProxyAddress, + tokensRateOracleStub, options?.overrides, ], afterDeploy: (c) => diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index bb030fcf..721ed54b 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -156,7 +156,7 @@ async function loadDeployedBridges( l2SignerOrProvider: SignerOrProvider ) { return { - l1Token: IERC20__factory.connect( + l1Token: ERC20WrapableStub__factory.connect( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), @@ -164,6 +164,11 @@ async function loadDeployedBridges( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), + tokensRateOracle: TokensRateOracleStub__factory.connect( + testingUtils.env.OPT_L1_TOKEN(), + l1SignerOrProvider + ), + ...connectBridgeContracts( { l2Token: testingUtils.env.OPT_L2_TOKEN(), @@ -197,7 +202,7 @@ async function deployTestBridge( ); const tokensRateOracleStub = await new TokensRateOracleStub__factory(optDeployer).deploy(); - await tokensRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("2000000000000000000")); + await tokensRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); await tokensRateOracleStub.setDecimals(18); await tokensRateOracleStub.setUpdatedAt(100); @@ -247,6 +252,7 @@ async function deployTestBridge( return { l1Token: l1Token.connect(ethProvider), l1TokenRebasable: l1TokenRebasable.connect(ethProvider), + tokensRateOracle: tokensRateOracleStub, ...connectBridgeContracts( { l2Token: optDeployScript.getContractAddress(1),