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"