diff --git a/.changeset/few-cameras-march.md b/.changeset/few-cameras-march.md new file mode 100644 index 0000000000000..4a1158aaedcb0 --- /dev/null +++ b/.changeset/few-cameras-march.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/contracts-periphery': minor +--- + +Releases the first version of the contracts-periphery package diff --git a/.circleci/config.yml b/.circleci/config.yml index f1bfa2699262c..c816201f9c9b1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -87,6 +87,7 @@ jobs: - node_modules - packages/common-ts/node_modules - packages/contracts/node_modules + - packages/contracts-periphery/node_modules - packages/core-utils/node_modules - packages/data-transport-layer/node_modules - packages/fault-detector/node_modules @@ -141,6 +142,24 @@ jobs: command: yarn test:coverage working_directory: packages/contracts + contracts-periphery-tests: + docker: + - image: ethereumoptimism/js-builder:latest + resource_class: xlarge + steps: + - restore_cache: + keys: + - v2-cache-yarn-build-{{ .Revision }} + - checkout + - run: + name: Lint + command: yarn lint:check + working_directory: packages/contracts-periphery + - run: + name: Test + command: yarn test:coverage + working_directory: packages/contracts-periphery + dtl-tests: docker: - image: ethereumoptimism/js-builder:latest @@ -178,6 +197,10 @@ jobs: name: Check contracts command: npx depcheck working_directory: packages/contracts + - run: + name: Check contracts-periphery + command: npx depcheck + working_directory: packages/contracts-periphery - run: name: Check core-utils command: npx depcheck @@ -489,6 +512,9 @@ workflows: - contracts-tests: requires: - yarn-monorepo + - contracts-periphery-tests: + requires: + - yarn-monorepo - js-lint-test: name: dtl-tests package_name: data-transport-layer diff --git a/.github/labeler.yml b/.github/labeler.yml index 8158e7e8d87d0..e6a363ac6bb56 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -5,6 +5,7 @@ - 'ops/**/*' - 'packages/batch-submitter/**/*' - 'packages/contracts/**/*' + - 'packages/contracts-periphery/**/*' - 'packages/data-transport-layer/**/*' - 'packages/message-relayer/**/*' - 'packages/fault-detector/**/*' @@ -25,6 +26,9 @@ M-batch-submitter: M-contracts: - any: ['packages/contracts/**/*'] +M-contracts-periphery: + - any: ['packages/contracts-periphery/**/*'] + M-core-utils: - any: ['packages/core-utils/**/*'] diff --git a/.gitignore b/.gitignore index 127e0bd22b787..a19f2c8229fe5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ packages/contracts/coverage* packages/contracts/@ens* packages/contracts/@openzeppelin* packages/contracts/hardhat* +packages/contracts-periphery/coverage* +packages/contracts-periphery/@openzeppelin* +packages/contracts-periphery/hardhat* packages/data-transport-layer/db diff --git a/.vscode/settings.json b/.vscode/settings.json index 90e0d12edc7a1..403c9aedc74c6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ {"directory": "packages/core-utils", "changeProcessCWD": true }, {"directory": "packages/common-ts", "changeProcessCWD": true }, {"directory": "packages/contracts", "changeProcessCWD": true }, + {"directory": "packages/contracts-periphery", "changeProcessCWD": true }, {"directory": "packages/data-transport-layer", "changeProcessCWD": true }, {"directory": "packages/batch-submitter", "changeProcessCWD": true }, {"directory": "packages/message-relayer", "changeProcessCWD": true }, diff --git a/README.md b/README.md index 438c9ce19a67a..274d54eeebf2f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ root ├── packages │ ├── common-ts: Common tools for building apps in TypeScript │ ├── contracts: L1 and L2 smart contracts for Optimism +│ ├── contracts-periphery: Peripheral contracts for Optimism │ ├── core-utils: Low-level utilities that make building Optimism easier │ ├── data-transport-layer: Service for indexing Optimism-related L1 data │ ├── fault-detector: diff --git a/ops/docker/Dockerfile.packages b/ops/docker/Dockerfile.packages index 80e924161ad98..0e1c83160ffd3 100644 --- a/ops/docker/Dockerfile.packages +++ b/ops/docker/Dockerfile.packages @@ -15,6 +15,7 @@ COPY packages/sdk/package.json ./packages/sdk/package.json COPY packages/core-utils/package.json ./packages/core-utils/package.json COPY packages/common-ts/package.json ./packages/common-ts/package.json COPY packages/contracts/package.json ./packages/contracts/package.json +COPY packages/contracts-periphery/package.json ./packages/contracts-periphery/package.json COPY packages/data-transport-layer/package.json ./packages/data-transport-layer/package.json COPY packages/message-relayer/package.json ./packages/message-relayer/package.json COPY packages/fault-detector/package.json ./packages/fault-detector/package.json diff --git a/packages/contracts-periphery/.depcheckrc b/packages/contracts-periphery/.depcheckrc new file mode 100644 index 0000000000000..70dde5b688f2f --- /dev/null +++ b/packages/contracts-periphery/.depcheckrc @@ -0,0 +1,10 @@ +ignores: [ + "@openzeppelin/contracts", + "@rari-capital/solmate", + "@types/node", + "hardhat-deploy", + "ts-node", + "typescript", + "prettier-plugin-solidity", + "solhint-plugin-prettier", +] diff --git a/packages/contracts-periphery/.env.example b/packages/contracts-periphery/.env.example new file mode 100644 index 0000000000000..8886a2b0890b8 --- /dev/null +++ b/packages/contracts-periphery/.env.example @@ -0,0 +1,5 @@ +# Etherscan API key for Ethereum and Ethereum testnets +ETHERSCAN_API_KEY= + +# Etherscan API key for Optimism and Optimism testnets +OPTIMISTIC_ETHERSCAN_API_KEY= diff --git a/packages/contracts-periphery/.eslintrc.js b/packages/contracts-periphery/.eslintrc.js new file mode 100644 index 0000000000000..bfd2057be80bd --- /dev/null +++ b/packages/contracts-periphery/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: '../../.eslintrc.js', +} diff --git a/packages/contracts-periphery/.gitattributes b/packages/contracts-periphery/.gitattributes new file mode 100644 index 0000000000000..52031de51c4ca --- /dev/null +++ b/packages/contracts-periphery/.gitattributes @@ -0,0 +1 @@ +*.sol linguist-language=Solidity diff --git a/packages/contracts-periphery/.gitignore b/packages/contracts-periphery/.gitignore new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/contracts-periphery/.lintstagedrc.yml b/packages/contracts-periphery/.lintstagedrc.yml new file mode 100644 index 0000000000000..4f8b363c2db4b --- /dev/null +++ b/packages/contracts-periphery/.lintstagedrc.yml @@ -0,0 +1,4 @@ +"*.{ts,js}": + - eslint +"*.sol": + - yarn solhint -f table diff --git a/packages/contracts-periphery/.prettierignore b/packages/contracts-periphery/.prettierignore new file mode 100644 index 0000000000000..4ebc8aea50e0a --- /dev/null +++ b/packages/contracts-periphery/.prettierignore @@ -0,0 +1 @@ +coverage diff --git a/packages/contracts-periphery/.prettierrc.js b/packages/contracts-periphery/.prettierrc.js new file mode 100644 index 0000000000000..2d293bab89253 --- /dev/null +++ b/packages/contracts-periphery/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('../../.prettierrc.js'), +} diff --git a/packages/contracts-periphery/.solcover.js b/packages/contracts-periphery/.solcover.js new file mode 100644 index 0000000000000..cc8f6f4b88395 --- /dev/null +++ b/packages/contracts-periphery/.solcover.js @@ -0,0 +1,9 @@ +module.exports = { + skipFiles: [ + './test-libraries', + ], + mocha: { + grep: "@skip-on-coverage", + invert: true + } +}; diff --git a/packages/contracts-periphery/.solhint.json b/packages/contracts-periphery/.solhint.json new file mode 100644 index 0000000000000..23c3d0b24c7bf --- /dev/null +++ b/packages/contracts-periphery/.solhint.json @@ -0,0 +1,18 @@ +{ + "extends": "solhint:recommended", + "plugins": ["prettier"], + "rules": { + "prettier/prettier": "error", + "compiler-version": "off", + "code-complexity": ["warn", 5], + "max-line-length": ["error", 100], + "func-param-name-mixedcase": "error", + "modifier-name-mixedcase": "error", + "ordering": "warn", + "not-rely-on-time": "off", + "no-complex-fallback": "off", + "not-rely-on-block-hash": "off", + "reentrancy": "off", + "contract-name-camelcase": "off" + } +} diff --git a/packages/contracts-periphery/.solhintignore b/packages/contracts-periphery/.solhintignore new file mode 100644 index 0000000000000..3c3629e647f5d --- /dev/null +++ b/packages/contracts-periphery/.solhintignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/contracts-periphery/LICENSE b/packages/contracts-periphery/LICENSE new file mode 100644 index 0000000000000..6a7da5218bb25 --- /dev/null +++ b/packages/contracts-periphery/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright 2020-2021 Optimism + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/contracts-periphery/README.md b/packages/contracts-periphery/README.md new file mode 100644 index 0000000000000..f8b8b2d7405c5 --- /dev/null +++ b/packages/contracts-periphery/README.md @@ -0,0 +1 @@ +# Optimism Peripheral Smart Contracts diff --git a/packages/contracts-periphery/codechecks.yml b/packages/contracts-periphery/codechecks.yml new file mode 100644 index 0000000000000..e163cc5b2f820 --- /dev/null +++ b/packages/contracts-periphery/codechecks.yml @@ -0,0 +1,7 @@ +checks: + - name: eth-gas-reporter/codechecks +settings: + speculativeBranchSelection: false + branches: + - develop + - master diff --git a/packages/contracts-periphery/contracts/testing/helpers/TestERC20.sol b/packages/contracts-periphery/contracts/testing/helpers/TestERC20.sol new file mode 100644 index 0000000000000..72585e0876c06 --- /dev/null +++ b/packages/contracts-periphery/contracts/testing/helpers/TestERC20.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestERC20 is ERC20 { + constructor() ERC20("TEST", "TST") {} + + function mint(address to, uint256 value) public { + _mint(to, value); + } +} diff --git a/packages/contracts-periphery/contracts/testing/helpers/TestERC721.sol b/packages/contracts-periphery/contracts/testing/helpers/TestERC721.sol new file mode 100644 index 0000000000000..fda40a386dc9a --- /dev/null +++ b/packages/contracts-periphery/contracts/testing/helpers/TestERC721.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract TestERC721 is ERC721 { + constructor() ERC721("TEST", "TST") {} + + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } +} diff --git a/packages/contracts-periphery/contracts/universal/RetroReceiver.sol b/packages/contracts-periphery/contracts/universal/RetroReceiver.sol new file mode 100644 index 0000000000000..25ebb5c550ac6 --- /dev/null +++ b/packages/contracts-periphery/contracts/universal/RetroReceiver.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { Owned } from "@rari-capital/solmate/src/auth/Owned.sol"; +import { ERC20 } from "@rari-capital/solmate/src/tokens/ERC20.sol"; +import { ERC721 } from "@rari-capital/solmate/src/tokens/ERC721.sol"; + +/** + * @title RetroReceiver + * @notice RetroReceiver is a minimal contract for receiving funds, meant to be deployed at the + * same address on every chain that supports EIP-2470. + */ +contract RetroReceiver is Owned { + /** + * Emitted when ETH is received by this address. + */ + event ReceivedETH(address indexed from, uint256 amount); + + /** + * Emitted when ETH is withdrawn from this address. + */ + event WithdrewETH(address indexed withdrawer, address indexed recipient, uint256 amount); + + /** + * Emitted when ERC20 tokens are withdrawn from this address. + */ + event WithdrewERC20( + address indexed withdrawer, + address indexed recipient, + address indexed asset, + uint256 amount + ); + + /** + * Emitted when ERC721 tokens are withdrawn from this address. + */ + event WithdrewERC721( + address indexed withdrawer, + address indexed recipient, + address indexed asset, + uint256 id + ); + + /** + * @param _owner Address to initially own the contract. + */ + constructor(address _owner) Owned(_owner) {} + + /** + * Make sure we can receive ETH. + */ + receive() external payable { + emit ReceivedETH(msg.sender, msg.value); + } + + /** + * Withdraws full ETH balance to the recipient. + * + * @param _to Address to receive the ETH balance. + */ + function withdrawETH(address payable _to) public onlyOwner { + withdrawETH(_to, address(this).balance); + } + + /** + * Withdraws partial ETH balance to the recipient. + * + * @param _to Address to receive the ETH balance. + * @param _amount Amount of ETH to withdraw. + */ + function withdrawETH(address payable _to, uint256 _amount) public onlyOwner { + _to.transfer(_amount); + emit WithdrewETH(msg.sender, _to, _amount); + } + + /** + * Withdraws full ERC20 balance to the recipient. + * + * @param _asset ERC20 token to withdraw. + * @param _to Address to receive the ERC20 balance. + */ + function withdrawERC20(ERC20 _asset, address _to) public onlyOwner { + withdrawERC20(_asset, _to, _asset.balanceOf(address(this))); + } + + /** + * Withdraws partial ERC20 balance to the recipient. + * + * @param _asset ERC20 token to withdraw. + * @param _to Address to receive the ERC20 balance. + * @param _amount Amount of ERC20 to withdraw. + */ + function withdrawERC20( + ERC20 _asset, + address _to, + uint256 _amount + ) public onlyOwner { + _asset.transfer(_to, _amount); + emit WithdrewERC20(msg.sender, _to, address(_asset), _amount); + } + + /** + * Withdraws ERC721 token to the recipient. + * + * @param _asset ERC721 token to withdraw. + * @param _to Address to receive the ERC721 token. + * @param _id Token ID of the ERC721 token to withdraw. + */ + function withdrawERC721( + ERC721 _asset, + address _to, + uint256 _id + ) public onlyOwner { + _asset.transferFrom(address(this), _to, _id); + emit WithdrewERC721(msg.sender, _to, address(_asset), _id); + } +} diff --git a/packages/contracts-periphery/hardhat.config.ts b/packages/contracts-periphery/hardhat.config.ts new file mode 100644 index 0000000000000..32a36362f2d33 --- /dev/null +++ b/packages/contracts-periphery/hardhat.config.ts @@ -0,0 +1,64 @@ +import { HardhatUserConfig } from 'hardhat/types' +import { getenv } from '@eth-optimism/core-utils' +import * as dotenv from 'dotenv' + +// Hardhat plugins +import '@nomiclabs/hardhat-ethers' +import '@nomiclabs/hardhat-waffle' +import '@nomiclabs/hardhat-etherscan' +import 'solidity-coverage' + +// Hardhat tasks +import './tasks' + +// Load environment variables from .env +dotenv.config() + +const config: HardhatUserConfig = { + networks: { + optimism: { + chainId: 10, + url: 'https://mainnet.optimsim.io', + }, + opkovan: { + chainId: 69, + url: 'https://kovan.optimism.io', + }, + mainnet: { + chainId: 1, + url: 'https://rpc.ankr.com/eth', + }, + }, + mocha: { + timeout: 50000, + }, + solidity: { + compilers: [ + { + version: '0.8.9', + settings: { + optimizer: { enabled: true, runs: 10_000 }, + }, + }, + ], + settings: { + metadata: { + bytecodeHash: 'none', + }, + outputSelection: { + '*': { + '*': ['metadata', 'storageLayout'], + }, + }, + }, + }, + etherscan: { + apiKey: { + mainnet: getenv('ETHERSCAN_API_KEY'), + optimisticEthereum: getenv('OPTIMISTIC_ETHERSCAN_API_KEY'), + optimisticKovan: getenv('OPTIMISTIC_ETHERSCAN_API_KEY'), + }, + }, +} + +export default config diff --git a/packages/contracts-periphery/package.json b/packages/contracts-periphery/package.json new file mode 100644 index 0000000000000..fea70058a1887 --- /dev/null +++ b/packages/contracts-periphery/package.json @@ -0,0 +1,94 @@ +{ + "name": "@eth-optimism/contracts-periphery", + "version": "0.0.1", + "description": "[Optimism] External (out-of-protocol) L1 and L2 smart contracts for Optimism", + "main": "dist/index", + "types": "dist/index", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "dist/types/*.ts", + "artifacts/contracts/**/*.json", + "deployments/**/*.json", + "L1", + "L2", + "libraries", + "standards" + ], + "scripts": { + "build": "yarn build:contracts", + "build:contracts": "hardhat compile --show-stack-traces", + "test": "yarn test:contracts", + "test:contracts": "hardhat test --show-stack-traces", + "test:coverage": "NODE_OPTIONS=--max_old_space_size=8192 hardhat coverage && istanbul check-coverage --statements 90 --branches 84 --functions 88 --lines 90", + "lint:ts:check": "eslint . --max-warnings=0", + "lint:contracts:check": "yarn solhint -f table 'contracts/**/*.sol'", + "lint:check": "yarn lint:contracts:check && yarn lint:ts:check", + "lint:ts:fix": "eslint --fix .", + "lint:contracts:fix": "yarn prettier --write 'contracts/**/*.sol'", + "lint:fix": "yarn lint:contracts:fix && yarn lint:ts:fix", + "lint": "yarn lint:fix && yarn lint:check", + "clean": "rm -rf ./dist ./artifacts ./cache ./coverage ./tsconfig.build.tsbuildinfo", + "prepublishOnly": "yarn copyfiles -u 1 -e \"**/test-*/**/*\" \"contracts/**/*\" ./", + "postpublish": "rimraf chugsplash L1 L2 libraries standards", + "prepack": "yarn prepublishOnly", + "postpack": "yarn postpublish", + "pre-commit": "lint-staged" + }, + "keywords": [ + "optimism", + "ethereum", + "contracts", + "periphery", + "solidity" + ], + "homepage": "https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts-periphery#readme", + "license": "MIT", + "author": "Optimism PBC", + "repository": { + "type": "git", + "url": "https://github.com/ethereum-optimism/optimism.git" + }, + "dependencies": { + "@eth-optimism/core-utils": "^0.8.4", + "@openzeppelin/contracts": "4.3.2" + }, + "devDependencies": { + "@ethersproject/hardware-wallets": "^5.5.0", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-etherscan": "^3.0.3", + "@nomiclabs/hardhat-waffle": "^2.0.1", + "@rari-capital/solmate": "^6.3.0", + "@types/chai": "^4.2.18", + "@types/mocha": "^8.2.2", + "@types/node": "^17.0.21", + "@typescript-eslint/eslint-plugin": "^4.26.0", + "@typescript-eslint/parser": "^4.26.0", + "babel-eslint": "^10.1.0", + "chai": "^4.3.4", + "copyfiles": "^2.3.0", + "dotenv": "^10.0.0", + "eslint": "^7.27.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-jsdoc": "^35.1.2", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-react": "^7.24.0", + "eslint-plugin-unicorn": "^32.0.1", + "ethereum-waffle": "^3.3.0", + "ethers": "^5.5.4", + "hardhat": "^2.9.2", + "hardhat-deploy": "^0.9.3", + "istanbul": "^0.4.5", + "lint-staged": "11.0.0", + "mocha": "^8.4.0", + "prettier": "^2.3.1", + "prettier-plugin-solidity": "^1.0.0-beta.18", + "solhint": "^3.3.6", + "solhint-plugin-prettier": "^0.0.5", + "solidity-coverage": "^0.7.17", + "ts-node": "^10.0.0", + "typescript": "^4.6.2" + } +} diff --git a/packages/contracts-periphery/slither.config.json b/packages/contracts-periphery/slither.config.json new file mode 100644 index 0000000000000..5b1bcb5048896 --- /dev/null +++ b/packages/contracts-periphery/slither.config.json @@ -0,0 +1,12 @@ +{ + "detectors_to_exclude": "conformance-to-solidity-naming-conventions,assembly-usage,low-level-calls,block-timestamp,pragma,solc-version,too-many-digits,boolean-equal,missing-zero-check", + "exclude_informational": false, + "exclude_low": false, + "exclude_medium": false, + "exclude_high": false, + "solc_disable_warnings": false, + "hardhat_ignore_compile": false, + "disable_color": false, + "exclude_dependencies": false, + "filter_paths": "@openzeppelin|hardhat|contracts/test-helpers|contracts/test-libraries|contracts/L2/predeploys/WETH9.sol" +} diff --git a/packages/contracts-periphery/tasks/deploy-receiver.ts b/packages/contracts-periphery/tasks/deploy-receiver.ts new file mode 100644 index 0000000000000..6e6d5c9d339f0 --- /dev/null +++ b/packages/contracts-periphery/tasks/deploy-receiver.ts @@ -0,0 +1,91 @@ +import { task } from 'hardhat/config' +import * as types from 'hardhat/internal/core/params/argumentTypes' +import { LedgerSigner } from '@ethersproject/hardware-wallets' + +task('deploy-receiver') + .addParam('creator', 'Creator address', undefined, types.string) + .addParam('owner', 'Owner address', undefined, types.string) + .setAction(async (args, hre) => { + console.log(`connecting to ledger...`) + const signer = new LedgerSigner( + hre.ethers.provider, + 'default', + hre.ethers.utils.defaultPath + ) + + const addr = await signer.getAddress() + if (args.creator !== addr) { + throw new Error(`Incorrect key. Creator ${args.creator}, Signer ${addr}`) + } + + const singleton = new hre.ethers.Contract( + '0xce0042B868300000d44A59004Da54A005ffdcf9f', + [ + { + constant: false, + inputs: [ + { + internalType: 'bytes', + name: '_initCode', + type: 'bytes', + }, + { + internalType: 'bytes32', + name: '_salt', + type: 'bytes32', + }, + ], + name: 'deploy', + outputs: [ + { + internalType: 'address payable', + name: 'createdContract', + type: 'address', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + ], + signer + ) + + const salt = + '0x0000000000000000000000000000000000000000000000000000000000000001' + const code = hre.ethers.utils.hexConcat([ + hre.artifacts.readArtifactSync('RetroReceiver').bytecode, + hre.ethers.utils.defaultAbiCoder.encode(['address'], [addr]), + ]) + + // Predict and connect to the contract address + const receiver = await hre.ethers.getContractAt( + 'RetroReceiver', + await singleton.callStatic.deploy(code, salt, { + gasLimit: 2_000_000, + }), + signer + ) + + console.log(`creating contract: ${receiver.address}...`) + const tx1 = await singleton.deploy(code, salt, { + gasLimit: 2_000_000, + }) + + console.log(`waiting for tx: ${tx1.hash}...`) + await tx1.wait() + + console.log(`transferring ownership to: ${args.owner}...`) + const tx2 = await receiver.setOwner(args.owner) + + console.log(`waiting for tx: ${tx2.hash}...`) + await tx2.wait() + + console.log(`verifying contract: ${receiver.address}...`) + await hre.run('verify:verify', { + address: receiver.address, + constructorArguments: [addr], + }) + + console.log(`all done`) + }) diff --git a/packages/contracts-periphery/tasks/index.ts b/packages/contracts-periphery/tasks/index.ts new file mode 100644 index 0000000000000..c6e884e41ae63 --- /dev/null +++ b/packages/contracts-periphery/tasks/index.ts @@ -0,0 +1 @@ +export * from './deploy-receiver' diff --git a/packages/contracts-periphery/test/contracts/universal/RetroReceiver.spec.ts b/packages/contracts-periphery/test/contracts/universal/RetroReceiver.spec.ts new file mode 100644 index 0000000000000..948e16ee49527 --- /dev/null +++ b/packages/contracts-periphery/test/contracts/universal/RetroReceiver.spec.ts @@ -0,0 +1,249 @@ +import hre from 'hardhat' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { Contract } from 'ethers' + +import { expect } from '../../setup' +import { deploy } from '../../helpers' + +describe('RetroReceiver', () => { + const DEFAULT_TOKEN_ID = 0 + const DEFAULT_AMOUNT = hre.ethers.constants.WeiPerEther + const DEFAULT_RECIPIENT = '0x' + '11'.repeat(20) + + let signer1: SignerWithAddress + let signer2: SignerWithAddress + before('signer setup', async () => { + ;[signer1, signer2] = await hre.ethers.getSigners() + }) + + let TestERC20: Contract + let TestERC721: Contract + let RetroReceiver: Contract + beforeEach('deploy contracts', async () => { + TestERC20 = await deploy('TestERC20', { signer: signer1 }) + TestERC721 = await deploy('TestERC721', { signer: signer1 }) + RetroReceiver = await deploy('RetroReceiver', { + signer: signer1, + args: [signer1.address], + }) + }) + + beforeEach('balance setup', async () => { + await TestERC20.mint(signer1.address, hre.ethers.constants.MaxUint256) + await TestERC721.mint(signer1.address, DEFAULT_TOKEN_ID) + await hre.ethers.provider.send('hardhat_setBalance', [ + DEFAULT_RECIPIENT, + '0x0', + ]) + }) + + describe('constructor', () => { + it('should set the owner', async () => { + expect(await RetroReceiver.owner()).to.equal(signer1.address) + }) + }) + + describe('receive', () => { + it('should be able to receive ETH', async () => { + await expect( + signer1.sendTransaction({ + to: RetroReceiver.address, + value: DEFAULT_AMOUNT, + }) + ).to.not.be.reverted + + expect( + await hre.ethers.provider.getBalance(RetroReceiver.address) + ).to.equal(DEFAULT_AMOUNT) + }) + }) + + describe('withdrawETH(address)', () => { + describe('when called by the owner', () => { + it('should withdraw all ETH in the contract', async () => { + await signer1.sendTransaction({ + to: RetroReceiver.address, + value: DEFAULT_AMOUNT, + }) + + await expect(RetroReceiver['withdrawETH(address)'](DEFAULT_RECIPIENT)) + .to.emit(RetroReceiver, 'WithdrewETH') + .withArgs(signer1.address, DEFAULT_RECIPIENT, DEFAULT_AMOUNT) + + expect( + await hre.ethers.provider.getBalance(RetroReceiver.address) + ).to.equal(0) + + expect( + await hre.ethers.provider.getBalance(DEFAULT_RECIPIENT) + ).to.equal(DEFAULT_AMOUNT) + }) + }) + + describe('when called by not the owner', () => { + it('should revert', async () => { + await expect( + RetroReceiver.connect(signer2)['withdrawETH(address)']( + signer2.address + ) + ).to.be.revertedWith('UNAUTHORIZED') + }) + }) + }) + + describe('withdrawETH(address,uint256)', () => { + describe('when called by the owner', () => { + it('should withdraw the given amount of ETH', async () => { + await signer1.sendTransaction({ + to: RetroReceiver.address, + value: DEFAULT_AMOUNT.mul(2), + }) + + await expect( + RetroReceiver['withdrawETH(address,uint256)']( + DEFAULT_RECIPIENT, + DEFAULT_AMOUNT + ) + ) + .to.emit(RetroReceiver, 'WithdrewETH') + .withArgs(signer1.address, DEFAULT_RECIPIENT, DEFAULT_AMOUNT) + + expect( + await hre.ethers.provider.getBalance(RetroReceiver.address) + ).to.equal(DEFAULT_AMOUNT) + + expect( + await hre.ethers.provider.getBalance(DEFAULT_RECIPIENT) + ).to.equal(DEFAULT_AMOUNT) + }) + }) + + describe('when called by not the owner', () => { + it('should revert', async () => { + await expect( + RetroReceiver.connect(signer2)['withdrawETH(address,uint256)']( + DEFAULT_RECIPIENT, + DEFAULT_AMOUNT + ) + ).to.be.revertedWith('UNAUTHORIZED') + }) + }) + }) + + describe('withdrawERC20(address,address)', () => { + describe('when called by the owner', () => { + it('should withdraw all ERC20 balance held by the contract', async () => { + await TestERC20.transfer(RetroReceiver.address, DEFAULT_AMOUNT) + + await expect( + RetroReceiver['withdrawERC20(address,address)']( + TestERC20.address, + DEFAULT_RECIPIENT + ) + ) + .to.emit(RetroReceiver, 'WithdrewERC20') + .withArgs( + signer1.address, + DEFAULT_RECIPIENT, + TestERC20.address, + DEFAULT_AMOUNT + ) + + expect(await TestERC20.balanceOf(DEFAULT_RECIPIENT)).to.equal( + DEFAULT_AMOUNT + ) + }) + }) + + describe('when called by not the owner', () => { + it('should revert', async () => { + await expect( + RetroReceiver.connect(signer2)['withdrawERC20(address,address)']( + TestERC20.address, + DEFAULT_RECIPIENT + ) + ).to.be.revertedWith('UNAUTHORIZED') + }) + }) + }) + + describe('withdrawERC20(address,address,uint256)', () => { + describe('when called by the owner', () => { + it('should withdraw the given ERC20 amount', async () => { + await TestERC20.transfer(RetroReceiver.address, DEFAULT_AMOUNT.mul(2)) + + await expect( + RetroReceiver['withdrawERC20(address,address,uint256)']( + TestERC20.address, + DEFAULT_RECIPIENT, + DEFAULT_AMOUNT + ) + ) + .to.emit(RetroReceiver, 'WithdrewERC20') + .withArgs( + signer1.address, + DEFAULT_RECIPIENT, + TestERC20.address, + DEFAULT_AMOUNT + ) + + expect(await TestERC20.balanceOf(DEFAULT_RECIPIENT)).to.equal( + DEFAULT_AMOUNT + ) + }) + }) + + describe('when called by not the owner', () => { + it('should revert', async () => { + await expect( + RetroReceiver.connect(signer2)[ + 'withdrawERC20(address,address,uint256)' + ](TestERC20.address, DEFAULT_RECIPIENT, DEFAULT_AMOUNT) + ).to.be.revertedWith('UNAUTHORIZED') + }) + }) + }) + + describe('withdrawERC721', () => { + describe('when called by the owner', () => { + it('should withdraw the token', async () => { + await TestERC721.transferFrom( + signer1.address, + RetroReceiver.address, + DEFAULT_TOKEN_ID + ) + + await expect( + RetroReceiver.withdrawERC721( + TestERC721.address, + DEFAULT_RECIPIENT, + DEFAULT_TOKEN_ID + ) + ) + .to.emit(RetroReceiver, 'WithdrewERC721') + .withArgs( + signer1.address, + DEFAULT_RECIPIENT, + TestERC721.address, + DEFAULT_TOKEN_ID + ) + + expect(await TestERC721.ownerOf(DEFAULT_TOKEN_ID)).to.equal( + DEFAULT_RECIPIENT + ) + }) + }) + + describe('when called by not the owner', () => { + it('should revert', async () => { + await expect( + RetroReceiver.connect(signer2).withdrawERC721( + TestERC721.address, + DEFAULT_RECIPIENT, + DEFAULT_TOKEN_ID + ) + ).to.be.revertedWith('UNAUTHORIZED') + }) + }) + }) +}) diff --git a/packages/contracts-periphery/test/helpers/deploy.ts b/packages/contracts-periphery/test/helpers/deploy.ts new file mode 100644 index 0000000000000..23525868b6fce --- /dev/null +++ b/packages/contracts-periphery/test/helpers/deploy.ts @@ -0,0 +1,12 @@ +import hre from 'hardhat' + +export const deploy = async ( + name: string, + opts?: { + args?: any[] + signer?: any + } +) => { + const factory = await hre.ethers.getContractFactory(name, opts?.signer) + return factory.deploy(...(opts?.args || [])) +} diff --git a/packages/contracts-periphery/test/helpers/index.ts b/packages/contracts-periphery/test/helpers/index.ts new file mode 100644 index 0000000000000..f52cfc5b0429a --- /dev/null +++ b/packages/contracts-periphery/test/helpers/index.ts @@ -0,0 +1 @@ +export * from './deploy' diff --git a/packages/contracts-periphery/test/setup.ts b/packages/contracts-periphery/test/setup.ts new file mode 100644 index 0000000000000..3b89f57894452 --- /dev/null +++ b/packages/contracts-periphery/test/setup.ts @@ -0,0 +1,10 @@ +/* External Imports */ +import chai = require('chai') +import Mocha from 'mocha' +import { solidity } from 'ethereum-waffle' + +chai.use(solidity) +const should = chai.should() +const expect = chai.expect + +export { should, expect, Mocha } diff --git a/packages/contracts-periphery/tsconfig.build.json b/packages/contracts-periphery/tsconfig.build.json new file mode 100644 index 0000000000000..7ade983395d0b --- /dev/null +++ b/packages/contracts-periphery/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + + "include": [ + "src/**/*" + ] +} diff --git a/packages/contracts-periphery/tsconfig.json b/packages/contracts-periphery/tsconfig.json new file mode 100644 index 0000000000000..b4e5ff40ae0e0 --- /dev/null +++ b/packages/contracts-periphery/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "resolveJsonModule": true, + } +} diff --git a/yarn.lock b/yarn.lock index 23204508eb931..491e779bf927a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -975,7 +975,7 @@ "@ethersproject/logger" "^5.5.0" bn.js "^4.11.9" -"@ethersproject/bignumber@5.6.1": +"@ethersproject/bignumber@5.6.1", "@ethersproject/bignumber@^5.6.0": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.1.tgz#d5e0da518eb82ab8d08ca9db501888bbf5f0c8fb" integrity sha512-UtMeZ3GaUuF9sx2u9nPZiPP3ULcAFmXyvynR7oHl/tPrM+vldZh7ocMsoa1PqKYGnQnqUZJoqxZnGN6J0qdipA== @@ -993,15 +993,6 @@ "@ethersproject/logger" "^5.4.0" bn.js "^4.11.9" -"@ethersproject/bignumber@^5.6.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.0.tgz#116c81b075c57fa765a8f3822648cf718a8a0e26" - integrity sha512-VziMaXIUHQlHJmkv1dlcd6GY2PmT0khtAqaMctCIDogxkrarMzA9L94KN1NeXqqOfFD6r0sJT3vCTOFSmZ07DA== - dependencies: - "@ethersproject/bytes" "^5.6.0" - "@ethersproject/logger" "^5.6.0" - bn.js "^4.11.9" - "@ethersproject/bytes@5.4.0", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.4.0.tgz#56fa32ce3bf67153756dbaefda921d1d4774404e" @@ -2997,6 +2988,11 @@ dependencies: squirrelly "^8.0.8" +"@rari-capital/solmate@^6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@rari-capital/solmate/-/solmate-6.3.0.tgz#01050e276e71dc4cd4169cf8002b447eb07d90c3" + integrity sha512-SWPbnfZUCe4ahHNqcb0qsPrzzAzMZMoA3x6SxZn04g0dLm0xupVeHonM3LK13uhPGIULF8HzXg8CgXE/fEnMlQ== + "@resolver-engine/core@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@resolver-engine/core/-/core-0.3.3.tgz#590f77d85d45bc7ecc4e06c654f41345db6ca967"