Commit 509348f9 authored by Conner Fromknecht's avatar Conner Fromknecht

feat: add L2 TeleportrDisburser contract

parent f6399795
// 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-inside-a-loop
//slither-disable-next-line reentrancy-vulnerabilities-3
(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.9;
/**
* @title FailingReceiver
*/
contract FailingReceiver {
/**
* @notice Receiver that always reverts upon receiving ether.
*/
receive() external payable {
require(false, "FailingReceiver");
}
}
# FailingReceiver
> FailingReceiver
# TeleportrDisburser
> TeleportrDisburser
## Methods
### disburse
```solidity
function disburse(uint256 _nextDepositId, TeleportrDisburser.Disbursement[] _disbursements) external payable
```
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.
#### Parameters
| Name | Type | Description |
|---|---|---|
| _nextDepositId | uint256 | The depositId of the first Dispursement.
| _disbursements | TeleportrDisburser.Disbursement[] | A list of Disbursements to process.
### owner
```solidity
function owner() external view returns (address)
```
*Returns the address of the current owner.*
#### Returns
| Name | Type | Description |
|---|---|---|
| _0 | address | undefined
### renounceOwnership
```solidity
function renounceOwnership() external nonpayable
```
*Leaves the contract without owner. It will not be possible to call `onlyOwner` functions anymore. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby removing any functionality that is only available to the owner.*
### totalDisbursements
```solidity
function totalDisbursements() external view returns (uint256)
```
The total number of disbursements processed.
#### Returns
| Name | Type | Description |
|---|---|---|
| _0 | uint256 | undefined
### transferOwnership
```solidity
function transferOwnership(address newOwner) external nonpayable
```
*Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.*
#### Parameters
| Name | Type | Description |
|---|---|---|
| newOwner | address | undefined
### withdrawBalance
```solidity
function withdrawBalance() external nonpayable
```
Sends the contract&#39;s current balance to the owner.
## Events
### BalanceWithdrawn
```solidity
event BalanceWithdrawn(address indexed owner, uint256 balance)
```
Emitted any time the balance is withdrawn by the owner.
#### Parameters
| Name | Type | Description |
|---|---|---|
| owner `indexed` | address | The current owner and recipient of the funds. |
| balance | uint256 | The current contract balance paid to the owner. |
### DisbursementFailed
```solidity
event DisbursementFailed(uint256 indexed depositId, address indexed to, uint256 amount)
```
Emitted any time a disbursement fails to send.
#### Parameters
| Name | Type | Description |
|---|---|---|
| depositId `indexed` | uint256 | The unique sequence number identifying the deposit. |
| to `indexed` | address | The intended recipient of the disbursement. |
| amount | uint256 | The amount intended to be sent to the recipient. |
### DisbursementSuccess
```solidity
event DisbursementSuccess(uint256 indexed depositId, address indexed to, uint256 amount)
```
Emitted any time a disbursement is successfuly sent.
#### Parameters
| Name | Type | Description |
|---|---|---|
| depositId `indexed` | uint256 | The unique sequence number identifying the deposit. |
| to `indexed` | address | The recipient of the disbursement. |
| amount | uint256 | The amount sent to the recipient. |
### OwnershipTransferred
```solidity
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
```
#### Parameters
| Name | Type | Description |
|---|---|---|
| previousOwner `indexed` | address | undefined |
| newOwner `indexed` | address | undefined |
/* External Imports */
import { ethers } from 'hardhat'
import { Signer, Contract, BigNumber } from 'ethers'
/* Internal Imports */
import { expect } from '../../../setup'
describe.only('TeleportrDisburser', async () => {
const zeroETH = ethers.utils.parseEther('0.0')
const oneETH = ethers.utils.parseEther('1.0')
const twoETH = ethers.utils.parseEther('2.0')
let teleportrDisburser: Contract
let failingReceiver: Contract
let signer: Signer
let signer2: Signer
let contractAddress: string
let failingReceiverAddress: string
let signerAddress: string
let signer2Address: string
before(async () => {
;[signer, signer2] = await ethers.getSigners()
teleportrDisburser = await (
await ethers.getContractFactory('TeleportrDisburser')
).deploy()
failingReceiver = await (
await ethers.getContractFactory('FailingReceiver')
).deploy()
contractAddress = teleportrDisburser.address
failingReceiverAddress = failingReceiver.address
signerAddress = await signer.getAddress()
signer2Address = await signer2.getAddress()
})
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, signer2Address]], {
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, signer2Address]], {
value: zeroETH,
})
).to.be.revertedWith('Disbursement total != amount sent')
})
})
describe('disburse single success', async () => {
let signerInitialBalance: BigNumber
let signer2InitialBalance: BigNumber
it('should emit DisbursementSuccess for successful disbursement', async () => {
signerInitialBalance = await ethers.provider.getBalance(signerAddress)
signer2InitialBalance = await ethers.provider.getBalance(signer2Address)
await expect(
teleportrDisburser.disburse(0, [[oneETH, signer2Address]], {
value: oneETH,
})
)
.to.emit(teleportrDisburser, 'DisbursementSuccess')
.withArgs(BigNumber.from(0), signer2Address, oneETH)
})
it('should show one total disbursement', async () => {
await expect(await teleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(1)
)
})
it('should leave contract balance at zero ETH', async () => {
await expect(
await ethers.provider.getBalance(contractAddress)
).to.be.equal(zeroETH)
})
it('should increase recipients balance by disbursement amount', async () => {
await expect(
await ethers.provider.getBalance(signer2Address)
).to.be.equal(signer2InitialBalance.add(oneETH))
})
it('should decrease owners balance by disbursement amount - fees', async () => {
await expect(
await ethers.provider.getBalance(signerAddress)
).to.be.closeTo(signerInitialBalance.sub(oneETH), 10 ** 15)
})
})
describe('disburse single failure', async () => {
let signerInitialBalance: BigNumber
it('should emit DisbursementFailed for failed disbursement', async () => {
signerInitialBalance = await ethers.provider.getBalance(signerAddress)
await expect(
teleportrDisburser.disburse(1, [[oneETH, failingReceiverAddress]], {
value: oneETH,
})
)
.to.emit(teleportrDisburser, 'DisbursementFailed')
.withArgs(BigNumber.from(1), failingReceiverAddress, oneETH)
})
it('should show two total disbursements', async () => {
await expect(await teleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(2)
)
})
it('should leave contract with disbursement amount', async () => {
await expect(
await ethers.provider.getBalance(contractAddress)
).to.be.equal(oneETH)
})
it('should leave recipients balance at zero ETH', async () => {
await expect(
await ethers.provider.getBalance(failingReceiverAddress)
).to.be.equal(zeroETH)
})
it('should decrease owners balance by disbursement amount - fees', async () => {
await expect(
await ethers.provider.getBalance(signerAddress)
).to.be.closeTo(signerInitialBalance.sub(oneETH), 10 ** 15)
})
})
describe('withdrawBalance', async () => {
let initialContractBalance: BigNumber
let initialSignerBalance: BigNumber
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 () => {
initialContractBalance = await ethers.provider.getBalance(contractAddress)
initialSignerBalance = await ethers.provider.getBalance(signerAddress)
await expect(teleportrDisburser.withdrawBalance())
.to.emit(teleportrDisburser, 'BalanceWithdrawn')
.withArgs(signerAddress, oneETH)
})
it('should leave contract with zero balance', async () => {
await expect(await ethers.provider.getBalance(contractAddress)).to.equal(
zeroETH
)
})
it('should credit owner with contract balance - fees', async () => {
const expSignerBalance = initialSignerBalance.add(initialContractBalance)
await expect(
await ethers.provider.getBalance(signerAddress)
).to.be.closeTo(expSignerBalance, 10 ** 15)
})
})
describe('disburse multiple', async () => {
let signerInitialBalance: BigNumber
let signer2InitialBalance: BigNumber
it('should emit DisbursementSuccess for successful disbursement', async () => {
signerInitialBalance = await ethers.provider.getBalance(signerAddress)
signer2InitialBalance = await ethers.provider.getBalance(signer2Address)
await expect(
teleportrDisburser.disburse(
2,
[
[oneETH, signer2Address],
[oneETH, failingReceiverAddress],
],
{ value: twoETH }
)
).to.not.be.reverted
})
it('should show four total disbursements', async () => {
await expect(await teleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(4)
)
})
it('should leave contract balance with failed disbursement amount', async () => {
await expect(
await ethers.provider.getBalance(contractAddress)
).to.be.equal(oneETH)
})
it('should increase success recipients balance by disbursement amount', async () => {
await expect(
await ethers.provider.getBalance(signer2Address)
).to.be.equal(signer2InitialBalance.add(oneETH))
})
it('should leave failed recipients balance at zero ETH', async () => {
await expect(
await ethers.provider.getBalance(failingReceiverAddress)
).to.be.equal(zeroETH)
})
it('should decrease owners balance by disbursement 2*amount - fees', async () => {
await expect(
await ethers.provider.getBalance(signerAddress)
).to.be.closeTo(signerInitialBalance.sub(twoETH), 10 ** 15)
})
})
})
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