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

[WIP] Add Rebasable token to Optimism #58

Draft
wants to merge 65 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
3256d9f
create new contract that represents stETH on L2
kovalgek Oct 10, 2023
e4b2cbd
add wrap/unwrap functions
kovalgek Oct 11, 2023
82038ea
add shares to rebasable token
kovalgek Oct 13, 2023
380caaa
add unit tests
kovalgek Oct 17, 2023
eab8718
add deposit flow for new rebasable token
kovalgek Nov 4, 2023
c3bc0db
add withdraw flow for rebasable token on optimism
kovalgek Nov 5, 2023
eb98d0b
send rate in data and time during deposit flow
kovalgek Nov 9, 2023
800e33f
add possibility to depost 0 tokens
kovalgek Nov 9, 2023
d9ac556
init structs
kovalgek Nov 9, 2023
ddb9ca3
add comments
kovalgek Dec 11, 2023
a73bcf4
add gas test
kovalgek Dec 11, 2023
6d37edc
update gas test
kovalgek Dec 12, 2023
8098fe1
fix tests
kovalgek Dec 16, 2023
3335339
add token rate oracle
kovalgek Dec 20, 2023
86037dc
add tests for new token, renaming
kovalgek Dec 24, 2023
86788b2
simplify oracle, remove warnings
kovalgek Dec 27, 2023
ddffe1f
update unit tests for token rate oracle
kovalgek Dec 28, 2023
7c79d2d
fix integration tests for rebasable token
kovalgek Jan 9, 2024
6174943
Merge branch 'main' into feature/add_stETH_token_to_optimism
kovalgek Jan 9, 2024
f0b891a
add test for push token rate method
kovalgek Jan 9, 2024
e314bb1
unused return values fix
kovalgek Jan 9, 2024
4b56ccf
fix deployment scripts for new oracle and token
kovalgek Jan 9, 2024
3b82da7
add hearbeat to oracle
kovalgek Jan 10, 2024
171db5a
pr fixes from first rount review: apply unstructured storage in token…
kovalgek Jan 22, 2024
76b4ff3
PR fixes: fix comments, rename interface, abstract wst
kovalgek Jan 29, 2024
7abf60f
use real implemnations of non rebasable token and oracle contracts in…
kovalgek Feb 2, 2024
707da9b
update unit tests for bridges
kovalgek Feb 8, 2024
1cb344a
fix evetns + formating
kovalgek Feb 13, 2024
f9463f0
Merge branch 'main' into feature/add_stETH_token_to_optimism
kovalgek Feb 29, 2024
3fd1e94
move token rate to the base contract
kovalgek Mar 6, 2024
66b2501
use inheritance for token rate in l1 bridge, fix e2e tests
kovalgek Mar 6, 2024
7badd9e
add rebasable token e2e tests
kovalgek Mar 6, 2024
72bc8f3
add bridging-to e2e tests for rebasable token
kovalgek Mar 6, 2024
83e292b
add token rate observer
kovalgek Mar 7, 2024
dbf2f92
add observers array
kovalgek Mar 14, 2024
47c50c6
add factory and tests
kovalgek Mar 18, 2024
7e45410
add unit tests for notifier
kovalgek Mar 19, 2024
55111b8
add token rate pusher
kovalgek Mar 22, 2024
d99a52f
update unit tests for token rate oracle
kovalgek Mar 22, 2024
fb3eb40
remove observer array
kovalgek Mar 22, 2024
b167ebd
fix token rate publicher and notifier unit tests
kovalgek Mar 22, 2024
55d2e3f
fix small comments from PR review
kovalgek Mar 22, 2024
13712ca
feat(steth): intermediate work on adding ERC-2612/EIP-1271 permit
arwer13 Mar 27, 2024
6a46704
update optimism sdk version to fix integration tests
kovalgek Mar 29, 2024
b9d5e56
update addresses to run e2e tests
kovalgek Mar 29, 2024
a3f4544
fix integration test
kovalgek Mar 29, 2024
e2c29e0
deployment scripts
kovalgek Apr 1, 2024
d28d53d
Merge branch 'feature/add_stETH_token_to_optimism' into feat/steth-pe…
arwer13 Apr 1, 2024
0d2df1d
add token rate oracle integration tests
kovalgek Apr 1, 2024
8dc246e
fix happy path test for permit for rebasable
arwer13 Apr 2, 2024
ba0b41e
test(rebasable permit): move the rest tests from core stethpermit.tes…
arwer13 Apr 2, 2024
18a1dc5
add e2e tests for oracle
kovalgek Apr 2, 2024
624219c
Merge pull request #74 from lidofinance/feat/steth-permit
kovalgek Apr 2, 2024
5f86091
remove increaseAllowance and decreaseAllowance from tokens
kovalgek Apr 3, 2024
cc868ef
change function names
kovalgek Apr 3, 2024
06d00c7
rename stETH to rebasable token in env
kovalgek Apr 3, 2024
0b22b0f
deploy all script, refactor other deploy scripts
kovalgek Apr 8, 2024
f563fab
add permit to non-rebasable token
kovalgek Apr 9, 2024
3332e30
add unit tests for non-rebasable token
kovalgek Apr 10, 2024
0545e33
PR fixes: fix comments, readme, rename bridges, notifier refactoring
kovalgek Apr 10, 2024
a55b5f0
use mapping for tokens in bridges
kovalgek Apr 15, 2024
f54eecd
fix bridge unit and integration tests, remove mapping in storing toke…
kovalgek Apr 15, 2024
bc1c56f
fix scripts
kovalgek Apr 15, 2024
b62fbee
PR fixes: add comments, rename constracts, more interfaces to contrac…
kovalgek Apr 15, 2024
f387de6
PR fixes: rename tokens, add sanity check to oracle update, add unit …
kovalgek Apr 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add tests for new token, renaming
  • Loading branch information
