Commit c0c2267a authored by Kelvin Fichter's avatar Kelvin Fichter

maint: remove teleportr

Removes teleportr since it's being deprecated.
parent 49685f81
...@@ -859,11 +859,6 @@ workflows: ...@@ -859,11 +859,6 @@ workflows:
name: proxyd-tests name: proxyd-tests
binary_name: proxyd binary_name: proxyd
working_directory: proxyd working_directory: proxyd
- go-lint-test-build:
name: teleportr-tests
binary_name: teleportr
working_directory: teleportr
dependencies: bss-core
- go-lint-test-build: - go-lint-test-build:
name: gas-oracle-tests name: gas-oracle-tests
binary_name: gas-oracle binary_name: gas-oracle
......
...@@ -31,7 +31,6 @@ jobs: ...@@ -31,7 +31,6 @@ jobs:
l2geth-exporter: ${{ steps.packages.outputs.l2geth-exporter }} l2geth-exporter: ${{ steps.packages.outputs.l2geth-exporter }}
batch-submitter-service: ${{ steps.packages.outputs.batch-submitter-service }} batch-submitter-service: ${{ steps.packages.outputs.batch-submitter-service }}
indexer: ${{ steps.packages.outputs.indexer }} indexer: ${{ steps.packages.outputs.indexer }}
teleportr: ${{ steps.packages.outputs.teleportr }}
endpoint-monitor: ${{ steps.packages.outputs.l2geth-exporter }} endpoint-monitor: ${{ steps.packages.outputs.l2geth-exporter }}
steps: steps:
...@@ -568,43 +567,6 @@ jobs: ...@@ -568,43 +567,6 @@ jobs:
GITCOMMIT=${{ steps.build_args.outputs.GITCOMMIT }} GITCOMMIT=${{ steps.build_args.outputs.GITCOMMIT }}
GITVERSION=${{ steps.build_args.outputs.GITVERSION }} GITVERSION=${{ steps.build_args.outputs.GITVERSION }}
teleportr:
name: Publish Teleportr Version ${{ needs.canary-publish.outputs.canary-docker-tag }}
needs: canary-publish
if: needs.canary-publish.outputs.teleportr != ''
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_SECRET }}
- name: Set build args
id: build_args
run: |
echo ::set-output name=GITDATE::"$(date +%d-%m-%Y)"
echo ::set-output name=GITVERSION::$(jq -r .version ./teleportr/package.json)
echo ::set-output name=GITCOMMIT::"$GITHUB_SHA"
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./teleportr/Dockerfile
push: true
tags: ethereumoptimism/teleportr:${{ needs.canary-publish.outputs.teleportr }}
build-args: |
GITDATE=${{ steps.build_args.outputs.GITDATE }}
GITCOMMIT=${{ steps.build_args.outputs.GITCOMMIT }}
GITVERSION=${{ steps.build_args.outputs.GITVERSION }}
endpoint-monitor: endpoint-monitor:
name: Publish endpoint-monitor Version ${{ needs.canary-publish.outputs.canary-docker-tag }} name: Publish endpoint-monitor Version ${{ needs.canary-publish.outputs.canary-docker-tag }}
needs: canary-publish needs: canary-publish
......
...@@ -26,7 +26,6 @@ jobs: ...@@ -26,7 +26,6 @@ jobs:
l2geth-exporter: ${{ steps.packages.outputs.l2geth-exporter }} l2geth-exporter: ${{ steps.packages.outputs.l2geth-exporter }}
batch-submitter-service: ${{ steps.packages.outputs.batch-submitter-service }} batch-submitter-service: ${{ steps.packages.outputs.batch-submitter-service }}
indexer: ${{ steps.packages.outputs.indexer }} indexer: ${{ steps.packages.outputs.indexer }}
teleportr: ${{ steps.packages.outputs.teleportr }}
ci-builder: ${{ steps.packages.outputs.ci-builder }} ci-builder: ${{ steps.packages.outputs.ci-builder }}
foundry: ${{ steps.packages.outputs.foundry }} foundry: ${{ steps.packages.outputs.foundry }}
endpoint-monitor: ${{ steps.packages.outputs.endpoint-monitor }} endpoint-monitor: ${{ steps.packages.outputs.endpoint-monitor }}
...@@ -600,43 +599,6 @@ jobs: ...@@ -600,43 +599,6 @@ jobs:
GITCOMMIT=${{ steps.build_args.outputs.GITCOMMIT }} GITCOMMIT=${{ steps.build_args.outputs.GITCOMMIT }}
GITVERSION=${{ steps.build_args.outputs.GITVERSION }} GITVERSION=${{ steps.build_args.outputs.GITVERSION }}
teleportr:
name: Publish Teleportr Version ${{ needs.release.outputs.teleportr }}
needs: release
if: needs.release.outputs.teleportr != ''
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_SECRET }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set build args
id: build_args
run: |
echo ::set-output name=GITDATE::"$(date +%d-%m-%Y)"
echo ::set-output name=GITVERSION::$(jq -r .version ./teleportr/package.json)
echo ::set-output name=GITCOMMIT::"$GITHUB_SHA"
- name: Publish Teleportr
uses: docker/build-push-action@v2
with:
context: .
file: ./teleportr/Dockerfile
push: true
tags: ethereumoptimism/teleportr:${{ needs.release.outputs.teleportr }},ethereumoptimism/teleportr:latest
build-args: |
GITDATE=${{ steps.build_args.outputs.GITDATE }}
GITCOMMIT=${{ steps.build_args.outputs.GITCOMMIT }}
GITVERSION=${{ steps.build_args.outputs.GITVERSION }}
endpoint-monitor: endpoint-monitor:
name: Publish endpoint-monitor Version ${{ needs.release.outputs.endpoint-monitor}} name: Publish endpoint-monitor Version ${{ needs.release.outputs.endpoint-monitor}}
needs: release needs: release
......
...@@ -73,7 +73,6 @@ Refer to the Directory Structure section below to understand which packages are ...@@ -73,7 +73,6 @@ Refer to the Directory Structure section below to understand which packages are
├── <a href="./op-exporter">op-exporter</a>: A prometheus exporter to collect/serve metrics from an Optimism node ├── <a href="./op-exporter">op-exporter</a>: A prometheus exporter to collect/serve metrics from an Optimism node
├── <a href="./proxyd">proxyd</a>: Configurable RPC request router and proxy ├── <a href="./proxyd">proxyd</a>: Configurable RPC request router and proxy
├── <a href="./technical-documents">technical-documents</a>: audits and post-mortem documents ├── <a href="./technical-documents">technical-documents</a>: audits and post-mortem documents
├── <a href="./teleportr">teleportr</a>: Bridge for teleporting ETH between L1 and L2 at low cost
~~ BEDROCK upgrade - Not production-ready yet, part of next major upgrade ~~ ~~ BEDROCK upgrade - Not production-ready yet, part of next major upgrade ~~
├── <a href="./packages">packages</a> ├── <a href="./packages">packages</a>
......
...@@ -17,7 +17,6 @@ use ( ...@@ -17,7 +17,6 @@ use (
./op-proposer ./op-proposer
./op-service ./op-service
./proxyd ./proxyd
./teleportr
) )
replace github.com/ethereum/go-ethereum v1.10.26 => github.com/ethereum-optimism/op-geth v0.0.0-20221104231810-30db39cae2be replace github.com/ethereum/go-ethereum v1.10.26 => github.com/ethereum-optimism/op-geth v0.0.0-20221104231810-30db39cae2be
......
...@@ -86,6 +86,7 @@ github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJ ...@@ -86,6 +86,7 @@ github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJ
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
github.com/c-bata/go-prompt v0.2.2 h1:uyKRz6Z6DUyj49QVijyM339UJV9yhbr70gESwbNU3e0= github.com/c-bata/go-prompt v0.2.2 h1:uyKRz6Z6DUyj49QVijyM339UJV9yhbr70gESwbNU3e0=
github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g= github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
...@@ -424,8 +425,6 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82 h1:LneqU9PHDsg/ ...@@ -424,8 +425,6 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82 h1:LneqU9PHDsg/
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537 h1:YGaxtkYjb8mnTvtufv2LKLwCQu2/C7qFB7UtrOlTWOY= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537 h1:YGaxtkYjb8mnTvtufv2LKLwCQu2/C7qFB7UtrOlTWOY=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smola/gocompat v0.2.0 h1:6b1oIMlUXIpz//VKEDzPVBK8KG7beVwmHIUEBIs/Pns= github.com/smola/gocompat v0.2.0 h1:6b1oIMlUXIpz//VKEDzPVBK8KG7beVwmHIUEBIs/Pns=
......
...@@ -20,7 +20,6 @@ ...@@ -20,7 +20,6 @@
"ops/docker/ci-builder", "ops/docker/ci-builder",
"ops/docker/foundry", "ops/docker/foundry",
"proxyd", "proxyd",
"teleportr",
"endpoint-monitor" "endpoint-monitor"
], ],
"nohoist": [ "nohoist": [
......
module.exports = { module.exports = {
skipFiles: [ skipFiles: [
'./test-libraries', './test-libraries',
'./foundry-tests' './foundry-tests',
'./testing'
], ],
mocha: { mocha: {
grep: "@skip-on-coverage", grep: "@skip-on-coverage",
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @custom:attribution https://github.com/0xclem/teleportr
* @title TeleportrDeposit
* @notice A contract meant to manage deposits into Optimism's Teleportr custodial bridge. Deposits
* are rate limited to avoid a situation where too much ETH is flowing through this bridge
* and cannot be properly disbursed on L2. Inspired by 0xclem's original Teleportr system
* (https://github.com/0xclem/teleportr).
*/
contract TeleportrDeposit is Ownable {
/**
* @notice Minimum deposit amount (in wei).
*/
uint256 public minDepositAmount;
/**
* @notice Maximum deposit amount (in wei).
*/
uint256 public maxDepositAmount;
/**
* @notice Maximum balance this contract will hold before it starts rejecting deposits.
*/
uint256 public maxBalance;
/**
* @notice Total number of deposits received.
*/
uint256 public totalDeposits;
/**
* @notice Emitted any time the minimum deposit amount is set.
*
* @param previousAmount The previous minimum deposit amount.
* @param newAmount The new minimum deposit amount.
*/
event MinDepositAmountSet(uint256 previousAmount, uint256 newAmount);
/**
* @notice Emitted any time the maximum deposit amount is set.
*
* @param previousAmount The previous maximum deposit amount.
* @param newAmount The new maximum deposit amount.
*/
event MaxDepositAmountSet(uint256 previousAmount, uint256 newAmount);
/**
* @notice Emitted any time the contract maximum balance is set.
*
* @param previousBalance The previous maximum contract balance.
* @param newBalance The new maximum contract balance.
*/
event MaxBalanceSet(uint256 previousBalance, uint256 newBalance);
/**
* @notice Emitted any time the balance is withdrawn by the owner.
*
* @param owner The current owner and recipient of the funds.
* @param balance The current contract balance paid to the owner.
*/
event BalanceWithdrawn(address indexed owner, uint256 balance);
/**
* @notice Emitted any time a successful deposit is received.
*
* @param depositId A unique sequencer number identifying the deposit.
* @param emitter The sending address of the payer.
* @param amount The amount deposited by the payer.
*/
event EtherReceived(uint256 indexed depositId, address indexed emitter, uint256 indexed amount);
/**
* @custom:semver 0.0.1
*
* @param _minDepositAmount The initial minimum deposit amount.
* @param _maxDepositAmount The initial maximum deposit amount.
* @param _maxBalance The initial maximum contract balance.
*/
constructor(
uint256 _minDepositAmount,
uint256 _maxDepositAmount,
uint256 _maxBalance
) {
minDepositAmount = _minDepositAmount;
maxDepositAmount = _maxDepositAmount;
maxBalance = _maxBalance;
totalDeposits = 0;
emit MinDepositAmountSet(0, _minDepositAmount);
emit MaxDepositAmountSet(0, _maxDepositAmount);
emit MaxBalanceSet(0, _maxBalance);
}
/**
* @notice Accepts deposits that will be disbursed to the sender's address on L2.
*/
receive() external payable {
require(msg.value >= minDepositAmount, "Deposit amount is too small");
require(msg.value <= maxDepositAmount, "Deposit amount is too big");
require(address(this).balance <= maxBalance, "Contract max balance exceeded");
emit EtherReceived(totalDeposits, msg.sender, msg.value);
unchecked {
totalDeposits += 1;
}
}
/**
* @notice Sends the contract's current balance to the owner.
*/
function withdrawBalance() external onlyOwner {
address _owner = owner();
uint256 _balance = address(this).balance;
emit BalanceWithdrawn(_owner, _balance);
payable(_owner).transfer(_balance);
}
/**
* @notice Sets the minimum amount that can be deposited in a receive.
*
* @param _minDepositAmount The new minimum deposit amount.
*/
function setMinAmount(uint256 _minDepositAmount) external onlyOwner {
emit MinDepositAmountSet(minDepositAmount, _minDepositAmount);
minDepositAmount = _minDepositAmount;
}
/**
* @notice Sets the maximum amount that can be deposited in a receive.
*
* @param _maxDepositAmount The new maximum deposit amount.
*/
function setMaxAmount(uint256 _maxDepositAmount) external onlyOwner {
emit MaxDepositAmountSet(maxDepositAmount, _maxDepositAmount);
maxDepositAmount = _maxDepositAmount;
}
/**
* @notice Sets the maximum balance the contract can hold after a receive.
*
* @param _maxBalance The new maximum contract balance.
*/
function setMaxBalance(uint256 _maxBalance) external onlyOwner {
emit MaxBalanceSet(maxBalance, _maxBalance);
maxBalance = _maxBalance;
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { AssetReceiver } from "../universal/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");
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title TeleportrDisburser
*/
contract TeleportrDisburser is Ownable {
/**
* @notice A struct holding the address and amount to disbursement.
*/
struct Disbursement {
uint256 amount;
address addr;
}
/**
* @notice Total number of disbursements processed.
*/
uint256 public totalDisbursements;
/**
* @notice Emitted any time the balance is withdrawn by the owner.
*
* @param owner The current owner and recipient of the funds.
* @param balance The current contract balance paid to the owner.
*/
event BalanceWithdrawn(address indexed owner, uint256 balance);
/**
* @notice Emitted any time a disbursement is successfuly sent.
*
* @param depositId The unique sequence number identifying the deposit.
* @param to The recipient of the disbursement.
* @param amount The amount sent to the recipient.
*/
event DisbursementSuccess(uint256 indexed depositId, address indexed to, uint256 amount);
/**
* @notice Emitted any time a disbursement fails to send.
*
* @param depositId The unique sequence number identifying the deposit.
* @param to The intended recipient of the disbursement.
* @param amount The amount intended to be sent to the recipient.
*/
event DisbursementFailed(uint256 indexed depositId, address indexed to, uint256 amount);
/**
* @custom:semver 0.0.1
*/
constructor() {
totalDisbursements = 0;
}
/**
* @notice Accepts a list of Disbursements and forwards the amount paid to the contract to each
* recipient. Reverts if there are zero disbursements, the total amount to forward
* differs from the amount sent in the transaction, or the _nextDepositId is
* unexpected. Failed disbursements will not cause the method to revert, but will
* instead be held by the contract and available for the owner to withdraw.
*
* @param _nextDepositId The depositId of the first Dispursement.
* @param _disbursements A list of Disbursements to process.
*/
function disburse(uint256 _nextDepositId, Disbursement[] calldata _disbursements)
external
payable
onlyOwner
{
// Ensure there are disbursements to process.
uint256 _numDisbursements = _disbursements.length;
require(_numDisbursements > 0, "No disbursements");
// Ensure the _nextDepositId matches our expected value.
uint256 _depositId = totalDisbursements;
require(_depositId == _nextDepositId, "Unexpected next deposit id");
unchecked {
totalDisbursements += _numDisbursements;
}
// Ensure the amount sent in the transaction is equal to the sum of the
// disbursements.
uint256 _totalDisbursed = 0;
for (uint256 i = 0; i < _numDisbursements; i++) {
_totalDisbursed += _disbursements[i].amount;
}
require(_totalDisbursed == msg.value, "Disbursement total != amount sent");
// Process disbursements.
for (uint256 i = 0; i < _numDisbursements; i++) {
uint256 _amount = _disbursements[i].amount;
address _addr = _disbursements[i].addr;
// Deliver the dispursement amount to the receiver. If the
// disbursement fails, the amount will be kept by the contract
// rather than reverting to prevent blocking progress on other
// disbursements.
// slither-disable-next-line calls-loop,reentrancy-events
(bool success, ) = _addr.call{ value: _amount, gas: 2300 }("");
if (success) emit DisbursementSuccess(_depositId, _addr, _amount);
else emit DisbursementFailed(_depositId, _addr, _amount);
unchecked {
_depositId += 1;
}
}
}
/**
* @notice Sends the contract's current balance to the owner.
*/
function withdrawBalance() external onlyOwner {
address _owner = owner();
uint256 balance = address(this).balance;
emit BalanceWithdrawn(_owner, balance);
payable(_owner).transfer(balance);
}
}
//SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/* Testing utilities */
import { Test } from "forge-std/Test.sol";
import { SimpleStorage } from "../testing/helpers/SimpleStorage.sol";
import { MockTeleportr } from "../testing/helpers/MockTeleportr.sol";
import { TeleportrWithdrawer } from "../universal/TeleportrWithdrawer.sol";
contract TeleportrWithdrawer_Initializer is Test {
address alice = address(128);
address bob = address(256);
TeleportrWithdrawer teleportrWithdrawer;
MockTeleportr mockTeleportr;
SimpleStorage simpleStorage;
function _setUp() public {
// Deploy MockTeleportr and SimpleStorage helper contracts
mockTeleportr = new MockTeleportr();
simpleStorage = new SimpleStorage();
// Deploy Transactor contract
teleportrWithdrawer = new TeleportrWithdrawer(address(alice));
vm.label(address(teleportrWithdrawer), "TeleportrWithdrawer");
// Give alice and bob some ETH
vm.deal(alice, 1 ether);
vm.deal(bob, 1 ether);
vm.label(alice, "alice");
vm.label(bob, "bob");
}
}
contract TeleportrWithdrawerTest is TeleportrWithdrawer_Initializer {
function setUp() public {
super._setUp();
}
// Tests if the owner was set correctly during deploy
function test_constructor() external {
assertEq(address(alice), teleportrWithdrawer.owner());
}
// Tests setRecipient function when called by authorized address
function test_setRecipient() external {
// Call setRecipient from alice
vm.prank(alice);
teleportrWithdrawer.setRecipient(address(alice));
assertEq(teleportrWithdrawer.recipient(), address(alice));
}
// setRecipient should fail if called by unauthorized address
function testFail_setRecipient() external {
teleportrWithdrawer.setRecipient(address(alice));
vm.expectRevert("UNAUTHORIZED");
}
// Tests setTeleportr function when called by authorized address
function test_setTeleportr() external {
// Call setRecipient from alice
vm.prank(alice);
teleportrWithdrawer.setTeleportr(address(mockTeleportr));
assertEq(teleportrWithdrawer.teleportr(), address(mockTeleportr));
}
// setTeleportr should fail if called by unauthorized address
function testFail_setTeleportr() external {
teleportrWithdrawer.setTeleportr(address(bob));
vm.expectRevert("UNAUTHORIZED");
}
// Tests setData function when called by authorized address
function test_setData() external {
bytes memory data = "0x1234567890";
// Call setData from alice
vm.prank(alice);
teleportrWithdrawer.setData(data);
assertEq(teleportrWithdrawer.data(), data);
}
// setData should fail if called by unauthorized address
function testFail_setData() external {
bytes memory data = "0x1234567890";
teleportrWithdrawer.setData(data);
vm.expectRevert("UNAUTHORIZED");
}
// Tests withdrawFromTeleportr, when called expected to withdraw the balance
// to the recipient address when the target is an EOA
function test_withdrawFromTeleportrToEOA() external {
// Fund the Teleportr contract with 1 ETH
vm.deal(address(teleportrWithdrawer), 1 ether);
// Set target address and Teleportr
vm.startPrank(alice);
teleportrWithdrawer.setRecipient(address(bob));
teleportrWithdrawer.setTeleportr(address(mockTeleportr));
vm.stopPrank();
// Run withdrawFromTeleportr
assertEq(address(bob).balance, 1 ether);
teleportrWithdrawer.withdrawFromTeleportr();
assertEq(address(bob).balance, 2 ether);
}
// When called from a contract account it should withdraw the balance and trigger the code
function test_withdrawFromTeleportrToContract() external {
bytes32 key = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;
bytes32 value = 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;
bytes memory data = abi.encodeWithSelector(simpleStorage.set.selector, key, value);
// Fund the Teleportr contract with 1 ETH
vm.deal(address(teleportrWithdrawer), 1 ether);
// Set target address and Teleportr
vm.startPrank(alice);
teleportrWithdrawer.setRecipient(address(simpleStorage));
teleportrWithdrawer.setTeleportr(address(mockTeleportr));
teleportrWithdrawer.setData(data);
vm.stopPrank();
// Run withdrawFromTeleportr
assertEq(address(simpleStorage).balance, 0);
teleportrWithdrawer.withdrawFromTeleportr();
assertEq(address(simpleStorage).balance, 1 ether);
assertEq(simpleStorage.get(key), value);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
contract MockTeleportr {
function withdrawBalance() external {
payable(msg.sender).transfer(address(this).balance);
}
}
/* Imports: External */
import { DeployFunction } from 'hardhat-deploy/dist/types'
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
const { deploy } = await hre.deployments.deterministic(
'TeleportrWithdrawer',
{
salt: hre.ethers.utils.solidityKeccak256(
['string'],
['TeleportrWithdrawer']
),
from: deployer,
args: [hre.deployConfig.ddd],
log: true,
}
)
await deploy()
}
deployFn.tags = ['TeleportrWithdrawer']
export default deployFn
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
"test": "yarn test:contracts", "test": "yarn test:contracts",
"test:contracts": "hardhat test --show-stack-traces", "test:contracts": "hardhat test --show-stack-traces",
"test:forge": "forge test", "test:forge": "forge test",
"test:coverage": "NODE_OPTIONS=--max_old_space_size=8192 hardhat coverage && istanbul check-coverage --statements 90 --branches 84 --functions 88 --lines 90", "test:coverage": "NODE_OPTIONS=--max_old_space_size=8192 hardhat coverage && istanbul check-coverage --statements 90 --branches 82 --functions 88 --lines 90",
"test:slither": "slither .", "test:slither": "slither .",
"gas-snapshot": "forge snapshot", "gas-snapshot": "forge snapshot",
"pretest:slither": "rm -f @openzeppelin && rm -f hardhat && ln -s node_modules/@openzeppelin @openzeppelin && ln -s ../../node_modules/hardhat hardhat", "pretest:slither": "rm -f @openzeppelin && rm -f hardhat && ln -s node_modules/@openzeppelin @openzeppelin && ln -s ../../node_modules/hardhat hardhat",
......
import { ethers } from 'hardhat'
import { Contract, BigNumber } from 'ethers'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from '../../setup'
import { deploy } from '../../helpers'
const initialMinDepositAmount = ethers.utils.parseEther('0.01')
const initialMaxDepositAmount = ethers.utils.parseEther('1')
const initialMaxBalance = ethers.utils.parseEther('2')
describe('TeleportrDeposit', async () => {
let signer1: SignerWithAddress
let signer2: SignerWithAddress
before(async () => {
;[signer1, signer2] = await ethers.getSigners()
})
let TeleportrDeposit: Contract
before(async () => {
TeleportrDeposit = await deploy('TeleportrDeposit', {
args: [
initialMinDepositAmount,
initialMaxDepositAmount,
initialMaxBalance,
],
})
})
describe('receive', async () => {
const oneETH = ethers.utils.parseEther('1.0')
const twoETH = ethers.utils.parseEther('2.0')
it('should revert if deposit amount is less than min amount', async () => {
await expect(
signer1.sendTransaction({
to: TeleportrDeposit.address,
value: ethers.utils.parseEther('0.001'),
})
).to.be.revertedWith('Deposit amount is too small')
})
it('should revert if deposit amount is greater than max amount', async () => {
await expect(
signer1.sendTransaction({
to: TeleportrDeposit.address,
value: ethers.utils.parseEther('1.1'),
})
).to.be.revertedWith('Deposit amount is too big')
})
it('should emit EtherReceived if called by non-owner', async () => {
await expect(
signer2.sendTransaction({
to: TeleportrDeposit.address,
value: oneETH,
})
)
.to.emit(TeleportrDeposit, 'EtherReceived')
.withArgs(BigNumber.from('0'), signer2.address, oneETH)
})
it('should increase the contract balance by deposit amount', async () => {
expect(
await ethers.provider.getBalance(TeleportrDeposit.address)
).to.equal(oneETH)
})
it('should emit EtherReceived if called by owner', async () => {
await expect(
signer1.sendTransaction({
to: TeleportrDeposit.address,
value: oneETH,
})
)
.to.emit(TeleportrDeposit, 'EtherReceived')
.withArgs(BigNumber.from('1'), signer1.address, oneETH)
})
it('should increase the contract balance by deposit amount', async () => {
expect(
await ethers.provider.getBalance(TeleportrDeposit.address)
).to.equal(twoETH)
})
it('should revert if deposit will exceed max balance', async () => {
await expect(
signer1.sendTransaction({
to: TeleportrDeposit.address,
value: initialMinDepositAmount,
})
).to.be.revertedWith('Contract max balance exceeded')
})
})
describe('withdrawBalance', async () => {
let initialContractBalance: BigNumber
let initialSignerBalance: BigNumber
before(async () => {
initialContractBalance = await ethers.provider.getBalance(
TeleportrDeposit.address
)
initialSignerBalance = await signer1.getBalance()
})
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDeposit.connect(signer2).withdrawBalance()
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit BalanceWithdrawn if called by owner', async () => {
await expect(TeleportrDeposit.withdrawBalance())
.to.emit(TeleportrDeposit, 'BalanceWithdrawn')
.withArgs(signer1.address, initialContractBalance)
})
it('should leave the contract with zero balance', async () => {
expect(
await ethers.provider.getBalance(TeleportrDeposit.address)
).to.equal(ethers.utils.parseEther('0'))
})
it('should credit owner with contract balance - fees', async () => {
const expSignerBalance = initialSignerBalance.add(initialContractBalance)
expect(await signer1.getBalance()).to.be.closeTo(
expSignerBalance,
10 ** 15
)
})
})
describe('setMinAmount', async () => {
const newMinDepositAmount = ethers.utils.parseEther('0.02')
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDeposit.connect(signer2).setMinAmount(newMinDepositAmount)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit MinDepositAmountSet if called by owner', async () => {
await expect(TeleportrDeposit.setMinAmount(newMinDepositAmount))
.to.emit(TeleportrDeposit, 'MinDepositAmountSet')
.withArgs(initialMinDepositAmount, newMinDepositAmount)
})
it('should have updated minDepositAmount after success', async () => {
expect(await TeleportrDeposit.minDepositAmount()).to.be.eq(
newMinDepositAmount
)
})
})
describe('setMaxAmount', async () => {
const newMaxDepositAmount = ethers.utils.parseEther('2')
it('should revert if called non-owner', async () => {
await expect(
TeleportrDeposit.connect(signer2).setMaxAmount(newMaxDepositAmount)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit MaxDepositAmountSet if called by owner', async () => {
await expect(TeleportrDeposit.setMaxAmount(newMaxDepositAmount))
.to.emit(TeleportrDeposit, 'MaxDepositAmountSet')
.withArgs(initialMaxDepositAmount, newMaxDepositAmount)
})
it('should have an updated maxDepositAmount after success', async () => {
expect(await TeleportrDeposit.maxDepositAmount()).to.be.eq(
newMaxDepositAmount
)
})
})
describe('setMaxBalance', async () => {
const newMaxBalance = ethers.utils.parseEther('2000')
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDeposit.connect(signer2).setMaxBalance(newMaxBalance)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit MaxBalanceSet if called by owner', async () => {
await expect(TeleportrDeposit.setMaxBalance(newMaxBalance))
.to.emit(TeleportrDeposit, 'MaxBalanceSet')
.withArgs(initialMaxBalance, newMaxBalance)
})
it('should have an updated maxBalance after success', async () => {
expect(await TeleportrDeposit.maxBalance()).to.be.eq(newMaxBalance)
})
})
})
import { ethers } from 'hardhat'
import { Contract, BigNumber } from 'ethers'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from '../../setup'
import { deploy } from '../../helpers'
const zeroETH = ethers.utils.parseEther('0.0')
const oneETH = ethers.utils.parseEther('1.0')
const twoETH = ethers.utils.parseEther('2.0')
describe('TeleportrDisburser', async () => {
let signer1: SignerWithAddress
let signer2: SignerWithAddress
before(async () => {
;[signer1, signer2] = await ethers.getSigners()
})
let TeleportrDisburser: Contract
let FailingReceiver: Contract
before(async () => {
TeleportrDisburser = await deploy('TeleportrDisburser')
FailingReceiver = await deploy('FailingReceiver')
})
describe('disburse checks', async () => {
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDisburser.connect(signer2).disburse(0, [], { value: oneETH })
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should revert if no disbursements is zero length', async () => {
await expect(
TeleportrDisburser.disburse(0, [], { value: oneETH })
).to.be.revertedWith('No disbursements')
})
it('should revert if nextDepositId does not match expected value', async () => {
await expect(
TeleportrDisburser.disburse(1, [[oneETH, signer2.address]], {
value: oneETH,
})
).to.be.revertedWith('Unexpected next deposit id')
})
it('should revert if msg.value does not match total to disburse', async () => {
await expect(
TeleportrDisburser.disburse(0, [[oneETH, signer2.address]], {
value: zeroETH,
})
).to.be.revertedWith('Disbursement total != amount sent')
})
})
describe('disburse single success', async () => {
let signer1InitialBalance: BigNumber
let signer2InitialBalance: BigNumber
before(async () => {
signer1InitialBalance = await ethers.provider.getBalance(signer1.address)
signer2InitialBalance = await ethers.provider.getBalance(signer2.address)
})
it('should emit DisbursementSuccess for successful disbursement', async () => {
await expect(
TeleportrDisburser.disburse(0, [[oneETH, signer2.address]], {
value: oneETH,
})
)
.to.emit(TeleportrDisburser, 'DisbursementSuccess')
.withArgs(BigNumber.from(0), signer2.address, oneETH)
})
it('should show one total disbursement', async () => {
expect(await TeleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(1)
)
})
it('should leave contract balance at zero ETH', async () => {
expect(
await ethers.provider.getBalance(TeleportrDisburser.address)
).to.be.equal(zeroETH)
})
it('should increase recipients balance by disbursement amount', async () => {
expect(await ethers.provider.getBalance(signer2.address)).to.be.equal(
signer2InitialBalance.add(oneETH)
)
})
it('should decrease owners balance by disbursement amount - fees', async () => {
expect(await ethers.provider.getBalance(signer1.address)).to.be.closeTo(
signer1InitialBalance.sub(oneETH),
10 ** 15
)
})
})
describe('disburse single failure', async () => {
let signer1InitialBalance: BigNumber
before(async () => {
signer1InitialBalance = await ethers.provider.getBalance(signer1.address)
})
it('should emit DisbursementFailed for failed disbursement', async () => {
await expect(
TeleportrDisburser.disburse(1, [[oneETH, FailingReceiver.address]], {
value: oneETH,
})
)
.to.emit(TeleportrDisburser, 'DisbursementFailed')
.withArgs(BigNumber.from(1), FailingReceiver.address, oneETH)
})
it('should show two total disbursements', async () => {
expect(await TeleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(2)
)
})
it('should leave contract with disbursement amount', async () => {
expect(
await ethers.provider.getBalance(TeleportrDisburser.address)
).to.be.equal(oneETH)
})
it('should leave recipients balance at zero ETH', async () => {
expect(
await ethers.provider.getBalance(FailingReceiver.address)
).to.be.equal(zeroETH)
})
it('should decrease owners balance by disbursement amount - fees', async () => {
expect(await ethers.provider.getBalance(signer1.address)).to.be.closeTo(
signer1InitialBalance.sub(oneETH),
10 ** 15
)
})
})
describe('withdrawBalance', async () => {
let signer1InitialBalance: BigNumber
let disburserInitialBalance: BigNumber
before(async () => {
signer1InitialBalance = await ethers.provider.getBalance(signer1.address)
disburserInitialBalance = await ethers.provider.getBalance(
TeleportrDisburser.address
)
})
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDisburser.connect(signer2).withdrawBalance()
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit BalanceWithdrawn if called by owner', async () => {
await expect(TeleportrDisburser.withdrawBalance())
.to.emit(TeleportrDisburser, 'BalanceWithdrawn')
.withArgs(signer1.address, oneETH)
})
it('should leave contract with zero balance', async () => {
expect(
await ethers.provider.getBalance(TeleportrDisburser.address)
).to.equal(zeroETH)
})
it('should credit owner with contract balance - fees', async () => {
expect(await ethers.provider.getBalance(signer1.address)).to.be.closeTo(
signer1InitialBalance.add(disburserInitialBalance),
10 ** 15
)
})
})
describe('disburse multiple', async () => {
let signer1InitialBalance: BigNumber
let signer2InitialBalance: BigNumber
before(async () => {
signer1InitialBalance = await ethers.provider.getBalance(signer1.address)
signer2InitialBalance = await ethers.provider.getBalance(signer2.address)
})
it('should emit DisbursementSuccess for successful disbursement', async () => {
await expect(
TeleportrDisburser.disburse(
2,
[
[oneETH, signer2.address],
[oneETH, FailingReceiver.address],
],
{ value: twoETH }
)
).to.not.be.reverted
})
it('should show four total disbursements', async () => {
expect(await TeleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(4)
)
})
it('should leave contract balance with failed disbursement amount', async () => {
expect(
await ethers.provider.getBalance(TeleportrDisburser.address)
).to.be.equal(oneETH)
})
it('should increase success recipients balance by disbursement amount', async () => {
expect(await ethers.provider.getBalance(signer2.address)).to.be.equal(
signer2InitialBalance.add(oneETH)
)
})
it('should leave failed recipients balance at zero ETH', async () => {
expect(
await ethers.provider.getBalance(FailingReceiver.address)
).to.be.equal(zeroETH)
})
it('should decrease owners balance by disbursement 2*amount - fees', async () => {
expect(await ethers.provider.getBalance(signer1.address)).to.be.closeTo(
signer1InitialBalance.sub(twoETH),
10 ** 15
)
})
})
})
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)
})
})
})
})
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.9;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title TeleportrDeposit
*
* Shout out to 0xclem for providing the inspiration for this contract:
* https://github.com/0xclem/teleportr/blob/main/contracts/BridgeDeposit.sol
*/
contract TeleportrDeposit is Ownable {
/// The minimum amount that be deposited in a receive.
uint256 public minDepositAmount;
/// The maximum amount that be deposited in a receive.
uint256 public maxDepositAmount;
/// The maximum balance the contract can hold after a receive.
uint256 public maxBalance;
/// The total number of successful deposits received.
uint256 public totalDeposits;
/**
* @notice Emitted any time the minimum deposit amount is set.
* @param previousAmount The previous minimum deposit amount.
* @param newAmount The new minimum deposit amount.
*/
event MinDepositAmountSet(uint256 previousAmount, uint256 newAmount);
/**
* @notice Emitted any time the maximum deposit amount is set.
* @param previousAmount The previous maximum deposit amount.
* @param newAmount The new maximum deposit amount.
*/
event MaxDepositAmountSet(uint256 previousAmount, uint256 newAmount);
/**
* @notice Emitted any time the contract maximum balance is set.
* @param previousBalance The previous maximum contract balance.
* @param newBalance The new maximum contract balance.
*/
event MaxBalanceSet(uint256 previousBalance, uint256 newBalance);
/**
* @notice Emitted any time the balance is withdrawn by the owner.
* @param owner The current owner and recipient of the funds.
* @param balance The current contract balance paid to the owner.
*/
event BalanceWithdrawn(address indexed owner, uint256 balance);
/**
* @notice Emitted any time a successful deposit is received.
* @param depositId A unique sequencer number identifying the deposit.
* @param emitter The sending address of the payer.
* @param amount The amount deposited by the payer.
*/
event EtherReceived(uint256 indexed depositId, address indexed emitter, uint256 indexed amount);
/**
* @notice Initializes a new TeleportrDeposit contract.
* @param _minDepositAmount The initial minimum deposit amount.
* @param _maxDepositAmount The initial maximum deposit amount.
* @param _maxBalance The initial maximum contract balance.
*/
constructor(
uint256 _minDepositAmount,
uint256 _maxDepositAmount,
uint256 _maxBalance
) {
minDepositAmount = _minDepositAmount;
maxDepositAmount = _maxDepositAmount;
maxBalance = _maxBalance;
totalDeposits = 0;
emit MinDepositAmountSet(0, _minDepositAmount);
emit MaxDepositAmountSet(0, _maxDepositAmount);
emit MaxBalanceSet(0, _maxBalance);
}
/**
* @notice Accepts deposits that will be disbursed to the sender's address on L2.
* The method reverts if the amount is less than the current
* minDepositAmount, the amount is greater than the current
* maxDepositAmount, or the amount causes the contract to exceed its maximum
* allowed balance.
*/
receive() external payable {
require(msg.value >= minDepositAmount, "Deposit amount is too small");
require(msg.value <= maxDepositAmount, "Deposit amount is too big");
require(address(this).balance <= maxBalance, "Contract max balance exceeded");
emit EtherReceived(totalDeposits, msg.sender, msg.value);
unchecked {
totalDeposits += 1;
}
}
/**
* @notice Sends the contract's current balance to the owner.
*/
function withdrawBalance() external onlyOwner {
address _owner = owner();
uint256 _balance = address(this).balance;
emit BalanceWithdrawn(_owner, _balance);
payable(_owner).transfer(_balance);
}
/**
* @notice Sets the minimum amount that can be deposited in a receive.
* @param _minDepositAmount The new minimum deposit amount.
*/
function setMinAmount(uint256 _minDepositAmount) external onlyOwner {
emit MinDepositAmountSet(minDepositAmount, _minDepositAmount);
minDepositAmount = _minDepositAmount;
}
/**
* @notice Sets the maximum amount that can be deposited in a receive.
* @param _maxDepositAmount The new maximum deposit amount.
*/
function setMaxAmount(uint256 _maxDepositAmount) external onlyOwner {
emit MaxDepositAmountSet(maxDepositAmount, _maxDepositAmount);
maxDepositAmount = _maxDepositAmount;
}
/**
* @notice Sets the maximum balance the contract can hold after a receive.
* @param _maxBalance The new maximum contract balance.
*/
function setMaxBalance(uint256 _maxBalance) external onlyOwner {
emit MaxBalanceSet(maxBalance, _maxBalance);
maxBalance = _maxBalance;
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.9;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title TeleportrDisburser
*/
contract TeleportrDisburser is Ownable {
/**
* @notice A struct holding the address and amount to disbursement.
*/
struct Disbursement {
uint256 amount;
address addr;
}
/// The total number of disbursements processed.
uint256 public totalDisbursements;
/**
* @notice Emitted any time the balance is withdrawn by the owner.
* @param owner The current owner and recipient of the funds.
* @param balance The current contract balance paid to the owner.
*/
event BalanceWithdrawn(address indexed owner, uint256 balance);
/**
* @notice Emitted any time a disbursement is successfuly sent.
* @param depositId The unique sequence number identifying the deposit.
* @param to The recipient of the disbursement.
* @param amount The amount sent to the recipient.
*/
event DisbursementSuccess(uint256 indexed depositId, address indexed to, uint256 amount);
/**
* @notice Emitted any time a disbursement fails to send.
* @param depositId The unique sequence number identifying the deposit.
* @param to The intended recipient of the disbursement.
* @param amount The amount intended to be sent to the recipient.
*/
event DisbursementFailed(uint256 indexed depositId, address indexed to, uint256 amount);
/**
* @notice Initializes a new TeleportrDisburser contract.
*/
constructor() {
totalDisbursements = 0;
}
/**
* @notice Accepts a list of Disbursements and forwards the amount paid to
* the contract to each recipient. The method reverts if there are zero
* disbursements, the total amount to forward differs from the amount sent
* in the transaction, or the _nextDepositId is unexpected. Failed
* disbursements will not cause the method to revert, but will instead be
* held by the contract and availabe for the owner to withdraw.
* @param _nextDepositId The depositId of the first Dispursement.
* @param _disbursements A list of Disbursements to process.
*/
function disburse(uint256 _nextDepositId, Disbursement[] calldata _disbursements)
external
payable
onlyOwner
{
// Ensure there are disbursements to process.
uint256 _numDisbursements = _disbursements.length;
require(_numDisbursements > 0, "No disbursements");
// Ensure the _nextDepositId matches our expected value.
uint256 _depositId = totalDisbursements;
require(_depositId == _nextDepositId, "Unexpected next deposit id");
unchecked {
totalDisbursements += _numDisbursements;
}
// Ensure the amount sent in the transaction is equal to the sum of the
// disbursements.
uint256 _totalDisbursed = 0;
for (uint256 i = 0; i < _numDisbursements; i++) {
_totalDisbursed += _disbursements[i].amount;
}
require(_totalDisbursed == msg.value, "Disbursement total != amount sent");
// Process disbursements.
for (uint256 i = 0; i < _numDisbursements; i++) {
uint256 _amount = _disbursements[i].amount;
address _addr = _disbursements[i].addr;
// Deliver the dispursement amount to the receiver. If the
// disbursement fails, the amount will be kept by the contract
// rather than reverting to prevent blocking progress on other
// disbursements.
// slither-disable-next-line calls-loop,reentrancy-events
(bool success, ) = _addr.call{ value: _amount, gas: 2300 }("");
if (success) emit DisbursementSuccess(_depositId, _addr, _amount);
else emit DisbursementFailed(_depositId, _addr, _amount);
unchecked {
_depositId += 1;
}
}
}
/**
* @notice Sends the contract's current balance to the owner.
*/
function withdrawBalance() external onlyOwner {
address _owner = owner();
uint256 balance = address(this).balance;
emit BalanceWithdrawn(_owner, balance);
payable(_owner).transfer(balance);
}
}
/* Imports: External */
import { ethers } from 'ethers'
import { task } from 'hardhat/config'
import * as types from 'hardhat/internal/core/params/argumentTypes'
task('deploy-teleportr-l1')
.addParam(
'minDepositAmountEth',
'Minimum deposit amount, in ETH.',
undefined,
types.string
)
.addParam(
'maxDepositAmountEth',
'Maximum deposit amount, in ETH.',
undefined,
types.string
)
.addParam(
'maxBalanceEth',
'Maximum contract balance, in ETH.',
undefined,
types.string
)
.addOptionalParam(
'numDeployConfirmations',
'Number of confirmations to wait for each transaction in the deployment. More is safer.',
1,
types.int
)
.setAction(
async (
{
minDepositAmountEth,
maxDepositAmountEth,
maxBalanceEth,
numDeployConfirmations,
},
hre: any
) => {
const { deploy } = hre.deployments
const { deployer } = await hre.getNamedAccounts()
console.log('Deploying TeleportrDeposit... ')
await deploy('TeleportrDeposit', {
from: deployer,
args: [
ethers.utils.parseEther(minDepositAmountEth),
ethers.utils.parseEther(maxDepositAmountEth),
ethers.utils.parseEther(maxBalanceEth),
],
log: true,
waitConfirmations: numDeployConfirmations,
})
console.log('Done.')
}
)
task('deploy-teleportr-l2').setAction(
async ({ numDeployConfirmations }, hre: any) => {
const { deploy } = hre.deployments
const { deployer } = await hre.getNamedAccounts()
console.log('Deploying TeleportrDisburser... ')
await deploy('TeleportrDisburser', {
from: deployer,
args: [],
log: true,
waitConfirmations: numDeployConfirmations,
})
console.log('Done.')
}
)
...@@ -5,5 +5,4 @@ export * from './validate-chugsplash-dictator' ...@@ -5,5 +5,4 @@ export * from './validate-chugsplash-dictator'
export * from './whitelist' export * from './whitelist'
export * from './withdraw-fees' export * from './withdraw-fees'
export * from './fetch-batches' export * from './fetch-batches'
export * from './deploy-teleportr'
export * from './inspect' export * from './inspect'
import { ethers } from 'hardhat'
import { Contract, BigNumber } from 'ethers'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from '../../../setup'
import { deploy } from '../../../helpers'
const initialMinDepositAmount = ethers.utils.parseEther('0.01')
const initialMaxDepositAmount = ethers.utils.parseEther('1')
const initialMaxBalance = ethers.utils.parseEther('2')
describe('TeleportrDeposit', async () => {
let signer1: SignerWithAddress
let signer2: SignerWithAddress
before(async () => {
;[signer1, signer2] = await ethers.getSigners()
})
let TeleportrDeposit: Contract
before(async () => {
TeleportrDeposit = await deploy('TeleportrDeposit', {
args: [
initialMinDepositAmount,
initialMaxDepositAmount,
initialMaxBalance,
],
})
})
describe('receive', async () => {
const oneETH = ethers.utils.parseEther('1.0')
const twoETH = ethers.utils.parseEther('2.0')
it('should revert if deposit amount is less than min amount', async () => {
await expect(
signer1.sendTransaction({
to: TeleportrDeposit.address,
value: ethers.utils.parseEther('0.001'),
})
).to.be.revertedWith('Deposit amount is too small')
})
it('should revert if deposit amount is greater than max amount', async () => {
await expect(
signer1.sendTransaction({
to: TeleportrDeposit.address,
value: ethers.utils.parseEther('1.1'),
})
).to.be.revertedWith('Deposit amount is too big')
})
it('should emit EtherReceived if called by non-owner', async () => {
await expect(
signer2.sendTransaction({
to: TeleportrDeposit.address,
value: oneETH,
})
)
.to.emit(TeleportrDeposit, 'EtherReceived')
.withArgs(BigNumber.from('0'), signer2.address, oneETH)
})
it('should increase the contract balance by deposit amount', async () => {
expect(
await ethers.provider.getBalance(TeleportrDeposit.address)
).to.equal(oneETH)
})
it('should emit EtherReceived if called by owner', async () => {
await expect(
signer1.sendTransaction({
to: TeleportrDeposit.address,
value: oneETH,
})
)
.to.emit(TeleportrDeposit, 'EtherReceived')
.withArgs(BigNumber.from('1'), signer1.address, oneETH)
})
it('should increase the contract balance by deposit amount', async () => {
expect(
await ethers.provider.getBalance(TeleportrDeposit.address)
).to.equal(twoETH)
})
it('should revert if deposit will exceed max balance', async () => {
await expect(
signer1.sendTransaction({
to: TeleportrDeposit.address,
value: initialMinDepositAmount,
})
).to.be.revertedWith('Contract max balance exceeded')
})
})
describe('withdrawBalance', async () => {
let initialContractBalance: BigNumber
let initialSignerBalance: BigNumber
before(async () => {
initialContractBalance = await ethers.provider.getBalance(
TeleportrDeposit.address
)
initialSignerBalance = await signer1.getBalance()
})
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDeposit.connect(signer2).withdrawBalance()
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit BalanceWithdrawn if called by owner', async () => {
await expect(TeleportrDeposit.withdrawBalance())
.to.emit(TeleportrDeposit, 'BalanceWithdrawn')
.withArgs(signer1.address, initialContractBalance)
})
it('should leave the contract with zero balance', async () => {
expect(
await ethers.provider.getBalance(TeleportrDeposit.address)
).to.equal(ethers.utils.parseEther('0'))
})
it('should credit owner with contract balance - fees', async () => {
const expSignerBalance = initialSignerBalance.add(initialContractBalance)
expect(await signer1.getBalance()).to.be.closeTo(
expSignerBalance,
10 ** 15
)
})
})
describe('setMinAmount', async () => {
const newMinDepositAmount = ethers.utils.parseEther('0.02')
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDeposit.connect(signer2).setMinAmount(newMinDepositAmount)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit MinDepositAmountSet if called by owner', async () => {
await expect(TeleportrDeposit.setMinAmount(newMinDepositAmount))
.to.emit(TeleportrDeposit, 'MinDepositAmountSet')
.withArgs(initialMinDepositAmount, newMinDepositAmount)
})
it('should have updated minDepositAmount after success', async () => {
expect(await TeleportrDeposit.minDepositAmount()).to.be.eq(
newMinDepositAmount
)
})
})
describe('setMaxAmount', async () => {
const newMaxDepositAmount = ethers.utils.parseEther('2')
it('should revert if called non-owner', async () => {
await expect(
TeleportrDeposit.connect(signer2).setMaxAmount(newMaxDepositAmount)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit MaxDepositAmountSet if called by owner', async () => {
await expect(TeleportrDeposit.setMaxAmount(newMaxDepositAmount))
.to.emit(TeleportrDeposit, 'MaxDepositAmountSet')
.withArgs(initialMaxDepositAmount, newMaxDepositAmount)
})
it('should have an updated maxDepositAmount after success', async () => {
expect(await TeleportrDeposit.maxDepositAmount()).to.be.eq(
newMaxDepositAmount
)
})
})
describe('setMaxBalance', async () => {
const newMaxBalance = ethers.utils.parseEther('2000')
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDeposit.connect(signer2).setMaxBalance(newMaxBalance)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit MaxBalanceSet if called by owner', async () => {
await expect(TeleportrDeposit.setMaxBalance(newMaxBalance))
.to.emit(TeleportrDeposit, 'MaxBalanceSet')
.withArgs(initialMaxBalance, newMaxBalance)
})
it('should have an updated maxBalance after success', async () => {
expect(await TeleportrDeposit.maxBalance()).to.be.eq(newMaxBalance)
})
})
})
import { ethers } from 'hardhat'
import { Contract, BigNumber } from 'ethers'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from '../../../setup'
import { deploy } from '../../../helpers'
const zeroETH = ethers.utils.parseEther('0.0')
const oneETH = ethers.utils.parseEther('1.0')
const twoETH = ethers.utils.parseEther('2.0')
describe('TeleportrDisburser', async () => {
let signer1: SignerWithAddress
let signer2: SignerWithAddress
before(async () => {
;[signer1, signer2] = await ethers.getSigners()
})
let TeleportrDisburser: Contract
let FailingReceiver: Contract
before(async () => {
TeleportrDisburser = await deploy('TeleportrDisburser')
FailingReceiver = await deploy('FailingReceiver')
})
describe('disburse checks', async () => {
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDisburser.connect(signer2).disburse(0, [], { value: oneETH })
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should revert if no disbursements is zero length', async () => {
await expect(
TeleportrDisburser.disburse(0, [], { value: oneETH })
).to.be.revertedWith('No disbursements')
})
it('should revert if nextDepositId does not match expected value', async () => {
await expect(
TeleportrDisburser.disburse(1, [[oneETH, signer2.address]], {
value: oneETH,
})
).to.be.revertedWith('Unexpected next deposit id')
})
it('should revert if msg.value does not match total to disburse', async () => {
await expect(
TeleportrDisburser.disburse(0, [[oneETH, signer2.address]], {
value: zeroETH,
})
).to.be.revertedWith('Disbursement total != amount sent')
})
})
describe('disburse single success', async () => {
let signer1InitialBalance: BigNumber
let signer2InitialBalance: BigNumber
before(async () => {
signer1InitialBalance = await ethers.provider.getBalance(signer1.address)
signer2InitialBalance = await ethers.provider.getBalance(signer2.address)
})
it('should emit DisbursementSuccess for successful disbursement', async () => {
await expect(
TeleportrDisburser.disburse(0, [[oneETH, signer2.address]], {
value: oneETH,
})
)
.to.emit(TeleportrDisburser, 'DisbursementSuccess')
.withArgs(BigNumber.from(0), signer2.address, oneETH)
})
it('should show one total disbursement', async () => {
expect(await TeleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(1)
)
})
it('should leave contract balance at zero ETH', async () => {
expect(
await ethers.provider.getBalance(TeleportrDisburser.address)
).to.be.equal(zeroETH)
})
it('should increase recipients balance by disbursement amount', async () => {
expect(await ethers.provider.getBalance(signer2.address)).to.be.equal(
signer2InitialBalance.add(oneETH)
)
})
it('should decrease owners balance by disbursement amount - fees', async () => {
expect(await ethers.provider.getBalance(signer1.address)).to.be.closeTo(
signer1InitialBalance.sub(oneETH),
10 ** 15
)
})
})
describe('disburse single failure', async () => {
let signer1InitialBalance: BigNumber
before(async () => {
signer1InitialBalance = await ethers.provider.getBalance(signer1.address)
})
it('should emit DisbursementFailed for failed disbursement', async () => {
await expect(
TeleportrDisburser.disburse(1, [[oneETH, FailingReceiver.address]], {
value: oneETH,
})
)
.to.emit(TeleportrDisburser, 'DisbursementFailed')
.withArgs(BigNumber.from(1), FailingReceiver.address, oneETH)
})
it('should show two total disbursements', async () => {
expect(await TeleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(2)
)
})
it('should leave contract with disbursement amount', async () => {
expect(
await ethers.provider.getBalance(TeleportrDisburser.address)
).to.be.equal(oneETH)
})
it('should leave recipients balance at zero ETH', async () => {
expect(
await ethers.provider.getBalance(FailingReceiver.address)
).to.be.equal(zeroETH)
})
it('should decrease owners balance by disbursement amount - fees', async () => {
expect(await ethers.provider.getBalance(signer1.address)).to.be.closeTo(
signer1InitialBalance.sub(oneETH),
10 ** 15
)
})
})
describe('withdrawBalance', async () => {
let signer1InitialBalance: BigNumber
let disburserInitialBalance: BigNumber
before(async () => {
signer1InitialBalance = await ethers.provider.getBalance(signer1.address)
disburserInitialBalance = await ethers.provider.getBalance(
TeleportrDisburser.address
)
})
it('should revert if called by non-owner', async () => {
await expect(
TeleportrDisburser.connect(signer2).withdrawBalance()
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit BalanceWithdrawn if called by owner', async () => {
await expect(TeleportrDisburser.withdrawBalance())
.to.emit(TeleportrDisburser, 'BalanceWithdrawn')
.withArgs(signer1.address, oneETH)
})
it('should leave contract with zero balance', async () => {
expect(
await ethers.provider.getBalance(TeleportrDisburser.address)
).to.equal(zeroETH)
})
it('should credit owner with contract balance - fees', async () => {
expect(await ethers.provider.getBalance(signer1.address)).to.be.closeTo(
signer1InitialBalance.add(disburserInitialBalance),
10 ** 15
)
})
})
describe('disburse multiple', async () => {
let signer1InitialBalance: BigNumber
let signer2InitialBalance: BigNumber
before(async () => {
signer1InitialBalance = await ethers.provider.getBalance(signer1.address)
signer2InitialBalance = await ethers.provider.getBalance(signer2.address)
})
it('should emit DisbursementSuccess for successful disbursement', async () => {
await expect(
TeleportrDisburser.disburse(
2,
[
[oneETH, signer2.address],
[oneETH, FailingReceiver.address],
],
{ value: twoETH }
)
).to.not.be.reverted
})
it('should show four total disbursements', async () => {
expect(await TeleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(4)
)
})
it('should leave contract balance with failed disbursement amount', async () => {
expect(
await ethers.provider.getBalance(TeleportrDisburser.address)
).to.be.equal(oneETH)
})
it('should increase success recipients balance by disbursement amount', async () => {
expect(await ethers.provider.getBalance(signer2.address)).to.be.equal(
signer2InitialBalance.add(oneETH)
)
})
it('should leave failed recipients balance at zero ETH', async () => {
expect(
await ethers.provider.getBalance(FailingReceiver.address)
).to.be.equal(zeroETH)
})
it('should decrease owners balance by disbursement 2*amount - fees', async () => {
expect(await ethers.provider.getBalance(signer1.address)).to.be.closeTo(
signer1InitialBalance.sub(twoETH),
10 ** 15
)
})
})
})
...@@ -34,7 +34,6 @@ Go modules which are not yet versioned: ...@@ -34,7 +34,6 @@ Go modules which are not yet versioned:
./l2geth-exporter (changesets) ./l2geth-exporter (changesets)
./op-exporter (changesets) ./op-exporter (changesets)
./proxyd (changesets) ./proxyd (changesets)
./teleportr (changesets)
./state-surgery ./state-surgery
``` ```
......
# @eth-optimism/teleportr
## 0.0.11
### Patch Changes
- 29ff7462: Revert es target back to 2017
## 0.0.10
### Patch Changes
- ed3a39fb: Fix panic
## 0.0.9
### Patch Changes
- 23dcba53: Better availability endpoint + retries
## 0.0.8
### Patch Changes
- 487a9731: Improve metrics
- b5ee3c70: Increase max disbursements to 15
## 0.0.7
### Patch Changes
- 6f458607: Bump go-ethereum to 1.10.17
- cd15c40a: Only do 5 disbursements at a time
## 0.0.6
### Patch Changes
- df61d215: Add disburser balance to status
- 32639605: Fix teleportr FailedDatabaseOperations method for upsert_disbursement
- 32639605: Expose metrics server
## 0.0.5
### Patch Changes
- 44c293d8: Fix confs_remaining calculation to prevent underflow
## 0.0.4
### Patch Changes
- 7a320e22: Use L2 gas price in driver
## 0.0.3
### Patch Changes
- 160f4c3d: Update docker image to use golang 1.18.0
## 0.0.2
### Patch Changes
- f101d38b: Add metrics for balances
## 0.0.1
### Patch Changes
- 172c3d74: Add SuggestGasTipCap fallback
- 6856b215: Count reverted transactions in failed_submissions
- f4f3054a: Add teleportr API server
- 3e57f559: Bump `go-ethereum` to `v1.10.16`
- bf1cc8f4: Restructure Deposit and CompletedTeleport to use struct embeddings
- bced4fa9: Add LoadInTeleport method to database
- e5732d97: Add btree index on deposit.txn_hash and deposit.address
FROM golang:1.18.0-alpine3.15 as builder
RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash
COPY ./bss-core /go/bss-core
COPY ./teleportr /go/teleportr
COPY ./teleportr/docker.go.work /go/go.work
WORKDIR /go/teleportr
RUN make teleportr teleportr-api
FROM alpine:3.15
RUN apk add --no-cache ca-certificates jq curl
COPY --from=builder /go/teleportr/teleportr /usr/local/bin/
COPY --from=builder /go/teleportr/teleportr-api /usr/local/bin/
CMD ["teleportr"]
GITCOMMIT := $(shell git rev-parse HEAD)
GITDATE := $(shell git show -s --format='%ct')
GITVERSION := $(shell cat package.json | jq .version)
LDFLAGSSTRING +=-X main.GitCommit=$(GITCOMMIT)
LDFLAGSSTRING +=-X main.GitDate=$(GITDATE)
LDFLAGSSTRING +=-X main.GitVersion=$(GITVERSION)
LDFLAGS := -ldflags "$(LDFLAGSSTRING)"
DEPOSIT_ARTIFACT := ../../packages/contracts/artifacts/contracts/L1/teleportr/TeleportrDeposit.sol/TeleportrDeposit.json
DISBURSER_ARTIFACT := ../../packages/contracts/artifacts/contracts/L2/teleportr/TeleportrDisburser.sol/TeleportrDisburser.json
teleportr:
env GO111MODULE=on go build -v $(LDFLAGS) ./cmd/teleportr
teleportr-api:
env GO111MODULE=on go build -v $(LDFLAGS) ./cmd/teleportr-api
clean:
rm teleportr
rm api
test:
go test -v ./...
lint:
golangci-lint run ./...
bindings: bindings-deposit bindings-disburser
bindings-deposit:
$(eval temp := $(shell mktemp))
cat $(DEPOSIT_ARTIFACT) | jq -r .bytecode > $(temp)
cat $(DEPOSIT_ARTIFACT) | jq .abi | \
abigen \
--pkg deposit \
--abi - \
--out bindings/deposit/teleportr_deposit.go \
--type TeleportrDeposit \
--bin $(temp)
bindings-disburser:
$(eval temp := $(shell mktemp))
cat $(DISBURSER_ARTIFACT) | jq -r .bytecode > $(temp)
cat $(DISBURSER_ARTIFACT) | jq .abi | \
abigen \
--pkg disburse \
--abi - \
--out bindings/disburse/teleportr_disburser.go \
--type TeleportrDisburser \
--bin $(temp)
.PHONY: \
teleportr \
teleportr-api \
bindings \
bindings-deposit \
bindings-disburser \
clean \
test \
lint
package api
import (
"context"
"math/big"
"sync"
"time"
"github.com/ethereum-optimism/optimism/teleportr/bindings/deposit"
"github.com/ethereum-optimism/optimism/teleportr/bindings/disburse"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
type ChainDataReader interface {
Get(ctx context.Context) (*ChainData, error)
}
type ChainData struct {
MaxBalance *big.Int
DisburserBalance *big.Int
NextDisbursementID uint64
DepositContractBalance *big.Int
NextDepositID uint64
MaxDepositAmount *big.Int
MinDepositAmount *big.Int
}
type chainDataReaderImpl struct {
l1Client *ethclient.Client
l2Client *ethclient.Client
depositContract *deposit.TeleportrDeposit
depositContractAddr common.Address
disburserContract *disburse.TeleportrDisburser
disburserWalletAddr common.Address
}
func NewChainDataReader(
l1Client, l2Client *ethclient.Client,
depositContractAddr, disburserWalletAddr common.Address,
depositContract *deposit.TeleportrDeposit,
disburserContract *disburse.TeleportrDisburser,
) ChainDataReader {
return &chainDataReaderImpl{
l1Client: l1Client,
l2Client: l2Client,
depositContract: depositContract,
depositContractAddr: depositContractAddr,
disburserContract: disburserContract,
disburserWalletAddr: disburserWalletAddr,
}
}
func (c *chainDataReaderImpl) maxDepositBalance(ctx context.Context) (*big.Int, error) {
return c.depositContract.MaxBalance(&bind.CallOpts{
Context: ctx,
})
}
func (c *chainDataReaderImpl) disburserBalance(ctx context.Context) (*big.Int, error) {
return c.l2Client.BalanceAt(ctx, c.disburserWalletAddr, nil)
}
func (c *chainDataReaderImpl) nextDisbursementID(ctx context.Context) (uint64, error) {
total, err := c.disburserContract.TotalDisbursements(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return 0, err
}
return total.Uint64(), nil
}
func (c *chainDataReaderImpl) depositContractBalance(ctx context.Context) (*big.Int, error) {
return c.l1Client.BalanceAt(ctx, c.depositContractAddr, nil)
}
func (c *chainDataReaderImpl) nextDepositID(ctx context.Context) (uint64, error) {
total, err := c.depositContract.TotalDeposits(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return 0, err
}
return total.Uint64(), nil
}
func (c *chainDataReaderImpl) maxDepositAmount(ctx context.Context) (*big.Int, error) {
return c.depositContract.MaxDepositAmount(&bind.CallOpts{
Context: ctx,
})
}
func (c *chainDataReaderImpl) minDepositAmount(ctx context.Context) (*big.Int, error) {
return c.depositContract.MinDepositAmount(&bind.CallOpts{
Context: ctx,
})
}
func (c *chainDataReaderImpl) Get(ctx context.Context) (*ChainData, error) {
maxBalance, err := c.maxDepositBalance(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("max_balance").Inc()
return nil, err
}
disburserBal, err := c.disburserBalance(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("disburser_wallet_balance_at").Inc()
return nil, err
}
nextDisbursementID, err := c.nextDisbursementID(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("next_disbursement_id").Inc()
return nil, err
}
depositContractBalance, err := c.depositContractBalance(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("deposit_balance_at").Inc()
return nil, err
}
nextDepositID, err := c.nextDepositID(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("next_deposit_id").Inc()
return nil, err
}
maxDepositAmount, err := c.maxDepositAmount(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("max_deposit_amount").Inc()
return nil, err
}
minDepositAmount, err := c.minDepositAmount(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("min_deposit_amount").Inc()
return nil, err
}
return &ChainData{
MaxBalance: maxBalance,
DisburserBalance: disburserBal,
NextDisbursementID: nextDisbursementID,
DepositContractBalance: depositContractBalance,
NextDepositID: nextDepositID,
MaxDepositAmount: maxDepositAmount,
MinDepositAmount: minDepositAmount,
}, nil
}
type cachingChainDataReader struct {
inner ChainDataReader
interval time.Duration
last time.Time
data *ChainData
mu sync.Mutex
}
func NewCachingChainDataReader(inner ChainDataReader, interval time.Duration) ChainDataReader {
return &cachingChainDataReader{
inner: inner,
interval: interval,
}
}
func (c *cachingChainDataReader) Get(ctx context.Context) (*ChainData, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.data != nil && time.Since(c.last) < c.interval {
return c.data, nil
}
data, err := c.inner.Get(ctx)
if err != nil {
return nil, err
}
c.data = data
c.last = time.Now()
return c.data, nil
}
package api
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type NoopChainDataReader struct {
CallCount int
Data *ChainData
}
func (n *NoopChainDataReader) Get(ctx context.Context) (*ChainData, error) {
n.CallCount++
return &ChainData{
MaxBalance: n.Data.MaxBalance,
DisburserBalance: n.Data.DisburserBalance,
NextDisbursementID: n.Data.NextDisbursementID,
DepositContractBalance: n.Data.DepositContractBalance,
NextDepositID: n.Data.NextDepositID,
MaxDepositAmount: n.Data.MaxDepositAmount,
MinDepositAmount: n.Data.MinDepositAmount,
}, nil
}
func TestCachingChainDataReaderGet(t *testing.T) {
inner := &NoopChainDataReader{
Data: &ChainData{
NextDisbursementID: 1,
},
}
require.Equal(t, inner.CallCount, 0)
cdr := NewCachingChainDataReader(inner, 5*time.Millisecond)
data, err := cdr.Get(context.Background())
require.NoError(t, err)
require.Equal(t, 1, inner.CallCount)
require.NotNil(t, data)
inner.Data = &ChainData{
NextDisbursementID: 2,
}
data, err = cdr.Get(context.Background())
require.NoError(t, err)
require.Equal(t, 1, inner.CallCount)
require.EqualValues(t, data.NextDisbursementID, 1)
time.Sleep(10 * time.Millisecond)
data, err = cdr.Get(context.Background())
require.NoError(t, err)
require.Equal(t, 2, inner.CallCount)
require.EqualValues(t, data.NextDisbursementID, 2)
}
package api
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const TeleportrAPINamespace = "teleportr_api"
var (
rpcRequestsTotal = promauto.NewCounter(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "rpc_requests_total",
Help: "Count of total client RPC requests.",
})
httpResponseCodesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "http_response_codes_total",
Help: "Count of total HTTP response codes.",
}, []string{
"status_code",
})
httpRequestDurationSumm = promauto.NewSummary(prometheus.SummaryOpts{
Namespace: TeleportrAPINamespace,
Name: "http_request_duration_seconds",
Help: "Summary of HTTP request durations, in seconds.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.005, 0.99: 0.001},
})
databaseErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "database_errors_total",
Help: "Count of total database failures.",
}, []string{
"method",
})
rpcErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "rpc_errors_total",
Help: "Count of total L1 rpc failures.",
}, []string{
"method",
})
)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
package main
import (
"fmt"
"os"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/teleportr/api"
"github.com/ethereum-optimism/optimism/teleportr/flags"
)
var (
GitVersion = ""
GitCommit = ""
GitDate = ""
)
func main() {
// Set up logger with a default INFO level in case we fail to parse flags.
// Otherwise the final critical log won't show what the parsing error was.
log.Root().SetHandler(
log.LvlFilterHandler(
log.LvlInfo,
log.StreamHandler(os.Stdout, log.TerminalFormat(true)),
),
)
app := cli.NewApp()
app.Flags = flags.APIFlags
app.Version = fmt.Sprintf("%s-%s-%s", GitVersion, GitCommit, GitDate)
app.Name = "teleportr-api"
app.Usage = "Teleportr API server"
app.Description = "API serving teleportr data"
app.Action = api.Main(GitVersion)
err := app.Run(os.Args)
if err != nil {
log.Crit("Application failed", "message", err)
}
}
package main
import (
"fmt"
"os"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/teleportr"
"github.com/ethereum-optimism/optimism/teleportr/flags"
)
var (
GitVersion = ""
GitCommit = ""
GitDate = ""
)
func main() {
// Set up logger with a default INFO level in case we fail to parse flags.
// Otherwise the final critical log won't show what the parsing error was.
log.Root().SetHandler(
log.LvlFilterHandler(
log.LvlInfo,
log.StreamHandler(os.Stdout, log.TerminalFormat(true)),
),
)
app := cli.NewApp()
app.Flags = flags.Flags
app.Version = fmt.Sprintf("%s-%s-%s", GitVersion, GitCommit, GitDate)
app.Name = "teleportr"
app.Usage = "Teleportr"
app.Description = "Teleportr bridge from L1 to L2"
app.Commands = []cli.Command{
{
Name: "migrate",
Usage: "Migrates teleportr's database",
Action: teleportr.Migrate(),
},
}
app.Action = teleportr.Main(GitVersion)
err := app.Run(os.Args)
if err != nil {
log.Crit("Application failed", "message", err)
}
}
package teleportr
import (
"time"
"github.com/ethereum-optimism/optimism/teleportr/flags"
"github.com/urfave/cli"
)
type Config struct {
/* Required Params */
// BuildEnv identifies the environment this binary is intended for, i.e.
// production, development, etc.
BuildEnv string
// EthNetworkName identifies the intended Ethereum network.
EthNetworkName string
// L1EthRpc is the HTTP provider URL for L1.
L1EthRpc string
// L2EthRpc is the HTTP provider URL for L1.
L2EthRpc string
// DepositAddress is the TeleportrDeposit contract adddress.
DepositAddress string
// DepositDeployBlockNumber is the deployment block number of the
// TeleportrDeposit contract.
DepositDeployBlockNumber uint64
// FilterQueryMaxBlocks is the maximum range of a filter query in blocks.
FilterQueryMaxBlocks uint64
// DisburserAddress is the TeleportrDisburser contract address.
DisburserAddress string
// MaxL2TxSize is the maximum size in bytes of any L2 transactions generated
// for teleportr disbursements.
MaxL2TxSize uint64
// NumDepositConfirmations is the number of confirmations required before a
// deposit is considered confirmed.
NumDepositConfirmations uint64
// PollInterval is the delay between querying L2 for more transaction
// and creating a new batch.
PollInterval time.Duration
// SafeAbortNonceTooLowCount is the number of ErrNonceTooLowObservations
// required to give up on a tx at a particular nonce without receiving
// confirmation.
SafeAbortNonceTooLowCount uint64
// ResubmissionTimeout is time we will wait before resubmitting a
// transaction.
ResubmissionTimeout time.Duration
// PostgresHost is the host of the teleportr postgres instance.
PostgresHost string
// PostgresPort is the port of the teleportr postgres instance.
PostgresPort uint16
// PostgresUser is the username for the teleportr postgres instance.
PostgresUser string
// PostgresPassword is the password for the teleportr postgres instance.
PostgresPassword string
// PostgresDBName is the database name of the teleportr postgres instance.
PostgresDBName string
// PostgresEnableSSL determines whether or not to enable SSL on connections
// to the teleportr postgres instance.
PostgresEnableSSL bool
/* Optional Params */
// LogLevel is the lowest log level that will be output.
LogLevel string
// LogTerminal if true, prints to stdout in terminal format, otherwise
// prints using JSON. If SentryEnable is true this flag is ignored, and logs
// are printed using JSON.
LogTerminal bool
// DisburserPrivKey the private key of the wallet used to submit
// transactions to the TeleportrDisburser contract.
DisburserPrivKey string
// Mnemonic is the HD seed used to derive the wallet private key for
// submitting to the TeleportrDisburser. Must be used in conjunction with
// DisburserHDPath.
Mnemonic string
// DisburserHDPath is the derivation path used to obtain the private key for
// the disburser transactions.
DisburserHDPath string
// MetricsServerEnable if true, will create a metrics client and log to
// Prometheus.
MetricsServerEnable bool
// MetricsHostname is the hostname at which the metrics server is running.
MetricsHostname string
// MetricsPort is the port at which the metrics server is running.
MetricsPort uint64
// DisableHTTP2 disables HTTP2 support.
DisableHTTP2 bool
}
func NewConfig(ctx *cli.Context) (Config, error) {
return Config{
/* Required Flags */
BuildEnv: ctx.GlobalString(flags.BuildEnvFlag.Name),
EthNetworkName: ctx.GlobalString(flags.EthNetworkNameFlag.Name),
L1EthRpc: ctx.GlobalString(flags.L1EthRpcFlag.Name),
L2EthRpc: ctx.GlobalString(flags.L2EthRpcFlag.Name),
DepositAddress: ctx.GlobalString(flags.DepositAddressFlag.Name),
DepositDeployBlockNumber: ctx.GlobalUint64(flags.DepositDeployBlockNumberFlag.Name),
DisburserAddress: ctx.GlobalString(flags.DisburserAddressFlag.Name),
MaxL2TxSize: ctx.GlobalUint64(flags.MaxL2TxSizeFlag.Name),
NumDepositConfirmations: ctx.GlobalUint64(flags.NumDepositConfirmationsFlag.Name),
FilterQueryMaxBlocks: ctx.GlobalUint64(flags.FilterQueryMaxBlocksFlag.Name),
PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name),
SafeAbortNonceTooLowCount: ctx.GlobalUint64(flags.SafeAbortNonceTooLowCountFlag.Name),
ResubmissionTimeout: ctx.GlobalDuration(flags.ResubmissionTimeoutFlag.Name),
PostgresHost: ctx.GlobalString(flags.PostgresHostFlag.Name),
PostgresPort: uint16(ctx.GlobalUint64(flags.PostgresPortFlag.Name)),
PostgresUser: ctx.GlobalString(flags.PostgresUserFlag.Name),
PostgresPassword: ctx.GlobalString(flags.PostgresPasswordFlag.Name),
PostgresDBName: ctx.GlobalString(flags.PostgresDBNameFlag.Name),
PostgresEnableSSL: ctx.GlobalBool(flags.PostgresEnableSSLFlag.Name),
/* Optional flags */
LogLevel: ctx.GlobalString(flags.LogLevelFlag.Name),
LogTerminal: ctx.GlobalBool(flags.LogTerminalFlag.Name),
DisburserPrivKey: ctx.GlobalString(flags.DisburserPrivateKeyFlag.Name),
Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name),
DisburserHDPath: ctx.GlobalString(flags.DisburserHDPathFlag.Name),
MetricsServerEnable: ctx.GlobalBool(flags.MetricsServerEnableFlag.Name),
MetricsHostname: ctx.GlobalString(flags.MetricsHostnameFlag.Name),
MetricsPort: ctx.GlobalUint64(flags.MetricsPortFlag.Name),
DisableHTTP2: ctx.GlobalBool(flags.HTTP2DisableFlag.Name),
}, nil
}
This diff is collapsed.
This diff is collapsed.
go 1.18
use (
./bss-core
./teleportr
)
This diff is collapsed.
package disburser
// FilterStartBlockNumberParams holds the arguments passed to
// FindFilterStartBlockNumber.
type FilterStartBlockNumberParams struct {
// BlockNumber the current block height of the chain.
BlockNumber uint64
// NumConfirmations is the number of confirmations required to consider a
// block final.
NumConfirmations uint64
// DeployBlockNumber is the deployment height of the Deposit contract.
DeployBlockNumber uint64
// LastProcessedBlockNumber is the height of the last processed block.
//
// NOTE: This will be nil on the first invocation, before blocks have been
// ingested.
LastProcessedBlockNumber *uint64
}
func (p *FilterStartBlockNumberParams) unconfirmed(blockNumber uint64) bool {
return p.BlockNumber+1 < blockNumber+p.NumConfirmations
}
// FindFilterStartBlockNumber returns the block height from which to begin
// filtering logs based on the relative heights of the chain, the contract
// deployment, and the last block that was processed.
func FindFilterStartBlockNumber(params FilterStartBlockNumberParams) uint64 {
// On initilization, always start at the deploy height.
if params.LastProcessedBlockNumber == nil {
return params.DeployBlockNumber
}
// If the deployment height has not exited the confirmation window, we can
// still begin our search from the deployment height.
if params.unconfirmed(params.DeployBlockNumber) {
return params.DeployBlockNumber
}
// Otherwise, start from the block immediately following the last processed
// block. If that height is still hasn't fully confirmed, we'll use the
// height of the last confirmed block.
var filterStartBlockNumber = *params.LastProcessedBlockNumber + 1
if params.unconfirmed(filterStartBlockNumber) {
filterStartBlockNumber = params.BlockNumber + 1 - params.NumConfirmations
}
return filterStartBlockNumber
}
package disburser_test
import (
"testing"
"github.com/ethereum-optimism/optimism/teleportr/drivers/disburser"
"github.com/stretchr/testify/require"
)
func uint64Ptr(x uint64) *uint64 {
return &x
}
type filterStartBlockNumberTestCase struct {
name string
params disburser.FilterStartBlockNumberParams
expStartBlockNumber uint64
}
// TestFindFilterStartBlockNumber exhaustively tests the behavior of
// FindFilterStartBlockNumber and its edge cases.
func TestFindFilterStartBlockNumber(t *testing.T) {
tests := []filterStartBlockNumberTestCase{
// Deploy number should be returned if LastProcessedBlockNumber is nil.
{
name: "init returns deploy block number",
params: disburser.FilterStartBlockNumberParams{
BlockNumber: 10,
NumConfirmations: 5,
DeployBlockNumber: 42,
LastProcessedBlockNumber: nil,
},
expStartBlockNumber: 42,
},
// Deploy number should be returned if the deploy number is still in our
// confirmation window.
{
name: "conf lookback before deploy number",
params: disburser.FilterStartBlockNumberParams{
BlockNumber: 43,
NumConfirmations: 5,
DeployBlockNumber: 42,
LastProcessedBlockNumber: uint64Ptr(43),
},
expStartBlockNumber: 42,
},
// Deploy number should be returned if the deploy number is still in our
// confirmation window.
{
name: "conf lookback before deploy number",
params: disburser.FilterStartBlockNumberParams{
BlockNumber: 43,
NumConfirmations: 44,
DeployBlockNumber: 42,
LastProcessedBlockNumber: uint64Ptr(43),
},
expStartBlockNumber: 42,
},
// If our confirmation window is ahead of the last deposit + 1, expect
// last deposit + 1.
{
name: "conf lookback gt last deposit plus one",
params: disburser.FilterStartBlockNumberParams{
BlockNumber: 100,
NumConfirmations: 5,
DeployBlockNumber: 42,
LastProcessedBlockNumber: uint64Ptr(43),
},
expStartBlockNumber: 44,
},
// If our confirmation window is equal to last deposit + 1, expect last
// deposit + 1.
{
name: "conf lookback eq last deposit plus one",
params: disburser.FilterStartBlockNumberParams{
BlockNumber: 48,
NumConfirmations: 5,
DeployBlockNumber: 42,
LastProcessedBlockNumber: uint64Ptr(43),
},
expStartBlockNumber: 44,
},
// If our confirmation window starts before last deposit + 1, expect
// block number - num confs + 1.
{
name: "conf lookback lt last deposit plus one",
params: disburser.FilterStartBlockNumberParams{
BlockNumber: 47,
NumConfirmations: 5,
DeployBlockNumber: 42,
LastProcessedBlockNumber: uint64Ptr(43),
},
expStartBlockNumber: 43,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testFindFilterStartBlockNumber(t, test)
})
}
}
func testFindFilterStartBlockNumber(
t *testing.T,
test filterStartBlockNumberTestCase,
) {
startBlockNumber := disburser.FindFilterStartBlockNumber(test.params)
require.Equal(t, test.expStartBlockNumber, startBlockNumber)
}
This diff is collapsed.
package disburser
import (
"context"
"errors"
"regexp"
"time"
"github.com/ethereum/go-ethereum/rpc"
"github.com/cenkalti/backoff"
)
var retryRegexes = []*regexp.Regexp{
regexp.MustCompile("read: connection reset by peer$"),
}
var DefaultBackoff = &backoff.ExponentialBackOff{
InitialInterval: backoff.DefaultInitialInterval,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 10 * time.Second,
MaxElapsedTime: time.Minute,
Clock: backoff.SystemClock,
}
func IsRetryableError(err error) bool {
msg := err.Error()
if httpErr, ok := err.(rpc.HTTPError); ok {
if httpErr.StatusCode == 503 || httpErr.StatusCode == 524 || httpErr.StatusCode == 429 {
return true
}
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
for _, reg := range retryRegexes {
if reg.MatchString(msg) {
return true
}
}
return false
}
package disburser
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)
func TestIsRetryableError(t *testing.T) {
var resCode int32
var res atomic.Value
res.Store([]byte{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(int(atomic.LoadInt32(&resCode)))
_, _ = w.Write(res.Load().([]byte))
}))
defer server.Close()
client, err := ethclient.Dial(server.URL)
require.NoError(t, err)
tests := []struct {
code int
retryable bool
}{
{
503,
true,
},
{
524,
true,
},
{
429,
true,
},
{
500,
false,
},
{
200,
false,
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("http %d", tt.code), func(t *testing.T) {
atomic.StoreInt32(&resCode, int32(tt.code))
_, err := client.BlockNumber(context.Background())
require.Equal(t, tt.retryable, IsRetryableError(err))
})
}
require.True(t, IsRetryableError(context.DeadlineExceeded))
require.True(t, IsRetryableError(errors.New("read: connection reset by peer")))
}
This diff is collapsed.
This diff is collapsed.
package flags
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
)
// TestRequiredFlagsSetRequired asserts that all flags deemed required properly
// have the Required field set to true.
func TestRequiredFlagsSetRequired(t *testing.T) {
for _, flag := range requiredFlags {
reqFlag, ok := flag.(cli.RequiredFlag)
require.True(t, ok)
require.True(t, reqFlag.IsRequired())
}
}
// TestOptionalFlagsDontSetRequired asserts that all flags deemed optional set
// the Required field to false.
func TestOptionalFlagsDontSetRequired(t *testing.T) {
for _, flag := range optionalFlags {
reqFlag, ok := flag.(cli.RequiredFlag)
require.True(t, ok)
require.False(t, reqFlag.IsRequired())
}
}
This diff is collapsed.
This diff is collapsed.
{
"name": "@eth-optimism/teleportr",
"version": "0.0.11",
"private": true,
"devDependencies": {}
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment