Skip to content

Commit

Permalink
Merge pull request #214 from OffchainLabs/express-lane-auction
Browse files Browse the repository at this point in the history
Express lane auction contracts
  • Loading branch information
gzeoneth authored Nov 25, 2024
2 parents b140ed6 + 625ad64 commit 1de32cb
Show file tree
Hide file tree
Showing 16 changed files with 4,568 additions and 3 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ deployments/
scripts/config.ts
forge-cache/
out/
.env
.env
lcov.info
output_directory
41 changes: 41 additions & 0 deletions deploy/ExpressLaneAuction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module.exports = async hre => {
const { deployments, getNamedAccounts } = hre
const { deploy } = deployments
const { deployer } = await getNamedAccounts()

await deploy('ExpressLaneAuction', {
from: deployer,
args: [],
proxy: {
proxyContract: 'TransparentUpgradeableProxy',
execute: {
init: {
methodName: 'initialize',
args: [{
_auctioneer: "0xeee584DA928A94950E177235EcB9A99bb655c7A0",
_biddingToken: "0x980B62Da83eFf3D4576C647993b0c1D7faf17c73", // WETH
_beneficiary: "0xeee584DA928A94950E177235EcB9A99bb655c7A0",
_roundTimingInfo: {
offsetTimestamp: 1727870000,
roundDurationSeconds: 60,
auctionClosingSeconds: 15,
reserveSubmissionSeconds: 15
},
_minReservePrice: ethers.utils.parseEther("0.00001"),
_auctioneerAdmin: "0xeee584DA928A94950E177235EcB9A99bb655c7A0",
_minReservePriceSetter: "0xeee584DA928A94950E177235EcB9A99bb655c7A0",
_reservePriceSetter: "0xeee584DA928A94950E177235EcB9A99bb655c7A0",
_reservePriceSetterAdmin: "0xeee584DA928A94950E177235EcB9A99bb655c7A0",
_beneficiarySetter: "0xeee584DA928A94950E177235EcB9A99bb655c7A0",
_roundTimingSetter: "0xeee584DA928A94950E177235EcB9A99bb655c7A0",
_masterAdmin: "0xeee584DA928A94950E177235EcB9A99bb655c7A0"
}],
},
},
owner: deployer,
},
})
}

module.exports.tags = ['ExpressLaneAuction']
module.exports.dependencies = []
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@arbitrum/nitro-contracts",
"version": "2.1.0",
"version": "2.1.1",
"description": "Layer 2 precompiles and rollup for Arbitrum Nitro",
"author": "Offchain Labs, Inc.",
"license": "BUSL-1.1",
Expand Down
2 changes: 1 addition & 1 deletion slither.db.json

Large diffs are not rendered by default.

142 changes: 142 additions & 0 deletions src/express-lane-auction/Balance.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import "./Errors.sol";

/// @notice Account balance and the round at which it can be withdrawn
/// Balances are withdrawn as part of a two step process - initiation and finalization
/// This is so that a bidder can't withdraw their balance after making a bid
/// Instead, if they initiate their withdrawal in round r, they must wait until the beginning of
/// round r+2 before they can withdraw the balance. In the mean time their balance can be used to
/// resolve an auction if it is part of a valid bid, however the auctioneer may choose to
/// reject bids from accounts with an initiated balance withdrawal
struct Balance {
/// @notice The amount of balance in the account
uint256 balance;
/// @notice The round at which all of the balance can be withdrawn
/// Is set to uint64.max when no withdrawal has been intiated
uint64 withdrawalRound;
}

