diff --git a/integration-tests/contracts/ERC20.sol b/integration-tests/contracts/ERC20.sol index f404b7eb63269..c67efd462b79a 100644 --- a/integration-tests/contracts/ERC20.sol +++ b/integration-tests/contracts/ERC20.sol @@ -42,7 +42,7 @@ contract ERC20 { function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { uint256 allowance = allowed[_from][msg.sender]; - require(balances[_from] >= _value && allowance >= _value); + require(balances[_from] >= _value && allowance >= _value, "bad allowance"); balances[_to] += _value; balances[_from] -= _value; if (allowance < MAX_UINT256) { @@ -65,4 +65,8 @@ contract ERC20 { function allowance(address _owner, address _spender) public view returns (uint256 remaining) { return allowed[_owner][_spender]; } + + function destroy() public { + selfdestruct(payable(msg.sender)); + } } diff --git a/integration-tests/hardhat.config.ts b/integration-tests/hardhat.config.ts index b92f0a355769f..49445a44c0a0d 100644 --- a/integration-tests/hardhat.config.ts +++ b/integration-tests/hardhat.config.ts @@ -18,18 +18,26 @@ const config: HardhatUserConfig = { timeout: isLiveNetwork() ? 300_000 : 75_000, }, solidity: { - version: '0.8.9', - settings: { - optimizer: { enabled: true, runs: 200 }, - metadata: { - bytecodeHash: 'none', + compilers: [ + { + version: '0.7.6', + settings: {}, }, - outputSelection: { - '*': { - '*': ['storageLayout'], + { + version: '0.8.9', + settings: { + optimizer: { enabled: true, runs: 200 }, + metadata: { + bytecodeHash: 'none', + }, + outputSelection: { + '*': { + '*': ['storageLayout'], + }, + }, }, }, - }, + ], }, gasReporter: { enabled: enableGasReport, diff --git a/integration-tests/package.json b/integration-tests/package.json index 789f982105bc9..ebe66be233ee3 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -31,6 +31,9 @@ "@types/shelljs": "^0.8.8", "@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/parser": "^4.26.0", + "@uniswap/v3-core": "1.0.0", + "@uniswap/v3-periphery": "^1.0.1", + "@uniswap/v3-sdk": "^3.6.2", "babel-eslint": "^10.1.0", "chai": "^4.3.4", "chai-as-promised": "^7.1.1", @@ -50,10 +53,11 @@ "ethers": "^5.4.5", "hardhat": "^2.3.0", "hardhat-gas-reporter": "^1.0.4", + "lint-staged": "11.0.0", "mocha": "^8.4.0", "rimraf": "^3.0.2", "shelljs": "^0.8.4", "typescript": "^4.3.5", - "lint-staged": "11.0.0" + "uniswap-v3-deploy-plugin": "^0.1.0" } } diff --git a/integration-tests/sync-tests/1-sync-verifier.spec.ts b/integration-tests/sync-tests/1-sync-verifier.spec.ts index 0abe8d69f9c5d..4712132a23b77 100644 --- a/integration-tests/sync-tests/1-sync-verifier.spec.ts +++ b/integration-tests/sync-tests/1-sync-verifier.spec.ts @@ -1,4 +1,4 @@ -import chai, { expect } from 'chai' +import { expect } from 'chai' import { Wallet, BigNumber, providers } from 'ethers' import { injectL2Context } from '@eth-optimism/core-utils' diff --git a/integration-tests/sync-tests/2-sync-replica.spec.ts b/integration-tests/sync-tests/2-sync-replica.spec.ts index ae8a108fbd03d..0658c150cc86d 100644 --- a/integration-tests/sync-tests/2-sync-replica.spec.ts +++ b/integration-tests/sync-tests/2-sync-replica.spec.ts @@ -1,4 +1,4 @@ -import chai, { expect } from 'chai' +import { expect } from 'chai' import { Wallet, Contract, ContractFactory, providers } from 'ethers' import { ethers } from 'hardhat' import { injectL2Context } from '@eth-optimism/core-utils' diff --git a/integration-tests/test/basic-l1-l2-communication.spec.ts b/integration-tests/test/basic-l1-l2-communication.spec.ts index 2b62a9c9a727c..2f94ff62eb317 100644 --- a/integration-tests/test/basic-l1-l2-communication.spec.ts +++ b/integration-tests/test/basic-l1-l2-communication.spec.ts @@ -2,14 +2,13 @@ import { expect } from 'chai' /* Imports: External */ import { Contract, ContractFactory } from 'ethers' -import { applyL1ToL2Alias } from '@eth-optimism/core-utils' +import { applyL1ToL2Alias, awaitCondition } from '@eth-optimism/core-utils' /* Imports: Internal */ import simpleStorageJson from '../artifacts/contracts/SimpleStorage.sol/SimpleStorage.json' import l2ReverterJson from '../artifacts/contracts/Reverter.sol/Reverter.json' import { Direction } from './shared/watcher-utils' import { OptimismEnv } from './shared/env' -import { awaitCondition } from '@eth-optimism/core-utils' describe('Basic L1<>L2 Communication', async () => { let Factory__L1SimpleStorage: ContractFactory diff --git a/integration-tests/test/contracts.spec.ts b/integration-tests/test/contracts.spec.ts new file mode 100644 index 0000000000000..f555be27a85bc --- /dev/null +++ b/integration-tests/test/contracts.spec.ts @@ -0,0 +1,205 @@ +import { BigNumber, Contract, ContractFactory, utils, Wallet } from 'ethers' +import { ethers } from 'hardhat' +import { solidity } from 'ethereum-waffle' +import chai, { expect } from 'chai' +import { UniswapV3Deployer } from 'uniswap-v3-deploy-plugin/dist/deployer/UniswapV3Deployer' + +import { OptimismEnv } from './shared/env' +import { FeeAmount, TICK_SPACINGS } from '@uniswap/v3-sdk' + +chai.use(solidity) + +// Below methods taken from the Uniswap test suite, see +// https://github.com/Uniswap/v3-periphery/blob/main/test/shared/ticks.ts +const getMinTick = (tickSpacing: number) => + Math.ceil(-887272 / tickSpacing) * tickSpacing +const getMaxTick = (tickSpacing: number) => + Math.floor(887272 / tickSpacing) * tickSpacing + +describe('Contract interactions', () => { + let env: OptimismEnv + + let Factory__ERC20: ContractFactory + + let otherWallet: Wallet + + before(async () => { + env = await OptimismEnv.new() + + Factory__ERC20 = await ethers.getContractFactory('ERC20', env.l2Wallet) + + otherWallet = Wallet.createRandom().connect(env.l2Wallet.provider) + await env.l2Wallet.sendTransaction({ + to: otherWallet.address, + value: utils.parseEther('0.1'), + }) + }) + + describe('ERC20s', () => { + let contract: Contract + + before(async () => { + Factory__ERC20 = await ethers.getContractFactory('ERC20', env.l2Wallet) + }) + + it('should successfully deploy the contract', async () => { + contract = await Factory__ERC20.deploy(100000000, 'OVM Test', 8, 'OVM') + await contract.deployed() + }) + + it('should support approvals', async () => { + const spender = '0x' + '22'.repeat(20) + const tx = await contract.approve(spender, 1000) + await tx.wait() + let allowance = await contract.allowance(env.l2Wallet.address, spender) + expect(allowance).to.deep.equal(BigNumber.from(1000)) + allowance = await contract.allowance(otherWallet.address, spender) + expect(allowance).to.deep.equal(BigNumber.from(0)) + + const logs = await contract.queryFilter( + contract.filters.Approval(env.l2Wallet.address), + 1, + 'latest' + ) + expect(logs[0].args._owner).to.equal(env.l2Wallet.address) + expect(logs[0].args._spender).to.equal(spender) + expect(logs[0].args._value).to.deep.equal(BigNumber.from(1000)) + }) + + it('should support transferring balances', async () => { + const tx = await contract.transfer(otherWallet.address, 1000) + await tx.wait() + const balFrom = await contract.balanceOf(env.l2Wallet.address) + const balTo = await contract.balanceOf(otherWallet.address) + expect(balFrom).to.deep.equal(BigNumber.from(100000000).sub(1000)) + expect(balTo).to.deep.equal(BigNumber.from(1000)) + + const logs = await contract.queryFilter( + contract.filters.Transfer(env.l2Wallet.address), + 1, + 'latest' + ) + expect(logs[0].args._from).to.equal(env.l2Wallet.address) + expect(logs[0].args._to).to.equal(otherWallet.address) + expect(logs[0].args._value).to.deep.equal(BigNumber.from(1000)) + }) + + it('should support being self destructed', async () => { + const tx = await contract.destroy() + await tx.wait() + const code = await env.l2Wallet.provider.getCode(contract.address) + expect(code).to.equal('0x') + }) + }) + + describe('uniswap', () => { + let contracts: { [name: string]: Contract } + let tokens: Contract[] + + before(async () => { + const tokenA = await Factory__ERC20.deploy(100000000, 'OVM1', 8, 'OVM1') + await tokenA.deployed() + const tokenB = await Factory__ERC20.deploy(100000000, 'OVM2', 8, 'OVM2') + await tokenB.deployed() + tokens = [tokenA, tokenB] + tokens.sort((a, b) => { + if (a.address > b.address) { + return 1 + } + + if (a.address < b.address) { + return -1 + } + + return 0 + }) + + const tx = await tokens[0].transfer(otherWallet.address, 100000) + await tx.wait() + }) + + it('should deploy the Uniswap ecosystem', async () => { + contracts = await UniswapV3Deployer.deploy(env.l2Wallet) + }) + + it('should deploy and initialize a liquidity pool', async () => { + const tx = + await contracts.positionManager.createAndInitializePoolIfNecessary( + tokens[0].address, + tokens[1].address, + FeeAmount.MEDIUM, + // initial ratio of 1/1 + BigNumber.from('79228162514264337593543950336') + ) + await tx.wait() + }) + + it('should approve the contracts', async () => { + for (const wallet of [env.l2Wallet, otherWallet]) { + for (const token of tokens) { + let tx = await token + .connect(wallet) + .approve(contracts.positionManager.address, 100000000) + await tx.wait() + tx = await token + .connect(wallet) + .approve(contracts.router.address, 100000000) + await tx.wait() + } + } + }) + + it('should mint new positions', async () => { + const tx = await contracts.positionManager.mint( + { + token0: tokens[0].address, + token1: tokens[1].address, + tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), + tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), + fee: FeeAmount.MEDIUM, + recipient: env.l2Wallet.address, + amount0Desired: 15, + amount1Desired: 15, + amount0Min: 0, + amount1Min: 0, + deadline: Date.now() * 2, + }, + { + gasLimit: 10000000, + } + ) + await tx.wait() + expect( + await contracts.positionManager.balanceOf(env.l2Wallet.address) + ).to.eq(1) + expect( + await contracts.positionManager.tokenOfOwnerByIndex( + env.l2Wallet.address, + 0 + ) + ).to.eq(1) + }) + + it('should swap', async () => { + const tx = await contracts.router.connect(otherWallet).exactInputSingle( + { + tokenIn: tokens[0].address, + tokenOut: tokens[1].address, + fee: FeeAmount.MEDIUM, + recipient: otherWallet.address, + deadline: Date.now() * 2, + amountIn: 10, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0, + }, + { + gasLimit: 10000000, + } + ) + await tx.wait() + expect(await tokens[1].balanceOf(otherWallet.address)).to.deep.equal( + BigNumber.from('5') + ) + }) + }) +}) diff --git a/integration-tests/test/shared/utils.ts b/integration-tests/test/shared/utils.ts index a5cb247478f6b..a2af2af00728d 100644 --- a/integration-tests/test/shared/utils.ts +++ b/integration-tests/test/shared/utils.ts @@ -174,22 +174,6 @@ export const waitForL2Geth = async ( return injectL2Context(provider) } -export const awaitCondition = async ( - cond: () => Promise, - rate = 1000, - attempts = 10 -) => { - for (let i = 0; i < attempts; i++) { - const ok = await cond() - if (ok) { - return - } - - await sleep(rate) - } - throw new Error('Timed out.') -} - export const gasPriceForL2 = async () => { if (isLiveNetwork()) { return Promise.resolve(BigNumber.from(10000)) diff --git a/yarn.lock b/yarn.lock index dd01d794d5b0f..39af55bc4addc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3192,6 +3192,19 @@ tiny-invariant "^1.1.0" tiny-warning "^1.0.3" +"@uniswap/v3-sdk@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.6.2.tgz#45fa659f7642e8807cb36939e4426355c7a5943c" + integrity sha512-RHJaFfO6+ugI0+v0xhGXuVadmJ9bTbAz/RnuS/xRpXrRIsLNRVC42bYZ3Ci0JPWhkaNygCcc3LJ36Bs6GganSg== + dependencies: + "@ethersproject/abi" "^5.0.12" + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "^3.0.1" + "@uniswap/v3-periphery" "^1.1.1" + "@uniswap/v3-staker" "1.0.0" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@uniswap/v3-staker@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@uniswap/v3-staker/-/v3-staker-1.0.0.tgz#9a6915ec980852479dfc903f50baf822ff8fa66e" @@ -5094,6 +5107,16 @@ cli-table3@^0.5.0: optionalDependencies: colors "^1.1.2" +cli-table3@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" + integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== + dependencies: + object-assign "^4.1.0" + string-width "^4.2.0" + optionalDependencies: + colors "^1.1.2" + cli-table@^0.3.1: version "0.3.6" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc" @@ -15258,6 +15281,15 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +uniswap-v3-deploy-plugin@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/uniswap-v3-deploy-plugin/-/uniswap-v3-deploy-plugin-0.1.0.tgz#dd1798106373c16108aa2b5a0ec3b176c5c51d43" + integrity sha512-goowszquDWierss0oCB91MoQY2W1ue1fixA7XrxPQbVU0pt7ZGUv2KvxqYtcjpey9AcJrveGJoGYvpSLxmkzaw== + dependencies: + "@uniswap/v3-core" "1.0.0" + "@uniswap/v3-periphery" "^1.0.1" + cli-table3 "^0.6.0" + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"