diff --git a/.github/workflows/ts-packages.yml b/.github/workflows/ts-packages.yml
index 2bad7c6f70c49..44b246aa11d69 100644
--- a/.github/workflows/ts-packages.yml
+++ b/.github/workflows/ts-packages.yml
@@ -40,7 +40,7 @@ jobs:
- name: Install Dependencies
# only install dependencies if there was a change in the deps
- # if: steps.yarn-cache.outputs.cache-hit != 'true'
+ if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install
- name: Build
@@ -155,7 +155,7 @@ jobs:
- name: Install Dependencies
# only install dependencies if there was a change in the deps
- # if: steps.yarn-cache.outputs.cache-hit != 'true'
+ if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install
# - name: Lint JS and TS
diff --git a/integration-tests/.env.example b/integration-tests/.env.example
index 188301c4eb088..48d991aa2ee07 100644
--- a/integration-tests/.env.example
+++ b/integration-tests/.env.example
@@ -2,5 +2,7 @@
PRIVATE_KEY=
L1_URL=
L2_URL=
+L1_LIQUIDITY_POOL_ADDRESS=
+L2_LIQUIDITY_POOL_ADDRESS=
ADDRESS_MANAGER=
L2_CHAINID=
diff --git a/integration-tests/CHANGELOG.md b/integration-tests/CHANGELOG.md
index dc72a05778dc0..019ff13d053ae 100644
--- a/integration-tests/CHANGELOG.md
+++ b/integration-tests/CHANGELOG.md
@@ -4,6 +4,7 @@
### Patch Changes
+- 1b30c5c6: Add ERC20 and ETH stress tests
- 918c08ca: Bump ethers dependency to 5.4.x to support eip1559
## 0.2.2
diff --git a/integration-tests/contracts/LP/L1LiquidityPool.sol b/integration-tests/contracts/LP/L1LiquidityPool.sol
new file mode 100644
index 0000000000000..184ff5027421f
--- /dev/null
+++ b/integration-tests/contracts/LP/L1LiquidityPool.sol
@@ -0,0 +1,565 @@
+// SPDX-License-Identifier: MIT
+// @unsupported: ovm
+pragma solidity >0.5.0;
+pragma experimental ABIEncoderV2;
+
+import "./interfaces/iL2LiquidityPool.sol";
+import "../libraries/OVM_CrossDomainEnabledFast.sol";
+
+/* External Imports */
+import '@openzeppelin/contracts/math/SafeMath.sol';
+import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol';
+import '@openzeppelin/contracts/access/Ownable.sol';
+
+/**
+ * @dev An L1 LiquidityPool implementation
+ */
+contract L1LiquidityPool is OVM_CrossDomainEnabledFast, Ownable {
+ using SafeMath for uint256;
+ using SafeERC20 for IERC20;
+
+ /**************
+ * Struct *
+ **************/
+ // Info of each user.
+ struct UserInfo {
+ uint256 amount; // How many LP tokens the user has provided.
+ uint256 rewardDebt; // Reward debt. See explanation below.
+ uint256 pendingReward; // Pending reward
+ //
+ // We do some fancy math here. Basically, any point in time, the amount of rewards
+ // entitled to a user but is pending to be distributed is:
+ //
+ // Update Reward Per Share:
+ // accUserRewardPerShare = accUserRewardPerShare + (accUserReward - lastAccUserReward) / userDepositAmount
+ //
+ // LP Provider:
+ // Deposit:
+ // Case 1 (new user):
+ // Update Reward Per Share();
+ // Calculate user.rewardDebt = amount * accUserRewardPerShare;
+ // Case 2 (user who has already deposited add more funds):
+ // Update Reward Per Share();
+ // Calculate user.pendingReward = amount * accUserRewardPerShare - user.rewardDebt;
+ // Calculate user.rewardDebt = (amount + new_amount) * accUserRewardPerShare;
+ //
+ // Withdraw
+ // Update Reward Per Share();
+ // Calculate user.pendingReward = amount * accUserRewardPerShare - user.rewardDebt;
+ // Calculate user.rewardDebt = (amount - withdraw_amount) * accUserRewardPerShare;
+ }
+ // Info of each pool.
+ struct PoolInfo {
+ address l1TokenAddress; // Address of token contract.
+ address l2TokenAddress; // Address of toekn contract.
+
+ // balance
+ uint256 userDepositAmount; // user deposit amount;
+
+ // user rewards
+ uint256 lastAccUserReward; // Last accumulated user reward
+ uint256 accUserReward; // Accumulated user reward.
+ uint256 accUserRewardPerShare; // Accumulated user rewards per share, times 1e12. See below.
+
+ // owner rewards
+ uint256 accOwnerReward; // Accumulated owner reward.
+
+ // start time -- used to calculate APR
+ uint256 startTime;
+ }
+
+ /*************
+ * Variables *
+ *************/
+
+ // mapping L1 and L2 token address to poolInfo
+ mapping(address => PoolInfo) public poolInfo;
+ // Info of each user that stakes tokens.
+ mapping(address => mapping(address => UserInfo)) public userInfo;
+
+ address L2LiquidityPoolAddress;
+ uint256 public totalFeeRate;
+ uint256 public userRewardFeeRate;
+ uint256 public ownerRewardFeeRate;
+ // Default gas value which can be overridden if more complex logic runs on L2.
+ uint32 public DEFAULT_FINALIZE_DEPOSIT_L2_GAS = 1200000;
+ uint256 constant internal SAFE_GAS_STIPEND = 2300;
+
+ /********************
+ * Events *
+ ********************/
+
+ event AddLiquidity(
+ address sender,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event OwnerRecoverFee(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event ClientDepositL1(
+ address sender,
+ uint256 receivedAmount,
+ address tokenAddress
+ );
+
+ event ClientPayL1(
+ address sender,
+ uint256 amount,
+ uint256 userRewardFee,
+ uint256 ownerRewardFee,
+ uint256 totalFee,
+ address tokenAddress
+ );
+
+ event WithdrawLiquidity(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event WithdrawReward(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ /********************
+ * Constructor *
+ ********************/
+ /**
+ * @param _l1CrossDomainMessenger L1 Messenger address being used for sending the cross-chain message.
+ * @param _l1CrossDomainMessengerFast L1 Messenger address being used for relaying cross-chain messages quickly.
+ */
+ constructor (
+ address _l1CrossDomainMessenger,
+ address _l1CrossDomainMessengerFast
+ )
+ OVM_CrossDomainEnabledFast(
+ _l1CrossDomainMessenger,
+ _l1CrossDomainMessengerFast
+ )
+ {}
+
+ /**********************
+ * Function Modifiers *
+ **********************/
+
+ modifier onlyInitialized() {
+ require(address(L2LiquidityPoolAddress) != address(0), "Contract has not yet been initialized");
+ _;
+ }
+
+ /********************
+ * Public Functions *
+ ********************/
+
+
+ /**
+ * @dev Initialize this contract.
+ *
+ * @param _userRewardFeeRate fee rate that users get
+ * @param _ownerRewardFeeRate fee rate that contract owner gets
+ * @param _L2LiquidityPoolAddress Address of the corresponding L2 LP deployed to the L2 chain
+ */
+ function init(
+ uint256 _userRewardFeeRate,
+ uint256 _ownerRewardFeeRate,
+ address _L2LiquidityPoolAddress
+ )
+ public
+ onlyOwner()
+ {
+ totalFeeRate = _userRewardFeeRate + _ownerRewardFeeRate;
+ userRewardFeeRate = _userRewardFeeRate;
+ ownerRewardFeeRate = _ownerRewardFeeRate;
+ L2LiquidityPoolAddress = _L2LiquidityPoolAddress;
+ }
+
+
+ /***
+ * @dev Add the new token pair to the pool
+ * DO NOT add the same LP token more than once. Rewards will be messed up if you do.
+ *
+ * @param _l1TokenAddress
+ * @param _l2TokenAddress
+ *
+ */
+ function registerPool (
+ address _l1TokenAddress,
+ address _l2TokenAddress
+ )
+ public
+ onlyOwner()
+ {
+ // use with caution, can register only once
+ PoolInfo storage pool = poolInfo[_l1TokenAddress];
+ // l2 token address equal to zero, then pair is not registered.
+ require(pool.l2TokenAddress == address(0), "Token Address Already Registered");
+ poolInfo[_l1TokenAddress] =
+ PoolInfo({
+ l1TokenAddress: _l1TokenAddress,
+ l2TokenAddress: _l2TokenAddress,
+ userDepositAmount: 0,
+ lastAccUserReward: 0,
+ accUserReward: 0,
+ accUserRewardPerShare: 0,
+ accOwnerReward: 0,
+ startTime: block.timestamp
+ });
+ }
+
+ /**
+ * @dev Overridable getter for the L2 gas limit, in the case it may be
+ * dynamic, and the above public constant does not suffice.
+ *
+ */
+ function getFinalizeDepositL2Gas()
+ internal
+ view
+ returns(
+ uint32
+ )
+ {
+ return DEFAULT_FINALIZE_DEPOSIT_L2_GAS;
+ }
+
+ /**
+ * Update the user reward per share
+ * @param _tokenAddress Address of the target token.
+ */
+ function updateUserRewardPerShare(
+ address _tokenAddress
+ )
+ public
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+ if (pool.lastAccUserReward < pool.accUserReward) {
+ uint256 accUserRewardDiff = (pool.accUserReward.sub(pool.lastAccUserReward));
+ if (pool.userDepositAmount != 0) {
+ pool.accUserRewardPerShare = pool.accUserRewardPerShare.add(
+ accUserRewardDiff.mul(1e12).div(pool.userDepositAmount)
+ );
+ }
+ pool.lastAccUserReward = pool.accUserReward;
+ }
+ }
+
+ /**
+ * Liquididity providers add liquidity
+ * @param _amount liquidity amount that users want to deposit.
+ * @param _tokenAddress address of the liquidity token.
+ */
+ function addLiquidity(
+ uint256 _amount,
+ address _tokenAddress
+ )
+ external
+ payable
+ {
+ // check whether user sends ETH or ERC20
+ if (msg.value != 0) {
+ // override the _amount and token address
+ _amount = msg.value;
+ _tokenAddress = address(0);
+ }
+
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+ UserInfo storage user = userInfo[_tokenAddress][msg.sender];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Register");
+
+ // Update accUserRewardPerShare
+ updateUserRewardPerShare(_tokenAddress);
+
+ // if the user has already deposited token, we move the rewards to
+ // pendingReward and update the reward debet.
+ if (user.amount > 0) {
+ user.pendingReward = user.pendingReward.add(
+ user.amount.mul(pool.accUserRewardPerShare).div(1e12).sub(user.rewardDebt)
+ );
+ user.rewardDebt = (user.amount.add(_amount)).mul(pool.accUserRewardPerShare).div(1e12);
+ } else {
+ user.rewardDebt = _amount.mul(pool.accUserRewardPerShare).div(1e12);
+ }
+
+ // transfer funds if users deposit ERC20
+ if (_tokenAddress != address(0)) {
+ IERC20(_tokenAddress).safeTransferFrom(msg.sender, address(this), _amount);
+ }
+
+ // update amounts
+ user.amount = user.amount.add(_amount);
+ pool.userDepositAmount = pool.userDepositAmount.add(_amount);
+
+ emit AddLiquidity(
+ msg.sender,
+ _amount,
+ _tokenAddress
+ );
+ }
+
+ /**
+ * Client deposit ERC20 from their account to this contract, which then releases funds on the L2 side
+ * @param _amount amount that client wants to transfer.
+ * @param _tokenAddress L2 token address
+ */
+ function clientDepositL1(
+ uint256 _amount,
+ address _tokenAddress
+ )
+ external
+ payable
+ {
+ // check whether user sends ETH or ERC20
+ if (msg.value != 0) {
+ // override the _amount and token address
+ _amount = msg.value;
+ _tokenAddress = address(0);
+ }
+
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Register");
+
+ // transfer funds if users deposit ERC20
+ if (_tokenAddress != address(0)) {
+ IERC20(_tokenAddress).safeTransferFrom(msg.sender, address(this), _amount);
+ }
+
+ // Construct calldata for L1LiquidityPool.depositToFinalize(_to, receivedAmount)
+ bytes memory data = abi.encodeWithSelector(
+ iL2LiquidityPool.clientPayL2.selector,
+ msg.sender,
+ _amount,
+ pool.l2TokenAddress
+ );
+
+ // Send calldata into L1
+ sendCrossDomainMessage(
+ address(L2LiquidityPoolAddress),
+ getFinalizeDepositL2Gas(),
+ data
+ );
+
+ emit ClientDepositL1(
+ msg.sender,
+ _amount,
+ _tokenAddress
+ );
+ }
+
+ /**
+ * Users withdraw token from LP
+ * @param _amount amount to withdraw
+ * @param _tokenAddress L1 token address
+ * @param _to receiver to get the funds
+ */
+ function withdrawLiquidity(
+ uint256 _amount,
+ address _tokenAddress,
+ address payable _to
+ )
+ external
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+ UserInfo storage user = userInfo[_tokenAddress][msg.sender];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Register");
+ require(user.amount >= _amount, "Withdraw Error");
+
+ // Update accUserRewardPerShare
+ updateUserRewardPerShare(_tokenAddress);
+
+ // calculate all the rewards and set it as pending rewards
+ user.pendingReward = user.pendingReward.add(
+ user.amount.mul(pool.accUserRewardPerShare).div(1e12).sub(user.rewardDebt)
+ );
+ // Update the user data
+ user.amount = user.amount.sub(_amount);
+ // update reward debt
+ user.rewardDebt = user.amount.mul(pool.accUserRewardPerShare).div(1e12);
+ // update total user deposit amount
+ pool.userDepositAmount = pool.userDepositAmount.sub(_amount);
+
+ if (_tokenAddress != address(0)) {
+ IERC20(_tokenAddress).safeTransfer(_to, _amount);
+ } else {
+ (bool sent,) = _to.call{gas: SAFE_GAS_STIPEND, value: _amount}("");
+ require(sent, "Failed to send Ether");
+ }
+
+ emit WithdrawLiquidity(
+ msg.sender,
+ _to,
+ _amount,
+ _tokenAddress
+ );
+ }
+
+ /**
+ * owner recovers fee from ERC20
+ * @param _amount amount that owner wants to recover.
+ * @param _tokenAddress L1 token address
+ * @param _to receiver to get the fee.
+ */
+ function ownerRecoverFee(
+ uint256 _amount,
+ address _tokenAddress,
+ address _to
+ )
+ external
+ onlyOwner()
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Register");
+ require(pool.accOwnerReward >= _amount, "Owner Reward Withdraw Error");
+
+ if (_tokenAddress != address(0)) {
+ IERC20(_tokenAddress).safeTransfer(_to, _amount);
+ } else {
+ (bool sent,) = _to.call{gas: SAFE_GAS_STIPEND, value: _amount}("");
+ require(sent, "Failed to send Ether");
+ }
+
+ pool.accOwnerReward = pool.accOwnerReward.sub(_amount);
+
+ emit OwnerRecoverFee(
+ msg.sender,
+ _to,
+ _amount,
+ _tokenAddress
+ );
+ }
+
+ /**
+ * withdraw reward from ERC20
+ * @param _amount reward amount that liquidity providers want to withdraw
+ * @param _tokenAddress L1 token address
+ * @param _to receiver to get the reward
+ */
+ function withdrawReward(
+ uint256 _amount,
+ address _tokenAddress,
+ address _to
+ )
+ external
+ onlyOwner()
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+ UserInfo storage user = userInfo[_tokenAddress][msg.sender];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Register");
+
+ uint256 pendingReward = user.pendingReward.add(
+ user.amount.mul(pool.accUserRewardPerShare).div(1e12).sub(user.rewardDebt)
+ );
+
+ require(pendingReward >= _amount, "Withdraw Reward Error");
+
+ user.pendingReward = pendingReward.sub(_amount);
+ user.rewardDebt = user.amount.mul(pool.accUserRewardPerShare).div(1e12);
+
+ if (_tokenAddress != address(0)) {
+ IERC20(_tokenAddress).safeTransfer(_to, _amount);
+ } else {
+ (bool sent,) = _to.call{gas: SAFE_GAS_STIPEND, value: _amount}("");
+ require(sent, "Failed to send Ether");
+ }
+
+ emit WithdrawReward(
+ msg.sender,
+ _to,
+ _amount,
+ _tokenAddress
+ );
+ }
+
+ /*************************
+ * Cross-chain Functions *
+ *************************/
+
+ /**
+ * Move funds from L2 to L1, and pay out from the right liquidity pool
+ * @param _to receiver to get the funds
+ * @param _amount amount to to be transferred.
+ * @param _tokenAddress L1 token address
+ */
+ function clientPayL1(
+ address payable _to,
+ uint256 _amount,
+ address _tokenAddress
+ )
+ external
+ onlyFromCrossDomainAccount(address(L2LiquidityPoolAddress))
+ {
+ bool replyNeeded = false;
+
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+ uint256 userRewardFee = (_amount.mul(userRewardFeeRate)).div(1000);
+ uint256 ownerRewardFee = (_amount.mul(ownerRewardFeeRate)).div(1000);
+ uint256 totalFee = userRewardFee.add(ownerRewardFee);
+ uint256 receivedAmount = _amount.sub(totalFee);
+
+ if (_tokenAddress != address(0)) {
+ //IERC20(_tokenAddress).safeTransfer(_to, _amount);
+ if (receivedAmount > IERC20(_tokenAddress).balanceOf(address(this))) {
+ replyNeeded = true;
+ } else {
+ pool.accUserReward = pool.accUserReward.add(userRewardFee);
+ pool.accOwnerReward = pool.accOwnerReward.add(ownerRewardFee);
+ IERC20(_tokenAddress).safeTransfer(_to, receivedAmount);
+ }
+ } else {
+ // //this is ETH
+ // // balances[address(0)] = balances[address(0)].sub(_amount);
+ // //_to.transfer(_amount); UNSAFE
+ // (bool sent,) = _to.call{gas: SAFE_GAS_STIPEND, value: _amount}("");
+ // require(sent, "Failed to send Ether");
+ if (receivedAmount > address(this).balance) {
+ replyNeeded = true;
+ } else {
+ pool.accUserReward = pool.accUserReward.add(userRewardFee);
+ pool.accOwnerReward = pool.accOwnerReward.add(ownerRewardFee);
+ //this is ETH
+ // balances[address(0)] = balances[address(0)].sub(_amount);
+ //_to.transfer(_amount); UNSAFE
+ (bool sent,) = _to.call{gas: SAFE_GAS_STIPEND, value: receivedAmount}("");
+ require(sent, "Failed to send Ether");
+ }
+ }
+
+ if (replyNeeded) {
+ // send cross domain message
+ bytes memory data = abi.encodeWithSelector(
+ iL2LiquidityPool.clientPayL2.selector,
+ _to,
+ _amount,
+ poolInfo[_tokenAddress].l2TokenAddress
+ );
+
+ sendCrossDomainMessage(
+ address(L2LiquidityPoolAddress),
+ getFinalizeDepositL2Gas(),
+ data
+ );
+ } else {
+ emit ClientPayL1(
+ _to,
+ receivedAmount,
+ userRewardFee,
+ ownerRewardFee,
+ totalFee,
+ _tokenAddress
+ );
+ }
+ }
+}
diff --git a/integration-tests/contracts/LP/L2LiquidityPool.sol b/integration-tests/contracts/LP/L2LiquidityPool.sol
new file mode 100644
index 0000000000000..9f4feae57d76c
--- /dev/null
+++ b/integration-tests/contracts/LP/L2LiquidityPool.sol
@@ -0,0 +1,487 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >0.5.0;
+
+import "./interfaces/iL1LiquidityPool.sol";
+
+/* Library Imports */
+import "@eth-optimism/contracts/contracts/optimistic-ethereum/libraries/bridge/OVM_CrossDomainEnabled.sol";
+
+/* External Imports */
+import '@openzeppelin/contracts/math/SafeMath.sol';
+import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol';
+import '@openzeppelin/contracts/access/Ownable.sol';
+
+/**
+ * @dev An L2 LiquidityPool implementation
+ */
+
+contract L2LiquidityPool is OVM_CrossDomainEnabled, Ownable {
+ using SafeERC20 for IERC20;
+ using SafeMath for uint256;
+
+ /**************
+ * Struct *
+ **************/
+ // Info of each user.
+ struct UserInfo {
+ uint256 amount; // How many LP tokens the user has provided.
+ uint256 rewardDebt; // Reward debt. See explanation below.
+ uint256 pendingReward; // Pending reward
+ //
+ // We do some fancy math here. Basically, any point in time, the amount of rewards
+ // entitled to a user but is pending to be distributed is:
+ //
+ // Update Reward Per Share:
+ // accUserRewardPerShare = accUserRewardPerShare + (accUserReward - lastAccUserReward) / userDepositAmount
+ //
+ // LP Provider:
+ // Deposit:
+ // Case 1 (new user):
+ // Update Reward Per Share();
+ // Calculate user.rewardDebt = amount * accUserRewardPerShare;
+ // Case 2 (user who has already deposited add more funds):
+ // Update Reward Per Share();
+ // Calculate user.pendingReward = amount * accUserRewardPerShare - user.rewardDebt;
+ // Calculate user.rewardDebt = (amount + new_amount) * accUserRewardPerShare;
+ //
+ // Withdraw
+ // Update Reward Per Share();
+ // Calculate user.pendingReward = amount * accUserRewardPerShare - user.rewardDebt;
+ // Calculate user.rewardDebt = (amount - withdraw_amount) * accUserRewardPerShare;
+ }
+ // Info of each pool.
+ struct PoolInfo {
+ address l1TokenAddress; // Address of token contract.
+ address l2TokenAddress; // Address of toekn contract.
+
+ // balance
+ uint256 userDepositAmount; // user deposit amount;
+
+ // user rewards
+ uint256 lastAccUserReward; // Last accumulated user reward
+ uint256 accUserReward; // Accumulated user reward.
+ uint256 accUserRewardPerShare; // Accumulated user rewards per share, times 1e12. See below.
+
+ // owner rewards
+ uint256 accOwnerReward; // Accumulated owner reward.
+
+ // start time
+ uint256 startTime;
+ }
+
+ /*************
+ * Variables *
+ *************/
+
+ // mapping L1 and L2 token address to poolInfo
+ mapping(address => PoolInfo) public poolInfo;
+ // Info of each user that stakes tokens.
+ mapping(address => mapping(address => UserInfo)) public userInfo;
+
+ address L1LiquidityPoolAddress;
+ uint256 public totalFeeRate;
+ uint256 public userRewardFeeRate;
+ uint256 public ownerRewardFeeRate;
+ // Default gas value which can be overridden if more complex logic runs on L1.
+ uint32 internal constant DEFAULT_FINALIZE_WITHDRAWAL_L1_GAS = 100000;
+
+ /********************
+ * Event *
+ ********************/
+
+ event AddLiquidity(
+ address sender,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event OwnerRecoverFee(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event ClientDepositL2(
+ address sender,
+ uint256 receivedAmount,
+ address tokenAddress
+ );
+
+ event ClientPayL2(
+ address sender,
+ uint256 amount,
+ uint256 userRewardFee,
+ uint256 ownerRewardFee,
+ uint256 totalFee,
+ address tokenAddress
+ );
+
+ event WithdrawLiquidity(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event WithdrawReward(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ /********************************
+ * Constructor & Initialization *
+ ********************************/
+
+ /**
+ * @param _l2CrossDomainMessenger L1 Messenger address being used for cross-chain communications.
+ */
+ constructor (
+ address _l2CrossDomainMessenger
+ )
+ OVM_CrossDomainEnabled(_l2CrossDomainMessenger)
+ {}
+
+ /**********************
+ * Function Modifiers *
+ **********************/
+
+ modifier onlyInitialized() {
+ require(address(L1LiquidityPoolAddress) != address(0), "Contract has not yet been initialized");
+ _;
+ }
+
+ /********************
+ * Public Functions *
+ ********************/
+
+ /**
+ * @dev Initialize this contract.
+ *
+ * @param _userRewardFeeRate fee rate that users get
+ * @param _ownerRewardFeeRate fee rate that contract owner gets
+ * @param _L1LiquidityPoolAddress Address of the corresponding L1 LP deployed to the main chain
+ */
+ function init(
+ uint256 _userRewardFeeRate,
+ uint256 _ownerRewardFeeRate,
+ address _L1LiquidityPoolAddress
+ )
+ public
+ onlyOwner()
+ {
+ totalFeeRate = _userRewardFeeRate + _ownerRewardFeeRate;
+ userRewardFeeRate = _userRewardFeeRate;
+ ownerRewardFeeRate = _ownerRewardFeeRate;
+ L1LiquidityPoolAddress = _L1LiquidityPoolAddress;
+ }
+
+ /***
+ * @dev Add the new token pair to the pool
+ * DO NOT add the same LP token more than once. Rewards will be messed up if you do.
+ *
+ * @param _l1TokenAddress
+ * @param _l2TokenAddress
+ *
+ */
+ function registerPool (
+ address _l1TokenAddress,
+ address _l2TokenAddress
+ )
+ public
+ onlyOwner()
+ {
+ // use with caution, can register only once
+ PoolInfo storage pool = poolInfo[_l2TokenAddress];
+ // l2 token address equal to zero, then pair is not registered.
+ require(pool.l2TokenAddress == address(0), "Token Address Already Registerd");
+ poolInfo[_l2TokenAddress] =
+ PoolInfo({
+ l1TokenAddress: _l1TokenAddress,
+ l2TokenAddress: _l2TokenAddress,
+ userDepositAmount: 0,
+ lastAccUserReward: 0,
+ accUserReward: 0,
+ accUserRewardPerShare: 0,
+ accOwnerReward: 0,
+ startTime: block.timestamp
+ });
+ }
+
+ /**
+ * @dev Overridable getter for the L1 gas limit of settling the deposit, in the case it may be
+ * dynamic, and the above public constant does not suffice.
+ *
+ */
+ function getFinalizeDepositL1Gas()
+ public
+ view
+ virtual
+ returns(
+ uint32
+ )
+ {
+ return DEFAULT_FINALIZE_WITHDRAWAL_L1_GAS;
+ }
+
+ /**
+ * Update the user reward per share
+ * @param _tokenAddress Address of the target token.
+ */
+ function updateUserRewardPerShare(
+ address _tokenAddress
+ )
+ public
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+ if (pool.lastAccUserReward < pool.accUserReward) {
+ uint256 accUserRewardDiff = (pool.accUserReward.sub(pool.lastAccUserReward));
+ if (pool.userDepositAmount != 0) {
+ pool.accUserRewardPerShare = pool.accUserRewardPerShare.add(
+ accUserRewardDiff.mul(1e12).div(pool.userDepositAmount)
+ );
+ }
+ pool.lastAccUserReward = pool.accUserReward;
+ }
+ }
+
+ /**
+ * Liquididity providers add liquidity
+ * @param _amount liquidity amount that users want to deposit.
+ * @param _tokenAddress address of the liquidity token.
+ */
+ function addLiquidity(
+ uint256 _amount,
+ address _tokenAddress
+ )
+ external
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+ UserInfo storage user = userInfo[_tokenAddress][msg.sender];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Registered");
+
+ // Update accUserRewardPerShare
+ updateUserRewardPerShare(_tokenAddress);
+
+ // if the user has already deposited token, we move the rewards to
+ // pendingReward and update the reward debet.
+ if (user.amount > 0) {
+ user.pendingReward = user.pendingReward.add(
+ user.amount.mul(pool.accUserRewardPerShare).div(1e12).sub(user.rewardDebt)
+ );
+ user.rewardDebt = (user.amount.add(_amount)).mul(pool.accUserRewardPerShare).div(1e12);
+ } else {
+ user.rewardDebt = _amount.mul(pool.accUserRewardPerShare).div(1e12);
+ }
+
+ // transfer funds
+ IERC20(_tokenAddress).safeTransferFrom(msg.sender, address(this), _amount);
+
+ // update amounts
+ user.amount = user.amount.add(_amount);
+ pool.userDepositAmount = pool.userDepositAmount.add(_amount);
+
+ emit AddLiquidity(
+ msg.sender,
+ _amount,
+ _tokenAddress
+ );
+ }
+
+ /**
+ * Client deposit ERC20 from their account to this contract, which then releases funds on the L1 side
+ * @param _amount amount that client wants to transfer.
+ * @param _tokenAddress L2 token address
+ */
+ function clientDepositL2(
+ uint256 _amount,
+ address _tokenAddress
+ )
+ external
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Registered");
+
+ IERC20(_tokenAddress).safeTransferFrom(msg.sender, address(this), _amount);
+
+ // Construct calldata for L1LiquidityPool.depositToFinalize(_to, receivedAmount)
+ bytes memory data = abi.encodeWithSelector(
+ iL1LiquidityPool.clientPayL1.selector,
+ msg.sender,
+ _amount,
+ pool.l1TokenAddress
+ );
+
+ // Send calldata into L1
+ sendCrossDomainMessage(
+ address(L1LiquidityPoolAddress),
+ getFinalizeDepositL1Gas(),
+ data
+ );
+
+ emit ClientDepositL2(
+ msg.sender,
+ _amount,
+ _tokenAddress
+ );
+
+ }
+
+ /**
+ * Users withdraw token from LP
+ * @param _amount amount to withdraw
+ * @param _tokenAddress L2 token address
+ * @param _to receiver to get the funds
+ */
+ function withdrawLiquidity(
+ uint256 _amount,
+ address _tokenAddress,
+ address payable _to
+ )
+ external
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+ UserInfo storage user = userInfo[_tokenAddress][msg.sender];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Registered");
+ require(user.amount >= _amount, "Withdraw Error");
+
+ // Update accUserRewardPerShare
+ updateUserRewardPerShare(_tokenAddress);
+
+ // calculate all the rewards and set it as pending rewards
+ user.pendingReward = user.pendingReward.add(
+ user.amount.mul(pool.accUserRewardPerShare).div(1e12).sub(user.rewardDebt)
+ );
+ // Update the user data
+ user.amount = user.amount.sub(_amount);
+ // update reward debt
+ user.rewardDebt = user.amount.mul(pool.accUserRewardPerShare).div(1e12);
+ // update total user deposit amount
+ pool.userDepositAmount = pool.userDepositAmount.sub(_amount);
+
+ IERC20(_tokenAddress).safeTransfer(_to, _amount);
+
+ emit WithdrawLiquidity(
+ msg.sender,
+ _to,
+ _amount,
+ _tokenAddress
+ );
+ }
+
+ /**
+ * owner recovers fee from ERC20
+ * @param _amount amount to transfer to the other account.
+ * @param _tokenAddress L2 token address
+ * @param _to receiver to get the fee.
+ */
+ function ownerRecoverFee(
+ uint256 _amount,
+ address _tokenAddress,
+ address _to
+ )
+ external
+ onlyOwner()
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Register");
+ require(pool.accOwnerReward >= _amount, "Owner Reward Withdraw Error");
+
+ IERC20(_tokenAddress).safeTransfer(_to, _amount);
+
+ pool.accOwnerReward = pool.accOwnerReward.sub(_amount);
+
+ emit OwnerRecoverFee(
+ msg.sender,
+ _to,
+ _amount,
+ _tokenAddress
+ );
+ }
+
+ /**
+ * withdraw reward from ERC20
+ * @param _amount reward amount that liquidity providers want to withdraw
+ * @param _tokenAddress L2 token address
+ * @param _to receiver to get the reward
+ */
+ function withdrawReward(
+ uint256 _amount,
+ address _tokenAddress,
+ address _to
+ )
+ external
+ onlyOwner()
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+ UserInfo storage user = userInfo[_tokenAddress][msg.sender];
+
+ require(pool.l2TokenAddress != address(0), "Token Address Not Registered");
+
+ uint256 pendingReward = user.pendingReward.add(
+ user.amount.mul(pool.accUserRewardPerShare).div(1e12).sub(user.rewardDebt)
+ );
+
+ require(pendingReward >= _amount, "Withdraw Reward Error");
+
+ user.pendingReward = pendingReward.sub(_amount);
+ user.rewardDebt = user.amount.mul(pool.accUserRewardPerShare).div(1e12);
+
+ IERC20(_tokenAddress).safeTransfer(_to, _amount);
+
+ emit WithdrawReward(
+ msg.sender,
+ _to,
+ _amount,
+ _tokenAddress
+ );
+ }
+
+ /*************************
+ * Cross-chain Functions *
+ *************************/
+
+ /**
+ * Move funds from L1 to L2, and pay out from the right liquidity pool
+ * @param _to receiver to get the funds
+ * @param _amount amount to to be transferred.
+ * @param _tokenAddress L2 token address
+ */
+ function clientPayL2(
+ address _to,
+ uint256 _amount,
+ address _tokenAddress
+ )
+ external
+ onlyInitialized()
+ onlyFromCrossDomainAccount(address(L1LiquidityPoolAddress))
+ {
+ PoolInfo storage pool = poolInfo[_tokenAddress];
+
+ uint256 userRewardFee = (_amount.mul(userRewardFeeRate)).div(1000);
+ uint256 ownerRewardFee = (_amount.mul(ownerRewardFeeRate)).div(1000);
+ uint256 totalFee = userRewardFee.add(ownerRewardFee);
+ uint256 receivedAmount = _amount.sub(totalFee);
+
+ pool.accUserReward = pool.accUserReward.add(userRewardFee);
+ pool.accOwnerReward = pool.accOwnerReward.add(ownerRewardFee);
+
+ IERC20(_tokenAddress).safeTransfer(_to, receivedAmount);
+
+ emit ClientPayL2(
+ _to,
+ receivedAmount,
+ userRewardFee,
+ ownerRewardFee,
+ totalFee,
+ _tokenAddress
+ );
+ }
+
+}
diff --git a/integration-tests/contracts/LP/README.md b/integration-tests/contracts/LP/README.md
new file mode 100644
index 0000000000000..a18528b19c4c9
--- /dev/null
+++ b/integration-tests/contracts/LP/README.md
@@ -0,0 +1,101 @@
+# Liquidity Pool
+
+
+
+The L2 liquidity pool is the main pool. It provides a way to deposit and withdraw tokens for liquidity providers. Swap users can deposit ETH or ERC20 tokens to fastly exit the L2.
+
+The L1 liquidity pool is the sub pool. Swap users can do fast onramp. When swap users do a fast exit via the L2 liquidity pool, it sends funds to the swap users.
+
+For OMGX, there are no delays for users to move funds from L1 to L2. The liquidity pool is used to help users quickly exit L2.
+
+## Calculation
+
+* A deposit 100
+
+ **A info**
+
+ | Deposit Amount | Reward Debet | Pending Reward |
+ | -------------- | ------------ | -------------- |
+ | 100 | 0 | 0 |
+
+ **Pool info**
+
+ | Total Rewards | Reward Per Share | Total Deposit Amount |
+ | ------------- | ---------------- | -------------------- |
+ | 0 | 0 | 100 |
+
+* The pool generates 10 rewards
+
+ **Pool info**
+
+ | Total Rewards | Reward Per Share | Total Deposit Amount |
+ | ------------- | ---------------- | -------------------- |
+ | 10 | 0 | 100 |
+
+* B deposit 100
+
+ We need to update the rewardPerShare first (don't consider the new deposit amount first!)
+
+ **Pool info**
+
+ | Total Rewards | Reward Per Share | Total Deposit Amount |
+ | ------------- | ---------------- | -------------------- |
+ | 10 | 10 / 100 | 100 |
+
+ Calculate the B info
+
+ **B info**
+
+ | Deposit Amount | Reward Debet | Pending Reward |
+ | -------------- | --------------------------------------------------- | -------------- |
+ | 100 | rewardPerShare * depositAmount = 100 * 10/ 100 = 10 | 0 |
+
+ The total deposit amount of the pool is 200 now.
+
+ **pool info**
+
+ | Total Rewards | Reward Per Share | Total Deposit Amount |
+ | ------------- | ---------------- | -------------------- |
+ | 10 | 10 / 100 | 200 |
+
+* The pool generates another 5 rewards
+
+ **Pool info**
+
+ | Total Rewards | Reward Per Share | Total Deposit Amount |
+ | ------------- | ---------------- | -------------------- |
+ | 15 | 10/100 | 200 |
+
+* If A withdraw 100 tokens
+
+ We need to update the rewardPerShare first.
+
+ **Pool info**
+
+ | Total Rewards | Reward Per Share | Total Deposit Amount |
+ | ------------- | ------------------------------------------------------------ | -------------------- |
+ | 15 | 10 / 100 + (increased_rewards) / total_deposit_amount = 10 / 100 + 5 / 200 | 200 |
+
+ The rewards for A is
+
+ ```
+ deposit_amount * reward_per_share - reward_debet = 100 * (10 / 100 + 5 / 200 ) - 0 = 12.5
+ ```
+
+* If B withdraw 100 tokens
+
+ We need to update the rewardPerShare first.
+
+ **Pool info**
+
+ | Total Rewards | Reward Per Share | Total Deposit Amount |
+ | ------------- | ------------------------------------------------------------ | -------------------- |
+ | 15 | 10 / 100 + (increased_rewards) / total_deposit_amount = 10 / 100 + 5 / 200 | 200 |
+
+ The rewards for B is
+
+ ```
+ deposit_amount * reward_per_share - reward_debet = 100 * (10 / 100 + 5 / 200 ) - 10 = 2.5
+ ```
+
+
\ No newline at end of file
diff --git a/integration-tests/contracts/LP/interfaces/iL1LiquidityPool.sol b/integration-tests/contracts/LP/interfaces/iL1LiquidityPool.sol
new file mode 100644
index 0000000000000..74f95bc65bb81
--- /dev/null
+++ b/integration-tests/contracts/LP/interfaces/iL1LiquidityPool.sol
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >0.5.0;
+pragma experimental ABIEncoderV2;
+
+/**
+ * @title iL1LiquidityPool
+ */
+interface iL1LiquidityPool {
+
+ /********************
+ * Events *
+ ********************/
+
+ event AddLiquidity(
+ address sender,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event OwnerRecoverFee(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event ClientDepositL1(
+ address sender,
+ uint256 receivedAmount,
+ address tokenAddress
+ );
+
+ event ClientPayL1(
+ address sender,
+ uint256 amount,
+ uint256 userRewardFee,
+ uint256 ownerRewardFee,
+ uint256 totalFee,
+ address tokenAddress
+ );
+
+ event WithdrawLiquidity(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event WithdrawReward(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ /*************************
+ * Cross-chain Functions *
+ *************************/
+
+ function clientPayL1(
+ address payable _to,
+ uint256 _amount,
+ address _tokenAddress
+ )
+ external;
+}
diff --git a/integration-tests/contracts/LP/interfaces/iL2LiquidityPool.sol b/integration-tests/contracts/LP/interfaces/iL2LiquidityPool.sol
new file mode 100644
index 0000000000000..c0c8cbea2c8e9
--- /dev/null
+++ b/integration-tests/contracts/LP/interfaces/iL2LiquidityPool.sol
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >0.5.0;
+pragma experimental ABIEncoderV2;
+
+/**
+ * @title iL2LiquidityPool
+ */
+interface iL2LiquidityPool {
+
+ /********************
+ * Events *
+ ********************/
+
+ event AddLiquidity(
+ address sender,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event OwnerRecoverFee(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event ClientDepositL2(
+ address sender,
+ uint256 receivedAmount,
+ address tokenAddress
+ );
+
+ event ClientPayL2(
+ address sender,
+ uint256 amount,
+ uint256 userRewardFee,
+ uint256 ownerRewardFee,
+ uint256 totalFee,
+ address tokenAddress
+ );
+
+ event WithdrawLiquidity(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ event WithdrawReward(
+ address sender,
+ address receiver,
+ uint256 amount,
+ address tokenAddress
+ );
+
+ /*************************
+ * Cross-chain Functions *
+ *************************/
+
+ function clientPayL2(
+ address payable _to,
+ uint256 _amount,
+ address _tokenAddress
+ )
+ external;
+}
diff --git a/integration-tests/contracts/libraries/OVM_CrossDomainEnabledFast.sol b/integration-tests/contracts/libraries/OVM_CrossDomainEnabledFast.sol
new file mode 100644
index 0000000000000..fd119f3cb7adb
--- /dev/null
+++ b/integration-tests/contracts/libraries/OVM_CrossDomainEnabledFast.sol
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >0.5.0 <0.8.0;
+/* Interface Imports */
+import { iOVM_CrossDomainMessenger } from "@eth-optimism/contracts/contracts/optimistic-ethereum/iOVM/bridge/messaging/iOVM_CrossDomainMessenger.sol";
+
+/**
+ * @title OVM_CrossDomainEnabledFast
+ * @dev Helper contract for contracts performing cross-domain communications
+ *
+ * Compiler used: defined by inheriting contract
+ * Runtime target: defined by inheriting contract
+ */
+contract OVM_CrossDomainEnabledFast {
+
+ // Messenger contract used to send and receive messages from the other domain.
+ address public senderMessenger;
+ address public relayerMessenger;
+
+ /***************
+ * Constructor *
+ ***************/
+ constructor(
+ address _senderMessenger,
+ address _relayerMessenger
+ ) {
+ senderMessenger = _senderMessenger;
+ relayerMessenger = _relayerMessenger;
+ }
+
+ /**********************
+ * Function Modifiers *
+ **********************/
+
+ /**
+ * @notice Enforces that the modified function is only callable by a specific cross-domain account.
+ * @param _sourceDomainAccount The only account on the originating domain which is authenticated to call this function.
+ */
+ modifier onlyFromCrossDomainAccount(
+ address _sourceDomainAccount
+ ) {
+ require(
+ msg.sender == address(getCrossDomainRelayerMessenger()),
+ "OVM_XCHAIN: messenger contract unauthenticated"
+ );
+
+ require(
+ getCrossDomainRelayerMessenger().xDomainMessageSender() == _sourceDomainAccount,
+ "OVM_XCHAIN: wrong sender of cross-domain message"
+ );
+
+ _;
+ }
+
+ /**********************
+ * Internal Functions *
+ **********************/
+
+ /**
+ * @notice Gets the messenger, usually from storage. This function is exposed in case a child contract needs to override.
+ * @return The address of the cross-domain messenger contract which should be used.
+ */
+ function getCrossDomainSenderMessenger()
+ internal
+ virtual
+ returns(
+ iOVM_CrossDomainMessenger
+ )
+ {
+ return iOVM_CrossDomainMessenger(senderMessenger);
+ }
+
+ /**
+ * @notice Gets the messenger, usually from storage. This function is exposed in case a child contract needs to override.
+ * @return The address of the cross-domain messenger contract which should be used.
+ */
+ function getCrossDomainRelayerMessenger()
+ internal
+ virtual
+ returns(
+ iOVM_CrossDomainMessenger
+ )
+ {
+ return iOVM_CrossDomainMessenger(relayerMessenger);
+ }
+
+ /**
+ * @notice Sends a message to an account on another domain
+ * @param _crossDomainTarget The intended recipient on the destination domain
+ * @param _data The data to send to the target (usually calldata to a function with `onlyFromCrossDomainAccount()`)
+ * @param _gasLimit The gasLimit for the receipt of the message on the target domain.
+ */
+ function sendCrossDomainMessage(
+ address _crossDomainTarget,
+ uint32 _gasLimit,
+ bytes memory _data
+ ) internal {
+ getCrossDomainSenderMessenger().sendMessage(_crossDomainTarget, _data, _gasLimit);
+ }
+}
diff --git a/integration-tests/package.json b/integration-tests/package.json
index cfb0062b3ec70..423f441178c36 100644
--- a/integration-tests/package.json
+++ b/integration-tests/package.json
@@ -15,6 +15,7 @@
"test:integration": "hardhat --network optimism test",
"test:integration:live": "IS_LIVE_NETWORK=true hardhat --network optimism-live test",
"test:sync": "hardhat --network optimism test sync-tests/*.spec.ts --no-compile",
+ "test": "hardhat --network optimism test test/stress-tests-erc20.spec.ts --no-compile",
"clean": "rimraf cache artifacts artifacts-ovm cache-ovm"
},
"devDependencies": {
diff --git a/integration-tests/test/shared/env.ts b/integration-tests/test/shared/env.ts
index 084010346e949..2abec25297f1d 100644
--- a/integration-tests/test/shared/env.ts
+++ b/integration-tests/test/shared/env.ts
@@ -17,10 +17,13 @@ import {
getL1Bridge,
getL2Bridge,
IS_LIVE_NETWORK,
+ L1_LIQUIDITY_POOL_ADDRESS,
+ L2_LIQUIDITY_POOL_ADDRESS,
sleep,
} from './utils'
import {
initWatcher,
+ initWatcherFast,
CrossDomainMessagePair,
Direction,
waitForXDomainTransaction,
@@ -43,11 +46,16 @@ export class OptimismEnv {
// The L1 <> L2 State watcher
watcher: Watcher
+ watcherFast: Watcher
// The wallets
l1Wallet: Wallet
l2Wallet: Wallet
+ // Liquidity pool address
+ l1LiquidityPoolAddress: string
+ l2LiquidityPoolAddress: string
+
constructor(args: any) {
this.addressManager = args.addressManager
this.l1Bridge = args.l1Bridge
@@ -57,8 +65,11 @@ export class OptimismEnv {
this.l2Messenger = args.l2Messenger
this.gasPriceOracle = args.gasPriceOracle
this.watcher = args.watcher
+ this.watcherFast = args.watcherFast
this.l1Wallet = args.l1Wallet
this.l2Wallet = args.l2Wallet
+ this.l1LiquidityPoolAddress = args.l1LiquidityPoolAddress
+ this.l2LiquidityPoolAddress = args.l2LiquidityPoolAddress
this.ctc = args.ctc
this.scc = args.scc
}
@@ -66,6 +77,11 @@ export class OptimismEnv {
static async new(): Promise {
const addressManager = getAddressManager(l1Wallet)
const watcher = await initWatcher(l1Provider, l2Provider, addressManager)
+ const watcherFast = await initWatcherFast(
+ l1Provider,
+ l2Provider,
+ addressManager
+ )
const l1Bridge = await getL1Bridge(l1Wallet, addressManager)
// fund the user if needed
@@ -111,8 +127,11 @@ export class OptimismEnv {
l2Bridge,
l2Messenger,
watcher,
+ watcherFast,
l1Wallet,
l2Wallet,
+ l1LiquidityPoolAddress: L1_LIQUIDITY_POOL_ADDRESS,
+ l2LiquidityPoolAddress: L2_LIQUIDITY_POOL_ADDRESS,
})
}
diff --git a/integration-tests/test/shared/stress-test-helpers.ts b/integration-tests/test/shared/stress-test-helpers.ts
index 0be1eb3e99b80..98a9dfbdaf6ff 100644
--- a/integration-tests/test/shared/stress-test-helpers.ts
+++ b/integration-tests/test/shared/stress-test-helpers.ts
@@ -1,9 +1,10 @@
/* Imports: External */
-import { ethers } from 'ethers'
+import { BigNumber, Contract, ethers } from 'ethers'
+import { predeploys } from '@eth-optimism/contracts'
/* Imports: Internal */
-import { OptimismEnv } from './env'
-import { Direction } from './watcher-utils'
+import { OptimismEnv, useDynamicTimeoutForWithdrawals } from './env'
+import { Direction, CrossDomainMessagePair } from './watcher-utils'
interface TransactionParams {
contract: ethers.Contract
@@ -13,6 +14,8 @@ interface TransactionParams {
// Arbitrary big amount of gas for the L1<>L2 messages.
const MESSAGE_GAS = 8_000_000
+const DEFAULT_TEST_GAS_L1 = 330_000
+const DEFAULT_TEST_GAS_L2 = 1_300_000
export const executeL1ToL2Transactions = async (
env: OptimismEnv,
@@ -20,6 +23,7 @@ export const executeL1ToL2Transactions = async (
) => {
for (const tx of txs) {
const signer = env.l1Wallet //ethers.Wallet.createRandom().connect(env.l1Wallet.provider)
+
const receipt = await env.l1Messenger
.connect(signer)
.sendMessage(
@@ -44,6 +48,7 @@ export const executeL2ToL1Transactions = async (
) => {
for (const tx of txs) {
const signer = env.l2Wallet
+
const receipt = await env.l2Messenger
.connect(signer)
.sendMessage(
@@ -78,6 +83,121 @@ export const executeL2Transactions = async (
}
}
+export const executeDepositErc20 = async (
+ env: OptimismEnv,
+ l1ERC20: Contract,
+ L1LiquidityPool: Contract,
+ txs: number[]
+) => {
+ for (const depositAmount of txs) {
+ // Move tokens from L1 to L2
+ const approveL1TX = await l1ERC20.approve(
+ L1LiquidityPool.address,
+ depositAmount
+ )
+ await approveL1TX.wait()
+
+ const depositTX = await L1LiquidityPool.clientDepositL1(
+ depositAmount,
+ l1ERC20.address,
+ { gasLimit: DEFAULT_TEST_GAS_L1 }
+ )
+
+ await depositTX.wait()
+
+ const [l1ToL2msgHash] = await env.watcher.getMessageHashesFromL1Tx(
+ depositTX.hash
+ )
+
+ await env.watcher.getL2TransactionReceipt(l1ToL2msgHash)
+ }
+}
+
+export const executeWithdrawErc20 = async (
+ env: OptimismEnv,
+ l2ERC20: Contract,
+ L2LiquidityPool: Contract,
+ txs: number[]
+) => {
+ for (const depositAmount of txs) {
+ // Move tokens from L2 to L1
+ const approveL2TX = await l2ERC20.approve(
+ L2LiquidityPool.address,
+ depositAmount
+ )
+ await approveL2TX.wait()
+
+ const withdrawTX = await L2LiquidityPool.clientDepositL2(
+ depositAmount,
+ l2ERC20.address
+ )
+ await withdrawTX.wait()
+
+ const [l2ToL1msgHash] = await env.watcherFast.getMessageHashesFromL2Tx(
+ withdrawTX.hash
+ )
+
+ await env.watcherFast.getL1TransactionReceipt(l2ToL1msgHash)
+ }
+}
+
+export const executeWithdrawETH = async (env: OptimismEnv, txs) => {
+ let totalL1FeePaid: BigNumber = BigNumber.from(0)
+ for (const withdrawAmount of txs) {
+ await useDynamicTimeoutForWithdrawals(this, env)
+ const transaction = await env.l2Bridge.withdraw(
+ predeploys.OVM_ETH,
+ withdrawAmount,
+ DEFAULT_TEST_GAS_L2,
+ '0xFFFF'
+ )
+ await transaction.wait()
+ await env.relayXDomainMessages(transaction)
+ const { tx } = await env.waitForXDomainTransaction(
+ transaction,
+ Direction.L2ToL1
+ )
+ totalL1FeePaid = totalL1FeePaid.add(tx.gasLimit.mul(tx.gasPrice))
+ }
+ return totalL1FeePaid
+}
+
+export const executeDepositETH = async (env: OptimismEnv, txs) => {
+ let totalL1FeePaid: BigNumber = BigNumber.from(0)
+ for (const depositAmount of txs) {
+ const { tx, receipt } = await env.waitForXDomainTransaction(
+ env.l1Bridge.depositETH(DEFAULT_TEST_GAS_L2, '0xFFFF', {
+ value: depositAmount,
+ gasLimit: DEFAULT_TEST_GAS_L1,
+ }),
+ Direction.L1ToL2
+ )
+ totalL1FeePaid = totalL1FeePaid.add(receipt.gasUsed.mul(tx.gasPrice))
+ }
+ return totalL1FeePaid
+}
+
+export const executeDepositETHParallel = async (env: OptimismEnv, txs) => {
+ let totalL1FeePaid: BigNumber = BigNumber.from(0)
+ const result: CrossDomainMessagePair[] = await Promise.all(
+ txs.map(async (depositAmount) => {
+ const receipt = await env.l1Bridge.depositETH(
+ DEFAULT_TEST_GAS_L2,
+ '0xFFFF',
+ {
+ value: depositAmount,
+ gasLimit: DEFAULT_TEST_GAS_L1,
+ }
+ )
+ await env.waitForXDomainTransaction(receipt, Direction.L1ToL2)
+ })
+ )
+ for (const { tx, receipt } of result) {
+ totalL1FeePaid = totalL1FeePaid.add(receipt.gasUsed.mul(tx.gasPrice))
+ }
+ return totalL1FeePaid
+}
+
export const executeRepeatedL1ToL2Transactions = async (
env: OptimismEnv,
tx: TransactionParams,
@@ -111,6 +231,69 @@ export const executeRepeatedL2Transactions = async (
)
}
+export const executeRepeatedDepositErc20 = async (
+ env: OptimismEnv,
+ l1ERC20: Contract,
+ l1LiquidityPool: Contract,
+ depositAmount: number,
+ count: number
+) => {
+ await executeDepositErc20(
+ env,
+ l1ERC20,
+ l1LiquidityPool,
+ [...Array(count).keys()].map(() => depositAmount)
+ )
+}
+
+export const executeRepeatedWithdrawErc20 = async (
+ env: OptimismEnv,
+ l2ERC20: Contract,
+ l2LiquidityPool: Contract,
+ withdrawAmount: number,
+ count: number
+) => {
+ await executeWithdrawErc20(
+ env,
+ l2ERC20,
+ l2LiquidityPool,
+ [...Array(count).keys()].map(() => withdrawAmount)
+ )
+}
+
+export const executeRepeatedWithdrawETH = async (
+ env: OptimismEnv,
+ withdrawAmount: number,
+ count: number
+) => {
+ return executeWithdrawETH(
+ env,
+ [...Array(count).keys()].map(() => withdrawAmount)
+ )
+}
+
+export const executeRepeatedDepositETH = async (
+ env: OptimismEnv,
+ depositAmount: number,
+ count: number
+) => {
+ return executeDepositETH(
+ env,
+ [...Array(count).keys()].map(() => depositAmount)
+ )
+}
+
+export const executeRepeatedDepositETHParallel = async (
+ env: OptimismEnv,
+ depositAmount: number,
+ count: number
+) => {
+ return executeDepositETHParallel(
+ env,
+ [...Array(count).keys()].map(() => depositAmount)
+ )
+}
+
export const executeL1ToL2TransactionsParallel = async (
env: OptimismEnv,
txs: TransactionParams[]
@@ -128,7 +311,7 @@ export const executeL1ToL2TransactionsParallel = async (
),
MESSAGE_GAS,
{
- gasPrice: 0,
+ gasPrice: 0
}
)
diff --git a/integration-tests/test/shared/utils.ts b/integration-tests/test/shared/utils.ts
index b89454043e02b..7d7e19cf5b8ea 100644
--- a/integration-tests/test/shared/utils.ts
+++ b/integration-tests/test/shared/utils.ts
@@ -31,6 +31,12 @@ if (process.env.IS_LIVE_NETWORK === 'true') {
const env = cleanEnv(process.env, {
L1_URL: str({ default: 'http://localhost:9545' }),
L2_URL: str({ default: 'http://localhost:8545' }),
+ L1_LIQUIDITY_POOL_ADDRESS: str({
+ default: '0x4c5859f0F772848b2D91F1D83E2Fe57935348029',
+ }),
+ L2_LIQUIDITY_POOL_ADDRESS: str({
+ default: '0x202CCe504e04bEd6fC0521238dDf04Bc9E8E15aB',
+ }),
VERIFIER_URL: str({ default: 'http://localhost:8547' }),
REPLICA_URL: str({ default: 'http://localhost:8549' }),
L1_POLLING_INTERVAL: num({ default: 10 }),
@@ -76,6 +82,9 @@ export const OVM_ETH_ADDRESS = predeploys.OVM_ETH
export const L2_CHAINID = env.L2_CHAINID
export const IS_LIVE_NETWORK = env.IS_LIVE_NETWORK
+export const L1_LIQUIDITY_POOL_ADDRESS = env.L1_LIQUIDITY_POOL_ADDRESS
+export const L2_LIQUIDITY_POOL_ADDRESS = env.L2_LIQUIDITY_POOL_ADDRESS
+
export const getAddressManager = (provider: any) => {
return getContractFactory('Lib_AddressManager')
.connect(provider)
diff --git a/integration-tests/test/shared/watcher-utils.ts b/integration-tests/test/shared/watcher-utils.ts
index 34c4defec75ce..35aab8d6a4ede 100644
--- a/integration-tests/test/shared/watcher-utils.ts
+++ b/integration-tests/test/shared/watcher-utils.ts
@@ -30,6 +30,36 @@ export const initWatcher = async (
})
}
+export const initWatcherFast = async (
+ l1Provider: JsonRpcProvider,
+ l2Provider: JsonRpcProvider,
+ AddressManager: Contract
+) => {
+ let l1MessengerAddress = await AddressManager.getAddress(
+ 'Proxy__OVM_L1CrossDomainMessengerFast'
+ )
+
+ if (l1MessengerAddress === '0x0000000000000000000000000000000000000000') {
+ l1MessengerAddress = await AddressManager.getAddress(
+ 'OVM_L1CrossDomainMessengerFast'
+ )
+ }
+
+ const l2MessengerAddress = await AddressManager.getAddress(
+ 'OVM_L2CrossDomainMessenger'
+ )
+ return new Watcher({
+ l1: {
+ provider: l1Provider,
+ messengerAddress: l1MessengerAddress,
+ },
+ l2: {
+ provider: l2Provider,
+ messengerAddress: l2MessengerAddress,
+ },
+ })
+}
+
export interface CrossDomainMessagePair {
tx: Transaction
receipt: TransactionReceipt
diff --git a/integration-tests/test/stress-tests-erc20.spec.ts b/integration-tests/test/stress-tests-erc20.spec.ts
new file mode 100644
index 0000000000000..d6a12e6d57c83
--- /dev/null
+++ b/integration-tests/test/stress-tests-erc20.spec.ts
@@ -0,0 +1,173 @@
+import { expect } from 'chai'
+
+/* Imports: External */
+import { BigNumber, Contract, ContractFactory, Wallet } from 'ethers'
+import { ethers } from 'hardhat'
+
+/* Imports: Internal */
+import { OptimismEnv } from './shared/env'
+import * as helpers from './shared/stress-test-helpers'
+
+/* Imports: Artifacts */
+import l1Erc20Json from '../artifacts/contracts/ERC20.sol/ERC20.json'
+import l2Erc20Json from '../artifacts-ovm/contracts/ERC20.sol/ERC20.json'
+import L1LiquidityPoolJson from '../artifacts/contracts/LP/L1LiquidityPool.sol/L1LiquidityPool.json'
+import L2LiquidityPoolJson from '../artifacts-ovm/contracts/LP/L2LiquidityPool.sol/L2LiquidityPool.json'
+
+// Need a big timeout to allow for all transactions to be processed.
+// For some reason I can't figure out how to set the timeout on a per-suite basis
+// so I'm instead setting it for every test.
+const STRESS_TEST_TIMEOUT = 300_000
+
+describe('stress tests', () => {
+ const initialAmount = 1000
+ const tokenName = 'OVM Test'
+ const tokenDecimals = 8
+ const tokenSymbol = 'OVM'
+ let env: OptimismEnv
+ let l1Factory__ERC20: ContractFactory
+ let l2Factory__ERC20: ContractFactory
+ let l1ERC20: Contract
+ let l2ERC20: Contract
+
+ before(async () => {
+ env = await OptimismEnv.new()
+ l1Factory__ERC20 = new ContractFactory(
+ l1Erc20Json.abi,
+ l1Erc20Json.bytecode
+ )
+ l2Factory__ERC20 = new ContractFactory(
+ l2Erc20Json.abi,
+ l2Erc20Json.bytecode
+ )
+ })
+
+ beforeEach(async () => {
+ l1ERC20 = await l1Factory__ERC20
+ .connect(env.l1Wallet)
+ .deploy(initialAmount, tokenName, tokenDecimals, tokenSymbol)
+ await l1ERC20.deployTransaction.wait()
+
+ l2ERC20 = await l2Factory__ERC20
+ .connect(env.l2Wallet)
+ .deploy(initialAmount, tokenName, tokenDecimals, tokenSymbol)
+ await l2ERC20.deployTransaction.wait()
+
+ const L1LiquidityPool = new ethers.Contract(
+ env.l1LiquidityPoolAddress,
+ L1LiquidityPoolJson.abi,
+ env.l1Wallet
+ )
+
+ const L2LiquidityPool = new ethers.Contract(
+ env.l2LiquidityPoolAddress,
+ L2LiquidityPoolJson.abi,
+ env.l2Wallet
+ )
+
+ await L1LiquidityPool
+ // Deployer PRIVATE KEY
+ .connect(new Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", env.l1Wallet.provider))
+ .registerPool(l1ERC20.address, l2ERC20.address)
+
+ await L2LiquidityPool
+ .connect(new Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", env.l2Wallet.provider))
+ .registerPool(l1ERC20.address, l2ERC20.address)
+
+ await l2ERC20.transfer(L2LiquidityPool.address, 500)
+ await l1ERC20.transfer(L1LiquidityPool.address, 500)
+ console.log("DONE beforeEach")
+ })
+
+ describe('ERC20 stress tests', () => {
+ it('deposit ERC20', async () => {
+ const depositAmount = 10
+ const numTransactions = 10
+
+ const l1Symbol = await l1ERC20.symbol()
+ expect(l1Symbol).to.equal(tokenSymbol)
+ const l2Symbol = await l2ERC20.symbol()
+ expect(l2Symbol).to.equal(tokenSymbol)
+
+ const preL1Balance: BigNumber = await l1ERC20.balanceOf(
+ env.l1Wallet.address
+ )
+ const preL2Balance: BigNumber = await l2ERC20.balanceOf(
+ env.l2Wallet.address
+ )
+
+ const L1LiquidityPool = new ethers.Contract(
+ env.l1LiquidityPoolAddress,
+ L1LiquidityPoolJson.abi,
+ env.l1Wallet
+ )
+
+ await helpers.executeRepeatedDepositErc20(
+ env,
+ l1ERC20,
+ L1LiquidityPool,
+ depositAmount,
+ numTransactions
+ )
+
+ const postL1Balance: BigNumber = await l1ERC20.balanceOf(
+ env.l1Wallet.address
+ )
+ const postL2Balance: BigNumber = await l2ERC20.balanceOf(
+ env.l2Wallet.address
+ )
+
+ expect(postL1Balance).to.deep.eq(
+ BigNumber.from(preL1Balance.sub(depositAmount * numTransactions))
+ )
+ expect(postL2Balance).to.deep.eq(
+ BigNumber.from(preL2Balance.add(depositAmount * numTransactions))
+ )
+ }).timeout(STRESS_TEST_TIMEOUT)
+
+ it('withdraw ERC20', async () => {
+ const depositAmount = 10
+ const numTransactions = 10
+
+ const l1Symbol = await l1ERC20.symbol()
+ expect(l1Symbol).to.equal(tokenSymbol)
+ const l2Symbol = await l2ERC20.symbol()
+ expect(l2Symbol).to.equal(tokenSymbol)
+
+ const preL1Balance: BigNumber = await l1ERC20.balanceOf(
+ env.l1Wallet.address
+ )
+ const preL2Balance: BigNumber = await l2ERC20.balanceOf(
+ env.l2Wallet.address
+ )
+
+ const L2LiquidityPool = new ethers.Contract(
+ env.l2LiquidityPoolAddress, // hardcoded
+ L2LiquidityPoolJson.abi,
+ env.l2Wallet
+ )
+
+ await helpers.executeRepeatedWithdrawErc20(
+ env,
+ l2ERC20,
+ L2LiquidityPool,
+ depositAmount,
+ numTransactions
+ )
+
+ const postL1Balance: BigNumber = await l1ERC20.balanceOf(
+ env.l1Wallet.address
+ )
+ const postL2Balance: BigNumber = await l2ERC20.balanceOf(
+ env.l2Wallet.address
+ )
+
+ expect(postL1Balance).to.deep.eq(
+ BigNumber.from(preL1Balance.add(depositAmount * numTransactions))
+ )
+ expect(postL2Balance).to.deep.eq(
+ BigNumber.from(preL2Balance.sub(depositAmount * numTransactions))
+ )
+ }).timeout(STRESS_TEST_TIMEOUT)
+ })
+})
diff --git a/integration-tests/test/stress-tests-eth.spec.ts b/integration-tests/test/stress-tests-eth.spec.ts
new file mode 100644
index 0000000000000..5974090ff3402
--- /dev/null
+++ b/integration-tests/test/stress-tests-eth.spec.ts
@@ -0,0 +1,99 @@
+import { expect } from 'chai'
+
+/* Imports: Internal */
+import { OptimismEnv } from './shared/env'
+import { expectApprox } from './shared/utils'
+import * as helpers from './shared/stress-test-helpers'
+
+// Need a big timeout to allow for all transactions to be processed.
+// For some reason I can't figure out how to set the timeout on a per-suite basis
+// so I'm instead setting it for every test.
+const STRESS_TEST_TIMEOUT = 300_000
+
+describe('stress tests', () => {
+ let env: OptimismEnv
+
+ before(async () => {
+ env = await OptimismEnv.new()
+ })
+
+ describe('ETH stress tests', () => {
+ const getBalances = async (_env: OptimismEnv) => {
+ const l1UserBalance = await _env.l1Wallet.getBalance()
+ const l2UserBalance = await _env.l2Wallet.getBalance()
+
+ const l1BridgeBalance = await _env.l1Wallet.provider.getBalance(
+ _env.l1Bridge.address
+ )
+
+ return {
+ l1UserBalance,
+ l2UserBalance,
+ l1BridgeBalance,
+ }
+ }
+
+ it('deposit ETH', async () => {
+ const numTransactions = 10
+ const depositAmount = 10
+ const preBalances = await getBalances(env)
+
+ const totalL1FeePaid = await helpers.executeRepeatedDepositETH(
+ env,
+ depositAmount,
+ numTransactions
+ )
+
+ const postBalances = await getBalances(env)
+
+ expect(postBalances.l1BridgeBalance).to.deep.eq(
+ preBalances.l1BridgeBalance.add(depositAmount * numTransactions)
+ )
+ expect(postBalances.l2UserBalance).to.deep.eq(
+ preBalances.l2UserBalance.add(depositAmount * numTransactions)
+ )
+ expect(postBalances.l1UserBalance).to.deep.eq(
+ preBalances.l1UserBalance.sub(
+ totalL1FeePaid.add(depositAmount * numTransactions)
+ )
+ )
+ }).timeout(STRESS_TEST_TIMEOUT)
+
+ it('withdraw ETH', async () => {
+ const numTransactions = 10
+
+ const withdrawAmount = 3
+ const preBalances = await getBalances(env)
+
+ expect(
+ preBalances.l2UserBalance.gt(0),
+ 'Cannot run withdrawal test before any deposits...'
+ )
+
+ const fee = await helpers.executeRepeatedWithdrawETH(
+ env,
+ withdrawAmount,
+ numTransactions
+ )
+
+ const postBalances = await getBalances(env)
+
+ // Approximate because there's a fee related to relaying the L2 => L1 message and it throws off the math.
+ expectApprox(
+ postBalances.l1BridgeBalance,
+ preBalances.l1BridgeBalance.sub(withdrawAmount),
+ { upperPercentDeviation: 1 }
+ )
+ expectApprox(
+ postBalances.l2UserBalance,
+ preBalances.l2UserBalance.sub(fee.add(withdrawAmount)),
+ { upperPercentDeviation: 1 }
+ )
+ expectApprox(
+ postBalances.l1UserBalance,
+ preBalances.l1UserBalance.add(withdrawAmount),
+ { upperPercentDeviation: 1 }
+ )
+ }).timeout(STRESS_TEST_TIMEOUT)
+ })
+})