/// @notice Balance mutation and view functionality. This is in it's own library so that we can
// reason about and test how the different ways balance mutations interact with each other
library BalanceLib {
/// @notice The available balance at the supplied round. Returns 0 if a withdrawal has been initiated and has
/// past the withdrawal round.
/// @param bal The balance to query
/// @param round The round to check the balance in
function balanceAtRound(Balance storage bal, uint64 round) internal view returns (uint256) {
return bal.balance - withdrawableBalanceAtRound(bal, round);
}

/// @notice The withdrawable balance at the supplied round. If a withdrawal has been initiated, the
/// supplied round is past the withdrawal round and has yet to be finalized, then the balance
/// of this account is returned. Otherwise 0.
/// @param bal The balance to query
/// @param round The round to check the withdrawable balance in
function withdrawableBalanceAtRound(Balance storage bal, uint64 round)
internal
view
returns (uint256)
{
return round >= bal.withdrawalRound ? bal.balance : 0;
}

/// @notice Increase a balance by a specified amount
/// Will cancel a withdrawal if called after a withdrawal has been initiated
/// @param bal The balance info
/// @param amount The amount to increase the balance by
function increase(Balance storage bal, uint256 amount) internal {
// no point increasing if no amount is being supplied
if (amount == 0) {
revert ZeroAmount();
}

// if the balance have never been used before then balance and withdrawal round will be 0
// in this case we initialize the balance by setting the withdrawal round into the future
// if a withdrawal for the balance has been initialized (withdrawal round != 0 and != max)
// then we cancel that initiated withdrawal. We do this since if a increase is being made that
// means a user wishes to increase their balance, not withdraw it.
if (bal.withdrawalRound != type(uint64).max) {
bal.withdrawalRound = type(uint64).max;
}

bal.balance += amount;
}

/// @notice Reduce a balance immediately. The balance must already be greater than the amount
/// and a withdrawal has been initiated for this balance it must be occuring in
/// a round after the supplied round. Withdrawals earlier than that will block this reduce.
/// @param bal The balance to reduce
/// @param amount The amount to reduce by
/// @param round The round to check withdrawals against. A withdrawal after this round will be ignored
/// and the balance reduced anyway, withdrawals before or on this round will be respected
/// and the reduce will revert
function reduce(
Balance storage bal,
uint256 amount,
uint64 round
) internal {
uint256 balRnd = balanceAtRound(bal, round);
// we add a zero check since it's possible for the amount to be zero
// but even in that case the user must have some balance
// to enforce that parties that havent done the deposit step cannot take part in the auction
if (balRnd == 0 || balRnd < amount) {
revert InsufficientBalance(amount, balRnd);
}

bal.balance -= amount;
}

/// @notice Initiate a withdrawal. A withdrawal is a reduction of the full amount.
/// Withdrawal is a two step process initialization and finalization. Finalization is only
/// after the supplied round parameter. The expected usage is to specify a round that is 2
/// in the future to ensure that the balance cannot be reduced unexpectedly without notice.
// An external observer can see that a withdrawal has been initiated, and will therefore
/// be able to take it into account and not rely on the balance being there.
/// In the case of the auction contract this allows the bidders to withdraw their
/// balance, but an auctioneer will know not to accept there bids in the mean time
/// @param bal The balance to iniate a reduction on
/// @param round The round that the withdrawal will be available in. Cannot be specified as the max round
function initiateWithdrawal(Balance storage bal, uint64 round) internal {
if (bal.balance == 0) {
revert ZeroAmount();
}

if (round == type(uint64).max) {
// we use max round to specify that a withdrawal is not taking place
// so we dont allow it as a withdrawal round. In practice max round should never
// be reached so this isnt an issue, we just put this here as an additional
// safety check
revert WithdrawalMaxRound();
}

if (bal.withdrawalRound != type(uint64).max) {
revert WithdrawalInProgress();
}

bal.withdrawalRound = round;
}

/// @notice Finalize an already initialized withdrawal. Reduces the balance to 0.
/// Can only be called two round after the withdrawal was initiated.
/// @param bal The balance to finalize
/// @param round The round to check whether withdrawal is valid in. Usually the current round. Cannot be max round.
function finalizeWithdrawal(Balance storage bal, uint64 round) internal returns (uint256) {
if (round == type(uint64).max) {
// we use max round to specify that a withdrawal is not taking place
// so we dont allow it as a withdrawal round. In practice max round should never
// be reached so this isnt an issue, we just put this here as an additional
// safety check
revert WithdrawalMaxRound();
}

uint256 withdrawableBalance = withdrawableBalanceAtRound(bal, round);
if (withdrawableBalance == 0) {
revert NothingToWithdraw();
}

bal.balance = 0;
return withdrawableBalance;
}
}
25 changes: 25 additions & 0 deletions src/express-lane-auction/Burner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {
ERC20BurnableUpgradeable
} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";
import "./Errors.sol";

