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 + +LP + +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) + }) +})