Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silver-planets-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@eth-optimism/contracts-periphery': patch
---

Adds new TeleportrWithdrawer contract for withdrawing from Teleportr
1 change: 1 addition & 0 deletions packages/contracts-periphery/config/deploy/ethereum.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DeployConfig } from '../../src'

const config: DeployConfig = {
ddd: '0x9C6373dE60c2D3297b18A8f964618ac46E011B58',
retroReceiverOwner: '0xc37f6a6c4AB335E20d10F034B90386E2fb70bbF5',
drippieOwner: '0xc37f6a6c4AB335E20d10F034B90386E2fb70bbF5',
}
Expand Down
1 change: 1 addition & 0 deletions packages/contracts-periphery/config/deploy/optimism.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DeployConfig } from '../../src'

const config: DeployConfig = {
ddd: '0x9C6373dE60c2D3297b18A8f964618ac46E011B58',
retroReceiverOwner: '0xc37f6a6c4AB335E20d10F034B90386E2fb70bbF5',
drippieOwner: '0xc37f6a6c4AB335E20d10F034B90386E2fb70bbF5',
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
1 change: 0 additions & 1 deletion packages/contracts-periphery/deploy/RetroReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,5 @@ const deployFn: DeployFunction = async (hre) => {
}

deployFn.tags = ['RetroReceiver']
deployFn.dependencies = ['OptimismAuthority']

export default deployFn
29 changes: 29 additions & 0 deletions packages/contracts-periphery/deploy/TeleportrWithdrawer.ts
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion packages/contracts-periphery/deploy/drippie/Drippie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,5 @@ const deployFn: DeployFunction = async (hre) => {
}

deployFn.tags = ['Drippie']
deployFn.dependencies = ['OptimismAuthority']

export default deployFn
12 changes: 12 additions & 0 deletions packages/contracts-periphery/src/config/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -24,6 +33,9 @@ const configSpec: {
default?: any
}
} = {
ddd: {
type: 'address',
},
retroReceiverOwner: {
type: 'address',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down