diff --git a/.changeset/silver-planets-drop.md b/.changeset/silver-planets-drop.md new file mode 100644 index 0000000000000..f2d7144fa8bf0 --- /dev/null +++ b/.changeset/silver-planets-drop.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/contracts-periphery': patch +--- + +Adds new TeleportrWithdrawer contract for withdrawing from Teleportr diff --git a/packages/contracts-periphery/config/deploy/ethereum.ts b/packages/contracts-periphery/config/deploy/ethereum.ts index a3bbb8d7a4806..62f82e5f18ac1 100644 --- a/packages/contracts-periphery/config/deploy/ethereum.ts +++ b/packages/contracts-periphery/config/deploy/ethereum.ts @@ -1,6 +1,7 @@ import { DeployConfig } from '../../src' const config: DeployConfig = { + ddd: '0x9C6373dE60c2D3297b18A8f964618ac46E011B58', retroReceiverOwner: '0xc37f6a6c4AB335E20d10F034B90386E2fb70bbF5', drippieOwner: '0xc37f6a6c4AB335E20d10F034B90386E2fb70bbF5', } diff --git a/packages/contracts-periphery/config/deploy/optimism.ts b/packages/contracts-periphery/config/deploy/optimism.ts index a3bbb8d7a4806..62f82e5f18ac1 100644 --- a/packages/contracts-periphery/config/deploy/optimism.ts +++ b/packages/contracts-periphery/config/deploy/optimism.ts @@ -1,6 +1,7 @@ import { DeployConfig } from '../../src' const config: DeployConfig = { + ddd: '0x9C6373dE60c2D3297b18A8f964618ac46E011B58', retroReceiverOwner: '0xc37f6a6c4AB335E20d10F034B90386E2fb70bbF5', drippieOwner: '0xc37f6a6c4AB335E20d10F034B90386E2fb70bbF5', } diff --git a/packages/contracts-periphery/contracts/testing/helpers/MockTeleportr.sol b/packages/contracts-periphery/contracts/testing/helpers/MockTeleportr.sol new file mode 100644 index 0000000000000..16a329711b0d1 --- /dev/null +++ b/packages/contracts-periphery/contracts/testing/helpers/MockTeleportr.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +contract MockTeleportr { + function withdrawBalance() external { + payable(msg.sender).transfer(address(this).balance); + } +} diff --git a/packages/contracts-periphery/contracts/universal/TeleportrWithdrawer.sol b/packages/contracts-periphery/contracts/universal/TeleportrWithdrawer.sol new file mode 100644 index 0000000000000..2abd0b620f45f --- /dev/null +++ b/packages/contracts-periphery/contracts/universal/TeleportrWithdrawer.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { AssetReceiver } from "./AssetReceiver.sol"; + +/** + * @notice Stub interface for Teleportr. + */ +interface Teleportr { + function withdrawBalance() external; +} + +/** + * @title TeleportrWithdrawer + * @notice The TeleportrWithdrawer is a simple contract capable of withdrawing funds from the + * TeleportrContract and sending them to some recipient address. + */ +contract TeleportrWithdrawer is AssetReceiver { + /** + * @notice Address of the Teleportr contract. + */ + address public teleportr; + + /** + * @notice Address that will receive Teleportr withdrawals. + */ + address public recipient; + + /** + * @notice Data to be sent to the recipient address. + */ + bytes public data; + + /** + * @param _owner Initial owner of the contract. + */ + constructor(address _owner) AssetReceiver(_owner) {} + + /** + * @notice Allows the owner to update the recipient address. + * + * @param _recipient New recipient address. + */ + function setRecipient(address _recipient) external onlyOwner { + recipient = _recipient; + } + + /** + * @notice Allows the owner to update the Teleportr contract address. + * + * @param _teleportr New Teleportr contract address. + */ + function setTeleportr(address _teleportr) external onlyOwner { + teleportr = _teleportr; + } + + /** + * @notice Allows the owner to update the data to be sent to the recipient address. + * + * @param _data New data to be sent to the recipient address. + */ + function setData(bytes memory _data) external onlyOwner { + data = _data; + } + + /** + * @notice Withdraws the full balance of the Teleportr contract to the recipient address. + * Anyone is allowed to trigger this function since the recipient address cannot be + * controlled by the msg.sender. + */ + function withdrawFromTeleportr() external { + Teleportr(teleportr).withdrawBalance(); + (bool success, ) = recipient.call{ value: address(this).balance }(data); + require(success, "TeleportrWithdrawer: send failed"); + } +} diff --git a/packages/contracts-periphery/deploy/RetroReceiver.ts b/packages/contracts-periphery/deploy/RetroReceiver.ts index 52847e3549ea5..ceb87feacb0b8 100644 --- a/packages/contracts-periphery/deploy/RetroReceiver.ts +++ b/packages/contracts-periphery/deploy/RetroReceiver.ts @@ -19,6 +19,5 @@ const deployFn: DeployFunction = async (hre) => { } deployFn.tags = ['RetroReceiver'] -deployFn.dependencies = ['OptimismAuthority'] export default deployFn diff --git a/packages/contracts-periphery/deploy/TeleportrWithdrawer.ts b/packages/contracts-periphery/deploy/TeleportrWithdrawer.ts new file mode 100644 index 0000000000000..434db2a0d5d1f --- /dev/null +++ b/packages/contracts-periphery/deploy/TeleportrWithdrawer.ts @@ -0,0 +1,29 @@ +/* Imports: External */ +import { DeployFunction } from 'hardhat-deploy/dist/types' + +import { getDeployConfig } from '../src' + +const deployFn: DeployFunction = async (hre) => { + const { deployer } = await hre.getNamedAccounts() + + const config = getDeployConfig(hre.network.name) + + const { deploy } = await hre.deployments.deterministic( + 'TeleportrWithdrawer', + { + salt: hre.ethers.utils.solidityKeccak256( + ['string'], + ['TeleportrWithdrawer'] + ), + from: deployer, + args: [config.ddd], + log: true, + } + ) + + await deploy() +} + +deployFn.tags = ['TeleportrWithdrawer'] + +export default deployFn diff --git a/packages/contracts-periphery/deploy/drippie/Drippie.ts b/packages/contracts-periphery/deploy/drippie/Drippie.ts index 00ed70e9a3f9e..4d2f2590e50d4 100644 --- a/packages/contracts-periphery/deploy/drippie/Drippie.ts +++ b/packages/contracts-periphery/deploy/drippie/Drippie.ts @@ -19,6 +19,5 @@ const deployFn: DeployFunction = async (hre) => { } deployFn.tags = ['Drippie'] -deployFn.dependencies = ['OptimismAuthority'] export default deployFn diff --git a/packages/contracts-periphery/src/config/deploy.ts b/packages/contracts-periphery/src/config/deploy.ts index a13958df125fb..19310c5b7c86a 100644 --- a/packages/contracts-periphery/src/config/deploy.ts +++ b/packages/contracts-periphery/src/config/deploy.ts @@ -4,6 +4,15 @@ import { ethers } from 'ethers' * Defines the configuration for a deployment. */ export interface DeployConfig { + /** + * Dedicated Deterministic Deployer address (DDD). + * When deploying authenticated deterministic smart contracts to the same address on various + * chains, it's necessary to have a single root address that will initially own the contract and + * later transfer ownership to the final contract owner. We call this address the DDD. We expect + * the DDD to transfer ownership to the final contract owner very quickly after deployment. + */ + ddd: string + /** * Initial RetroReceiver owner. */ @@ -24,6 +33,9 @@ const configSpec: { default?: any } } = { + ddd: { + type: 'address', + }, retroReceiverOwner: { type: 'address', }, diff --git a/packages/contracts-periphery/test/contracts/universal/TeleportrWithdrawer.spec.ts b/packages/contracts-periphery/test/contracts/universal/TeleportrWithdrawer.spec.ts new file mode 100644 index 0000000000000..f3dcc2758ff60 --- /dev/null +++ b/packages/contracts-periphery/test/contracts/universal/TeleportrWithdrawer.spec.ts @@ -0,0 +1,120 @@ +import hre from 'hardhat' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { Contract } from 'ethers' +import { toRpcHexString } from '@eth-optimism/core-utils' + +import { expect } from '../../setup' +import { deploy } from '../../helpers' + +describe('TeleportrWithdrawer', () => { + let signer1: SignerWithAddress + let signer2: SignerWithAddress + before('signer setup', async () => { + ;[signer1, signer2] = await hre.ethers.getSigners() + }) + + let SimpleStorage: Contract + let MockTeleportr: Contract + let TeleportrWithdrawer: Contract + beforeEach('deploy contracts', async () => { + SimpleStorage = await deploy('SimpleStorage') + MockTeleportr = await deploy('MockTeleportr') + TeleportrWithdrawer = await deploy('TeleportrWithdrawer', { + signer: signer1, + args: [signer1.address], + }) + }) + + describe('setRecipient', () => { + describe('when called by authorized address', () => { + it('should set the recipient', async () => { + await TeleportrWithdrawer.setRecipient(signer1.address) + expect(await TeleportrWithdrawer.recipient()).to.equal(signer1.address) + }) + }) + + describe('when called by not authorized address', () => { + it('should revert', async () => { + await expect( + TeleportrWithdrawer.connect(signer2).setRecipient(signer2.address) + ).to.be.revertedWith('UNAUTHORIZED') + }) + }) + }) + + describe('setTeleportr', () => { + describe('when called by authorized address', () => { + it('should set the recipient', async () => { + await TeleportrWithdrawer.setTeleportr(MockTeleportr.address) + expect(await TeleportrWithdrawer.teleportr()).to.equal( + MockTeleportr.address + ) + }) + }) + + describe('when called by not authorized address', () => { + it('should revert', async () => { + await expect( + TeleportrWithdrawer.connect(signer2).setTeleportr(signer2.address) + ).to.be.revertedWith('UNAUTHORIZED') + }) + }) + }) + + describe('setData', () => { + const data = `0x${'ff'.repeat(64)}` + + describe('when called by authorized address', () => { + it('should set the data', async () => { + await TeleportrWithdrawer.setData(data) + expect(await TeleportrWithdrawer.data()).to.equal(data) + }) + }) + + describe('when called by not authorized address', () => { + it('should revert', async () => { + await expect( + TeleportrWithdrawer.connect(signer2).setData(data) + ).to.be.revertedWith('UNAUTHORIZED') + }) + }) + }) + + describe('withdrawTeleportrBalance', () => { + const recipient = `0x${'11'.repeat(20)}` + const amount = hre.ethers.constants.WeiPerEther + beforeEach(async () => { + await hre.ethers.provider.send('hardhat_setBalance', [ + MockTeleportr.address, + toRpcHexString(amount), + ]) + await TeleportrWithdrawer.setRecipient(recipient) + await TeleportrWithdrawer.setTeleportr(MockTeleportr.address) + }) + + describe('when target is an EOA', () => { + it('should withdraw the balance', async () => { + await TeleportrWithdrawer.withdrawFromTeleportr() + expect(await hre.ethers.provider.getBalance(recipient)).to.equal(amount) + }) + }) + + describe('when target is a contract', () => { + it('should withdraw the balance and trigger code', async () => { + const key = `0x${'dd'.repeat(32)}` + const val = `0x${'ee'.repeat(32)}` + await TeleportrWithdrawer.setRecipient(SimpleStorage.address) + await TeleportrWithdrawer.setData( + SimpleStorage.interface.encodeFunctionData('set', [key, val]) + ) + + await TeleportrWithdrawer.withdrawFromTeleportr() + + expect( + await hre.ethers.provider.getBalance(SimpleStorage.address) + ).to.equal(amount) + expect(await SimpleStorage.get(key)).to.equal(val) + }) + }) + }) +}) diff --git a/packages/contracts-periphery/test/contracts/universal/Transactor.spec.ts b/packages/contracts-periphery/test/contracts/universal/Transactor.spec.ts index ca128c45a01ac..68df7f77efbbc 100644 --- a/packages/contracts-periphery/test/contracts/universal/Transactor.spec.ts +++ b/packages/contracts-periphery/test/contracts/universal/Transactor.spec.ts @@ -5,7 +5,7 @@ import { Contract } from 'ethers' import { expect } from '../../setup' import { decodeSolidityRevert, deploy } from '../../helpers' -describe('AssetReceiver', () => { +describe('Transactor', () => { let signer1: SignerWithAddress let signer2: SignerWithAddress before('signer setup', async () => {