/// @notice A simple contract that can burn any tokens that are transferred to it
/// Token must support the ERC20BurnableUpgradeable.burn(uint256) interface
contract Burner {
ERC20BurnableUpgradeable public immutable token;

constructor(address _token) {
if (_token == address(0)) {
revert ZeroAddress();
}
token = ERC20BurnableUpgradeable(_token);
}

/// @notice Can be called at any time by anyone to burn any tokens held by this burner
function burn() external {
token.burn(token.balanceOf(address(this)));
}
}
73 changes: 73 additions & 0 deletions src/express-lane-auction/ELCRound.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import "./Errors.sol";

/// @notice When an auction round is resolved a new express lane controller is chosen for that round
/// An elc round stores that selected express lane controller against the round number
struct ELCRound {
/// @notice The express lane controller for this round
address expressLaneController;
/// @notice The round number
uint64 round;
}

/// @notice Latest resolved express lane controller auction rounds
// Only the two latest resolved rounds are stored
library LatestELCRoundsLib {
/// @notice The last resolved express lane controller round, and its index in the array
/// @param rounds The stored resolved rounds
/// @return The last resolved elc round
/// @return The index of that last resolved round within the supplied array
function latestELCRound(ELCRound[2] storage rounds)
internal
view
returns (ELCRound storage, uint8)
{
ELCRound storage latestRound = rounds[0];
uint8 index = 0;
if (latestRound.round < rounds[1].round) {
latestRound = rounds[1];
index = 1;
}
return (latestRound, index);
}

/// @notice Finds the elc round that matches the supplied round. Reverts if no matching round found.
/// @param latestResolvedRounds The resolved elc rounds
/// @param round The round number to find a resolved round for
function resolvedRound(ELCRound[2] storage latestResolvedRounds, uint64 round)
internal
view
returns (ELCRound storage)
{
if (latestResolvedRounds[0].round == round) {
return latestResolvedRounds[0];
} else if (latestResolvedRounds[1].round == round) {
return latestResolvedRounds[1];
} else {
// not resolved or too old
revert RoundNotResolved(round);
}
}

/// @notice Set a resolved round into the array, overwriting the oldest resolved round
/// in the array.
/// @param latestResolvedRounds The resolved rounds aray
/// @param round The round to resolve
/// @param expressLaneController The new express lane controller for that round
function setResolvedRound(
ELCRound[2] storage latestResolvedRounds,
uint64 round,
address expressLaneController
) internal {
(ELCRound storage lastRoundResolved, uint8 index) = latestELCRound(latestResolvedRounds);
if (lastRoundResolved.round >= round) {
revert RoundAlreadyResolved(round);
}

// dont replace the newest round, use the oldest slot
uint8 oldestRoundIndex = index ^ 1;
latestResolvedRounds[oldestRoundIndex] = ELCRound(expressLaneController, round);
}
}
31 changes: 31 additions & 0 deletions src/express-lane-auction/Errors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

error InsufficientBalance(uint256 amountRequested, uint256 balance);
error InsufficientBalanceAcc(address account, uint256 amountRequested, uint256 balance);
error RoundDurationTooShort();
error NothingToWithdraw();
error ZeroAmount();
error ZeroBiddingToken();
error WithdrawalInProgress();
error WithdrawalMaxRound();
error RoundAlreadyResolved(uint64 round);
error SameBidder();
error BidsWrongOrder();
error TieBidsWrongOrder();
error AuctionNotClosed();
error ReservePriceTooLow(uint256 reservePrice, uint256 minReservePrice);
error ReservePriceNotMet(uint256 bidAmount, uint256 reservePrice);
error ReserveBlackout();
error RoundTooOld(uint64 round, uint64 currentRound);
error RoundNotResolved(uint64 round);
error NotExpressLaneController(uint64 round, address controller, address sender);
error FixedTransferor(uint64 fixedUntilRound);
error NotTransferor(uint64 round, address expectedTransferor, address msgSender);
error InvalidNewRound(uint64 currentRound, uint64 newRound);
error InvalidNewStart(uint64 currentStart, uint64 newStart);
error RoundTooLong(uint64 roundDurationSeconds);
error ZeroAuctionClosingSeconds();
error NegativeOffset();
error NegativeRoundStart(int64 roundStart);
error ZeroAddress();
Loading

0 comments on commit 1de32cb

Please sign in to comment.