diff --git a/src/express-lane-auction/Errors.sol b/src/express-lane-auction/Errors.sol index 04262283..ef4c8bd8 100644 --- a/src/express-lane-auction/Errors.sol +++ b/src/express-lane-auction/Errors.sol @@ -20,6 +20,8 @@ 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); diff --git a/src/express-lane-auction/ExpressLaneAuction.sol b/src/express-lane-auction/ExpressLaneAuction.sol index 91e5b546..a0a5fe6b 100644 --- a/src/express-lane-auction/ExpressLaneAuction.sol +++ b/src/express-lane-auction/ExpressLaneAuction.sol @@ -9,15 +9,13 @@ import { AccessControlEnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import {DelegateCallAware} from "../libraries/DelegateCallAware.sol"; -import {IExpressLaneAuction, Bid, InitArgs} from "./IExpressLaneAuction.sol"; +import {IExpressLaneAuction, Bid, InitArgs, Transferor} from "./IExpressLaneAuction.sol"; import {ELCRound, LatestELCRoundsLib} from "./ELCRound.sol"; import {RoundTimingInfo, RoundTimingInfoLib} from "./RoundTimingInfo.sol"; import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; -// CHRIS: TODO: add ability to set the transferrer of controller rights - /// @title ExpressLaneAuction /// @notice The express lane allows a controller to submit undelayed transactions to the sequencer /// The right to be the express lane controller are auctioned off in rounds, by an offchain auctioneer. @@ -74,6 +72,9 @@ contract ExpressLaneAuction is /// @inheritdoc IExpressLaneAuction uint256 public beneficiaryBalance; + /// @inheritdoc IExpressLaneAuction + mapping(address => Transferor) public transferorOf; + /// @inheritdoc IExpressLaneAuction function initialize(InitArgs memory args) public initializer onlyDelegated { __AccessControl_init(); @@ -328,6 +329,7 @@ contract ExpressLaneAuction is biddingForRound, address(0), firstPriceBid.expressLaneController, + address(0), roundStart, roundEnd ); @@ -471,6 +473,22 @@ contract ExpressLaneAuction is ); } + /// @inheritdoc IExpressLaneAuction + function setTransferor(Transferor calldata transferor) external { + // if a transferor has already been set, it may be fixed until a future round + Transferor storage currentTransferor = transferorOf[msg.sender]; + if ( + currentTransferor.addr != address(0) && + currentTransferor.fixedUntilRound > roundTimingInfo.currentRound() + ) { + revert FixedTransferor(currentTransferor.fixedUntilRound); + } + + transferorOf[msg.sender] = transferor; + + emit SetTransferor(msg.sender, transferor.addr, transferor.fixedUntilRound); + } + /// @inheritdoc IExpressLaneAuction function transferExpressLaneController(uint64 round, address newExpressLaneController) external @@ -486,7 +504,14 @@ contract ExpressLaneAuction is ELCRound storage resolvedRound = latestResolvedRounds.resolvedRound(round); address resolvedELC = resolvedRound.expressLaneController; - if (resolvedELC != msg.sender) { + address transferor = transferorOf[resolvedELC].addr; + // can only be the transferor if one has been set + // otherwise we default to the express lane controller to do the transfer + if (transferor != address(0)) { + if (transferor != msg.sender) { + revert NotTransferor(round, transferor, msg.sender); + } + } else if (resolvedELC != msg.sender) { revert NotExpressLaneController(round, resolvedELC, msg.sender); } @@ -497,6 +522,7 @@ contract ExpressLaneAuction is round, resolvedELC, newExpressLaneController, + transferor != address(0) ? transferor : resolvedELC, start < uint64(block.timestamp) ? uint64(block.timestamp) : start, end ); diff --git a/src/express-lane-auction/IExpressLaneAuction.sol b/src/express-lane-auction/IExpressLaneAuction.sol index 914c0831..13ea0dc2 100644 --- a/src/express-lane-auction/IExpressLaneAuction.sol +++ b/src/express-lane-auction/IExpressLaneAuction.sol @@ -25,6 +25,18 @@ struct Bid { bytes signature; } +/// @notice Sets a transferor for an express lane controller +/// The transferor is an address that will have the right to transfer express lane controller rights +/// on behalf an express lane controller. +struct Transferor { + /// @notice The address of the transferor + address addr; + /// @notice The express lane controller can choose to fix the transferor until a future round number + /// This gives them ability to guarantee to other parties that they will not change transferor during an ongoing round + /// The express lane controller can ignore this feature by setting this value to 0. + uint64 fixedUntilRound; +} + /// @notice The arguments used to initialize an express lane auction struct InitArgs { /// @notice The address who can resolve auctions @@ -100,16 +112,28 @@ interface IExpressLaneAuction is IAccessControlEnumerableUpgradeable, IERC165Upg /// @param round The round which the express lane controller will control /// @param previousExpressLaneController The previous express lane controller /// @param newExpressLaneController The new express lane controller + /// @param transferor The address that transferored the controller rights. The transferor if set, otherwise the express lane controller /// @param startTimestamp The timestamp at which the new express lane controller takes over /// @param endTimestamp The timestamp at which the new express lane controller will cease to have control event SetExpressLaneController( uint64 round, - address previousExpressLaneController, - address newExpressLaneController, + address indexed previousExpressLaneController, + address indexed newExpressLaneController, + address indexed transferor, uint64 startTimestamp, uint64 endTimestamp ); + /// @notice A new transferor has been set for + /// @param expressLaneController The express lane controller that has a transferor + /// @param transferor The transferor chosen + /// @param fixedUntilRound The round until which this transferor is fixed for this controller + event SetTransferor( + address indexed expressLaneController, + address indexed transferor, + uint64 fixedUntilRound + ); + /// @notice The minimum reserve price was set /// @param oldPrice The previous minimum reserve price /// @param newPrice The new minimum reserve price @@ -181,6 +205,14 @@ interface IExpressLaneAuction is IAccessControlEnumerableUpgradeable, IERC165Upg /// This is a gas optimisation to avoid making a transfer every time an auction is resolved function beneficiaryBalance() external returns (uint256); + /// @notice Express lane controllers can optionally set a transferor address that has the rights + /// to transfer their controller rights. This function returns the transferor if one has been set + /// Returns the transferor for the supplied controller, and the round until which this + /// transferor is fixed if set. + function transferorOf(address expressLaneController) + external + returns (address addr, uint64 fixedUntil); + /// @notice Initialize the auction /// @param args Initialization parameters function initialize(InitArgs memory args) external; @@ -333,6 +365,12 @@ interface IExpressLaneAuction is IAccessControlEnumerableUpgradeable, IERC165Upg function resolveMultiBidAuction(Bid calldata firstPriceBid, Bid calldata secondPriceBid) external; + /// @notice Sets a transferor for an express lane controller + /// The transferor is an address that will have the right to transfer express lane controller rights + /// on behalf an express lane controller. + /// @param transferor The transferor to set + function setTransferor(Transferor calldata transferor) external; + /// @notice Express lane controllers are allowed to transfer their express lane rights for the current or future /// round to another address. They may use this for reselling their rights after purchasing them /// Again, the priviledged accounts mentioned in resolve documentation are trusted not to try to receive rights via this message. diff --git a/test/foundry/ExpressLaneAuction.t.sol b/test/foundry/ExpressLaneAuction.t.sol index 4b4486fc..c8f509a0 100644 --- a/test/foundry/ExpressLaneAuction.t.sol +++ b/test/foundry/ExpressLaneAuction.t.sol @@ -40,12 +40,18 @@ contract ExpressLaneAuctionTest is Test { event SetMinReservePrice(uint256 oldPrice, uint256 newPrice); event SetExpressLaneController( uint64 round, - address from, - address to, + address indexed from, + address indexed to, + address indexed transferor, uint64 startTimestamp, uint64 endTimestamp ); event SetBeneficiary(address oldBeneficiary, address newBeneficiary); + event SetTransferor( + address indexed expressLaneController, + address indexed transferor, + uint64 fixedUntilRound + ); event SetRoundTimingInfo( uint64 currentRound, uint64 offsetTimestamp, @@ -1086,6 +1092,7 @@ contract ExpressLaneAuctionTest is Test { biddingForRound, address(0), bidders[1].elc, + address(0), uint64(block.timestamp + auctionClosingSeconds), uint64(block.timestamp + auctionClosingSeconds + roundDurationSeconds - 1) ); @@ -1184,6 +1191,7 @@ contract ExpressLaneAuctionTest is Test { biddingForRound, address(0), bidders[3].elc, + address(0), uint64(block.timestamp + auctionClosingSeconds), uint64(block.timestamp + auctionClosingSeconds + roundDurationSeconds - 1) ); @@ -1328,6 +1336,7 @@ contract ExpressLaneAuctionTest is Test { biddingForRound, address(0), bidders[1].elc, + address(0), uint64(block.timestamp + auctionClosingSeconds), uint64(block.timestamp + auctionClosingSeconds + roundDurationSeconds - 1) ); @@ -1539,7 +1548,14 @@ contract ExpressLaneAuctionTest is Test { (uint64 start, uint64 end) = rs.auction.roundTimestamps(testRound + 1); vm.prank(bidders[1].elc); vm.expectEmit(true, true, true, true); - emit SetExpressLaneController(testRound + 1, bidders[1].elc, bidders[0].elc, start, end); + emit SetExpressLaneController( + testRound + 1, + bidders[1].elc, + bidders[0].elc, + bidders[1].elc, + start, + end + ); rs.auction.transferExpressLaneController(testRound + 1, bidders[0].elc); (, uint64 roundDurationSeconds, , ) = rs.auction.roundTimingInfo(); @@ -1558,6 +1574,7 @@ contract ExpressLaneAuctionTest is Test { testRound + 1, bidders[0].elc, bidders[1].elc, + bidders[0].elc, uint64(block.timestamp), end ); @@ -1587,6 +1604,7 @@ contract ExpressLaneAuctionTest is Test { testRound + 1, bidders[1].elc, bidders[0].elc, + bidders[1].elc, uint64(block.timestamp), end ); @@ -1608,8 +1626,90 @@ contract ExpressLaneAuctionTest is Test { end = end + roundDuration; vm.prank(bidders[3].elc); vm.expectEmit(true, true, true, true); - emit SetExpressLaneController(testRound + 2, bidders[3].elc, bidders[2].elc, start, end); + emit SetExpressLaneController( + testRound + 2, + bidders[3].elc, + bidders[2].elc, + bidders[3].elc, + start, + end + ); + rs.auction.transferExpressLaneController(testRound + 2, bidders[2].elc); + + // set a transferor and have them transfer + vm.prank(bidders[2].elc); + rs.auction.setTransferor(Transferor(bidders[2].addr, 1000)); + + vm.prank(bidders[3].elc); + vm.expectRevert( + abi.encodeWithSelector( + testRound + 2, + NotTransferor.selector, + bidders[2].addr, + bidders[3].elc + ) + ); rs.auction.transferExpressLaneController(testRound + 2, bidders[2].elc); + + // change next now + vm.prank(bidders[2].addr); + vm.expectEmit(true, true, true, true); + emit SetExpressLaneController( + testRound + 2, + bidders[2].elc, + bidders[3].elc, + bidders[2].addr, + start, + end + ); + rs.auction.transferExpressLaneController(testRound + 2, bidders[3].elc); + } + + function testSetTransferor() public { + (, IExpressLaneAuction auction) = deploy(); + + address elc = vm.addr(1559); + address transferor = vm.addr(1560); + address transferor2 = vm.addr(1561); + uint64 fixedUntilRound = 137; + address actualTransferor; + uint64 actualFixedUntil; + (actualTransferor, actualFixedUntil) = auction.transferorOf(elc); + assertEq(actualTransferor, address(0)); + assertEq(actualFixedUntil, 0); + + vm.prank(elc); + vm.expectEmit(true, true, true, true); + emit SetTransferor(elc, transferor, 0); + auction.setTransferor(Transferor({addr: transferor, fixedUntilRound: 0})); + (actualTransferor, actualFixedUntil) = auction.transferorOf(elc); + assertEq(actualTransferor, transferor); + assertEq(actualFixedUntil, 0); + + vm.prank(elc); + vm.expectEmit(true, true, true, true); + emit SetTransferor(elc, transferor2, fixedUntilRound); + auction.setTransferor(Transferor({addr: transferor2, fixedUntilRound: fixedUntilRound})); + (actualTransferor, actualFixedUntil) = auction.transferorOf(elc); + assertEq(actualTransferor, transferor2); + assertEq(actualFixedUntil, fixedUntilRound); + + vm.prank(elc); + vm.expectRevert(abi.encodeWithSelector(FixedTransferor.selector, fixedUntilRound)); + auction.setTransferor(Transferor({addr: transferor, fixedUntilRound: fixedUntilRound + 1})); + + while (auction.currentRound() < fixedUntilRound) { + vm.warp(block.timestamp + roundDuration); + } + + assertEq(auction.currentRound(), fixedUntilRound); + vm.prank(elc); + vm.expectEmit(true, true, true, true); + emit SetTransferor(elc, transferor, fixedUntilRound + 1); + auction.setTransferor(Transferor({addr: transferor, fixedUntilRound: fixedUntilRound + 1})); + (actualTransferor, actualFixedUntil) = auction.transferorOf(elc); + assertEq(actualTransferor, transferor); + assertEq(actualFixedUntil, fixedUntilRound + 1); } function testSetBeneficiary() public {