kovalgek committed Dec 24, 2023
commit 86037dcebc4b61ddf579ea32c76851b1a0e61b7f
4 changes: 2 additions & 2 deletions contracts/optimism/L1ERC20TokenBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import {DepositDataCodec} from "./DepositDataCodec.sol";

import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol";
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
import "hardhat/console.sol";

Check failure on line 19 in contracts/optimism/L1ERC20TokenBridge.sol

View workflow job for this annotation

GitHub Actions / solhint

Unexpected import of console file

// Check if Optimism changed API for bridges. They could depricate methods.
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
// Optimise gas usage with data transfer. Maybe cache rate and see if it changed.
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -71,7 +71,7 @@
whenDepositsEnabled
onlySupportedL1Token(l1Token_)
onlySupportedL2Token(l2Token_)
{
{
if (Address.isContract(msg.sender)) {
revert ErrorSenderNotEOA();
}
Expand Down Expand Up @@ -140,7 +140,7 @@
if (isRebasableTokenFlow(l1Token_, l2Token_)) {

DepositData memory depositData = DepositData({
rate: IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(), // replace by stETHPerToken
rate: IERC20Wrapable(l1TokenNonRebasable).stETHPerToken(),
time: block.timestamp,
data: data_
});
Expand Down
4 changes: 2 additions & 2 deletions contracts/optimism/L2ERC20TokenBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import {CrossDomainEnabled} from "./CrossDomainEnabled.sol";
import {DepositDataCodec} from "./DepositDataCodec.sol";

import { console } from "hardhat/console.sol";

Check failure on line 20 in contracts/optimism/L2ERC20TokenBridge.sol

View workflow job for this annotation

GitHub Actions / solhint

Unexpected import of console file

/// @author psirex
/// @notice The L2 token bridge works with the L1 token bridge to enable ERC20 token bridging
Expand Down Expand Up @@ -110,8 +110,8 @@
{
if (isRebasableTokenFlow(l1Token_, l2Token_)) {
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
DepositData memory depositData = decodeDepositData(data_);
ITokenRateOracle tokensRateOracle = ERC20Rebasable(l2TokenRebasable).tokensRateOracle();
tokensRateOracle.updateRate(int256(depositData.rate), depositData.time);
ITokenRateOracle tokenRateOracle = ERC20Rebasable(l2TokenRebasable).tokenRateOracle();
tokenRateOracle.updateRate(int256(depositData.rate), depositData.time, 0);
ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_);
emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data);
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
} else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) {
Expand Down
2 changes: 1 addition & 1 deletion contracts/stubs/ERC20WrapableStub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 {
return stETHAmount;
}

function tokensPerStEth() external view returns (uint256) {
function stETHPerToken() external view returns (uint256) {
return tokensRate;
}
}
6 changes: 3 additions & 3 deletions contracts/stubs/TokenRateOracleStub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ contract TokenRateOracleStub is ITokenRateOracle {
return latestRoundDataAnswer;
}

function updateRate(int256 rate, uint256 updatedAt) external {
function updateRate(int256 tokenRate_, uint256 rateL1Timestamp_, uint256 lastProcessingRefSlot_) external {
// check timestamp not late as current one.
latestRoundDataAnswer = rate;
latestRoundDataUpdatedAt = updatedAt;
latestRoundDataAnswer = tokenRate_;
latestRoundDataUpdatedAt = rateL1Timestamp_;
}
}
99 changes: 57 additions & 42 deletions contracts/token/ERC20Rebasable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Wrapable} from "./interfaces/IERC20Wrapable.sol";
import {ITokenRateOracle} from "./interfaces/ITokenRateOracle.sol";
import {ERC20Metadata} from "./ERC20Metadata.sol";
import { console } from "hardhat/console.sol";
// import { console } from "hardhat/console.sol";

/// @author kovalgek
/// @notice Extends the ERC20Shared functionality
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -23,24 +23,43 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata {
error ErrorNotEnoughAllowance();
error ErrorAccountIsZeroAddress();
error ErrorDecreasedAllowanceBelowZero();
error ErrorNotBridge();

/// @notice Bridge which can mint and burn tokens on L2.
address public immutable bridge;

/// @notice Contract of non-rebasable token to wrap.
IERC20 public immutable wrappedToken;
ITokenRateOracle public immutable tokensRateOracle;

/// @param wrappedToken_ address of the ERC20 token to wrap
/// @param tokensRateOracle_ address of oracle that returns tokens rate
/// @notice Oracle contract used to get token rate for wrapping/unwrapping tokens.
ITokenRateOracle public immutable tokenRateOracle;

/// @inheritdoc IERC20
mapping(address => mapping(address => uint256)) public allowance;
kovalgek marked this conversation as resolved.
Show resolved Hide resolved

/// @notice Basic unit representing the token holder's share in the total amount of ether controlled by the protocol.
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
mapping (address => uint256) private shares;

/// @notice The total amount of shares in existence.
uint256 private totalShares;

/// @param name_ The name of the token
/// @param symbol_ The symbol of the token
/// @param decimals_ The decimals places of the token
/// @param wrappedToken_ address of the ERC20 token to wrap
/// @param tokenRateOracle_ address of oracle that returns tokens rate
/// @param bridge_ The bridge address which allowd to mint/burn tokens
constructor(
address wrappedToken_,
address tokensRateOracle_,
string memory name_,
string memory symbol_,
uint8 decimals_
uint8 decimals_,
address wrappedToken_,
address tokenRateOracle_,
address bridge_
) ERC20Metadata(name_, symbol_, decimals_) {
wrappedToken = IERC20(wrappedToken_);
tokensRateOracle = ITokenRateOracle(tokensRateOracle_);
tokenRateOracle = ITokenRateOracle(tokenRateOracle_);
bridge = bridge_;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ please consider petrifying implementations with __disable_Initializers() or whatever

Copy link
Contributor

Choose a reason for hiding this comment

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

https://forum.openzeppelin.com/t/what-does-disableinitializers-function-mean/28730

relevant mostly not for this contract, but for others (bridges, might be applicable for TokenRateOracle, whatever)


/// @notice Sets the name and the symbol of the tokens if they both are empty
Expand All @@ -51,8 +70,6 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata {
_setERC20MetadataSymbol(symbol_);
}

/// ------------IERC20Wrapable------------

/// @inheritdoc IERC20Wrapable
function wrap(uint256 sharesAmount_) external returns (uint256) {
if (sharesAmount_ == 0) revert ErrorZeroSharesWrap();
Expand All @@ -75,25 +92,28 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata {
return sharesAmount;
}

function tokensPerStEth() external pure returns (uint256) {
return 0;
/// @inheritdoc IERC20Wrapable
function stETHPerToken() external view returns (uint256) {
return uint256(tokenRateOracle.latestAnswer());
}

// allow call only bridge
function mintShares(address account_, uint256 amount_) external returns (uint256) {
function mintShares(address account_, uint256 amount_) external onlyBridge returns (uint256) {
return _mintShares(account_, amount_);
}

// allow call only bridge
function burnShares(address account_, uint256 amount_) external {
function burnShares(address account_, uint256 amount_) external onlyBridge {
_burnShares(account_, amount_);
}


/// ------------ERC20------------

/// @inheritdoc IERC20
mapping(address => mapping(address => uint256)) public allowance;
/// @dev Validates that sender of the transaction is the bridge
modifier onlyBridge() {
if (msg.sender != bridge) {
revert ErrorNotBridge();
}
_;
}

/// @inheritdoc IERC20
function totalSupply() external view returns (uint256) {
Expand Down Expand Up @@ -212,35 +232,32 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata {
emit Approval(owner_, spender_, amount_);
}


/// ------------Shares------------
// API
function sharesOf(address _account) external view returns (uint256) {
return _sharesOf(_account);
/// @notice Get shares amount of the provided account.
/// @param account_ provided account address.
/// @return amount of shares owned by `_account`.
function sharesOf(address account_) external view returns (uint256) {
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
return _sharesOf(account_);
}

/// @return total amount of shares.
function getTotalShares() external view returns (uint256) {
return _getTotalShares();
}

/// @notice Get amount of tokens for a given amount of shares.
/// @param sharesAmount_ amount of shares.
/// @return amount of tokens for a given shares amount.
function getTokensByShares(uint256 sharesAmount_) external view returns (uint256) {
return _getTokensByShares(sharesAmount_);
}

/// @notice Get amount of shares for a given amount of tokens.
/// @param tokenAmount_ provided tokens amount.
/// @return amount of shares for a given tokens amount.
function getSharesByTokens(uint256 tokenAmount_) external view returns (uint256) {
return _getSharesByTokens(tokenAmount_);
}

function getTokensRateAndDecimal() external view returns (uint256, uint256) {
return _getTokensRateAndDecimal();
}

// private/internal

mapping (address => uint256) private shares;

uint256 private totalShares;

function _sharesOf(address account_) internal view returns (uint256) {
return shares[account_];
}
Expand All @@ -251,32 +268,28 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata {

function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) {
(uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal();
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
return (sharesAmount_ * (10 ** decimals)) / tokensRate;
return (sharesAmount_ * tokensRate) / (10 ** decimals);
Copy link
Contributor

Choose a reason for hiding this comment

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

e2e test is needed to check that decimals dimension stays correct starting from L1 side up to operations with stETH on L2

i.e.,

  • you wrap 1.273 stETH on L1 to X wstETH
  • you wrap 1.273 stETH on L2 to Y wstETH

⇒ invariant: X == Y


the same is for unwrap

}

function _getSharesByTokens(uint256 tokenAmount_) internal view returns (uint256) {
(uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal();
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
return (tokenAmount_ * tokensRate) / (10 ** decimals);
return (tokenAmount_ * (10 ** decimals)) / tokensRate;
}

function _getTokensRateAndDecimal() internal view returns (uint256, uint256) {
uint8 rateDecimals = tokensRateOracle.decimals();
console.log("_getTokensRateAndDecimal1");
uint8 rateDecimals = tokenRateOracle.decimals();

if (rateDecimals == uint8(0) || rateDecimals > uint8(18)) revert ErrorInvalidRateDecimals(rateDecimals);
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
console.log("_getTokensRateAndDecimal2");

(,
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
int256 answer
,
,
uint256 updatedAt
,) = tokensRateOracle.latestRoundData();
console.log("_getTokensRateAndDecimal3");
,) = tokenRateOracle.latestRoundData();

if (updatedAt == 0) revert ErrorWrongOracleUpdateTime();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ 🔴 here we can once again prevent token from minting on L2 when it was bridged from L1
the solution space is:

  • accept the risks
  • mint wstETH instead of stETH

let's outline it in the spec and ask reviewers to weigh in

if (answer <= 0) revert ErrorOracleAnswerIsNegative();
console.log("_getTokensRateAndDecimal4");

return (uint256(answer), uint256(rateDecimals));
}
Expand All @@ -290,6 +303,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata {
) internal onlyNonZeroAccount(recipient_) returns (uint256) {
totalShares = totalShares + amount_;
shares[recipient_] = shares[recipient_] + amount_;
emit Transfer(address(0), recipient_, amount_);
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
return totalShares;
}

Expand All @@ -304,6 +318,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata {
if (accountShares < amount_) revert ErrorNotEnoughBalance();
totalShares = totalShares - amount_;
shares[account_] = accountShares - amount_;
emit Transfer(account_, address(0), amount_);
return totalShares;
}

Expand Down
2 changes: 1 addition & 1 deletion contracts/token/interfaces/IERC20Wrapable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ interface IERC20Wrapable {
* @notice Get amount of wstETH for a one stETH
* @return Amount of wstETH for a 1 stETH
*/
function tokensPerStEth() external view returns (uint256);
function stETHPerToken() external view returns (uint256);
kovalgek marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 1 addition & 1 deletion contracts/token/interfaces/ITokenRateOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ interface ITokenRateOracle {
function decimals() external view returns (uint8);

/// @notice Updates token rate.
function updateRate(int256 rate, uint256 rateL1Timestamp) external;
function updateRate(int256 tokenRate_, uint256 rateL1Timestamp_, uint256 lastProcessingRefSlot_) external;
}
4 changes: 2 additions & 2 deletions test/bridge-executor/optimism.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ async function ctxFactory() {
"TTR"
);

const tokensRateOracleStub = await new TokenRateOracle__factory(l2Deployer).deploy();
const tokenRateOracleStub = await new TokenRateOracle__factory(l2Deployer).deploy();

const optAddresses = optimism.addresses(networkName);

Expand All @@ -243,7 +243,7 @@ async function ctxFactory() {
.erc20TokenBridgeDeployScript(
l1Token.address,
l1TokenRebasable.address,
tokensRateOracleStub.address,
tokenRateOracleStub.address,
{
deployer: l1Deployer,
admins: {
Expand Down
Loading
Loading