diff --git a/src/express-lane-auction/ExpressLaneAuction.sol b/src/express-lane-auction/ExpressLaneAuction.sol index 315fb7ed..2ede3b19 100644 --- a/src/express-lane-auction/ExpressLaneAuction.sol +++ b/src/express-lane-auction/ExpressLaneAuction.sol @@ -255,11 +255,31 @@ contract ExpressLaneAuction is return _balanceOf[account].balanceAtRound(roundTimingInfo.currentRound()); } + /// @inheritdoc IExpressLaneAuction + function balanceOfAtRound(address account, uint64 round) external view returns (uint256) { + if (round < roundTimingInfo.currentRound()) { + revert RoundTooOld(round, roundTimingInfo.currentRound()); + } + return _balanceOf[account].balanceAtRound(round); + } + /// @inheritdoc IExpressLaneAuction function withdrawableBalance(address account) external view returns (uint256) { return _balanceOf[account].withdrawableBalanceAtRound(roundTimingInfo.currentRound()); } + /// @inheritdoc IExpressLaneAuction + function withdrawableBalanceAtRound(address account, uint64 round) + external + view + returns (uint256) + { + if (round < roundTimingInfo.currentRound()) { + revert RoundTooOld(round, roundTimingInfo.currentRound()); + } + return _balanceOf[account].withdrawableBalanceAtRound(round); + } + /// @inheritdoc IExpressLaneAuction function deposit(uint256 amount) external { _balanceOf[msg.sender].increase(amount); diff --git a/src/express-lane-auction/IExpressLaneAuction.sol b/src/express-lane-auction/IExpressLaneAuction.sol index 2d7f25b8..f13ddfc9 100644 --- a/src/express-lane-auction/IExpressLaneAuction.sol +++ b/src/express-lane-auction/IExpressLaneAuction.sol @@ -312,11 +312,33 @@ interface IExpressLaneAuction is IAccessControlEnumerableUpgradeable, IERC165Upg /// @param account The specified account function balanceOf(address account) external view returns (uint256); + /// @notice Get what the balance will be at some future round + /// Since withdrawals are scheduled for future rounds it is possible to see that a balance + /// will reduce at some future round, this method provides a way to query that. + /// Specifically this will return 0 if the withdrawal round has been set, and is < the supplied round + /// Will revert if a round from the past is supplied + /// @param account The specified account + /// @param round The round to query the balance at + function balanceOfAtRound(address account, uint64 round) external view returns (uint256); + /// @notice The amount of balance that can currently be withdrawn via the finalize method /// This balance only increases current round + 2 after a withdrawal is initiated /// @param account The account the check the withdrawable balance for function withdrawableBalance(address account) external view returns (uint256); + /// @notice The amount of balance that can currently be withdrawn via the finalize method + /// Since withdrawals are scheduled for future rounds it is possible to see that a withdrawal balance + /// will increase at some future round, this method provides a way to query that. + /// Specifically this will return 0 unless the withdrawal round has been set, and is >= the supplied round + /// Will revert if a round from the past is supplied + /// This balance only increases current round + 2 after a withdrawal is initiated + /// @param account The account the check the withdrawable balance for + /// @param round The round to query the withdrawable balance at + function withdrawableBalanceAtRound(address account, uint64 round) + external + view + returns (uint256); + /// @notice Deposit an amount of ERC20 token to the auction to make bids with /// Deposits must be submitted prior to bidding. /// When withdrawing the full balance must be withdrawn. This is done via a two step process diff --git a/test/foundry/ExpressLaneAuction.t.sol b/test/foundry/ExpressLaneAuction.t.sol index 2cae70d6..c8f0b168 100644 --- a/test/foundry/ExpressLaneAuction.t.sol +++ b/test/foundry/ExpressLaneAuction.t.sol @@ -393,6 +393,254 @@ contract ExpressLaneAuctionTest is Test { vm.stopPrank(); } + function testBalanceOf() public { + (MockERC20 erc20, IExpressLaneAuction auction) = deploy(); + erc20.transfer(bidders[0].addr, bidders[0].amount); + + uint64 currentRound = auction.currentRound(); + vm.expectRevert( + abi.encodeWithSelector(RoundTooOld.selector, currentRound - 1, currentRound) + ); + auction.balanceOfAtRound(bidders[0].addr, currentRound - 1); + vm.expectRevert( + abi.encodeWithSelector(RoundTooOld.selector, currentRound - 1, currentRound) + ); + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound - 1); + + vm.prank(bidders[0].addr); + erc20.approve(address(auction), 20); + + vm.expectEmit(true, true, true, true); + emit Deposit(bidders[0].addr, 20); + vm.prank(bidders[0].addr); + auction.deposit(20); + assertEq(auction.balanceOf(bidders[0].addr), 20, "First balance"); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound), + 20, + "First balance at round" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 1), + 20, + "First balance at round + 1" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 2), + 20, + "First balance at round + 2" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 3), + 20, + "First balance at round + 3" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound), + 0, + "First withdrawable balance at round" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 1), + 0, + "First withdrawable balance at round + 1" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 2), + 0, + "First withdrawable balance at round + 2" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 3), + 0, + "First withdrawable balance at round + 3" + ); + assertEq( + erc20.balanceOf(bidders[0].addr), + bidders[0].amount - 20, + "First bidders[0].addr erc20 balance" + ); + assertEq(erc20.balanceOf(address(auction)), 20, "First auction erc20 balance"); + + // resolve the auction + vm.warp(block.timestamp + roundDuration - roundDuration / 4); + + bytes32 h0 = auction.getBidHash(currentRound + 1, bidders[0].elc, minReservePrice + 1); + Bid memory bid0 = Bid({ + amount: minReservePrice + 1, + expressLaneController: bidders[0].elc, + signature: sign(bidders[0].privKey, h0) + }); + + vm.prank(auctioneer); + auction.resolveSingleBidAuction(bid0); + + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound), + 20 - minReservePrice, + "Second balance at round" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 1), + 20 - minReservePrice, + "Second balance at round + 1" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 2), + 20 - minReservePrice, + "Second balance at round + 2" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 3), + 20 - minReservePrice, + "Second balance at round + 3" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound), + 0, + "Second withdrawable balance at round" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 1), + 0, + "Second withdrawable balance at round + 1" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 2), + 0, + "Second withdrawable balance at round + 2" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 3), + 0, + "Second withdrawable balance at round + 3" + ); + + vm.prank(bidders[0].addr); + auction.initiateWithdrawal(); + + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound), + 20 - minReservePrice, + "Third balance at round" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 1), + 20 - minReservePrice, + "Third balance at round + 1" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 2), + 0, + "Third balance at round + 2" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 3), + 0, + "Third balance at round + 3" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound), + 0, + "Third withdrawable balance at round" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 1), + 0, + "Third withdrawable balance at round + 1" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 2), + 20 - minReservePrice, + "Third withdrawable balance at round + 2" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 3), + 20 - minReservePrice, + "Third withdrawable balance at round + 3" + ); + + vm.warp(block.timestamp + roundDuration); + + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 1), + 20 - minReservePrice, + "Fourth balance at round + 1" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 2), + 0, + "Fourth balance at round + 2" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 3), + 0, + "Fourth balance at round + 3" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 1), + 0, + "Fourth withdrawable balance at round + 1" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 2), + 20 - minReservePrice, + "Fourth withdrawable balance at round + 2" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 3), + 20 - minReservePrice, + "Fourth withdrawable balance at round + 3" + ); + + vm.warp(block.timestamp + roundDuration); + + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 2), + 0, + "Fifth balance at round + 2" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 3), + 0, + "Fifth balance at round + 3" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 2), + 20 - minReservePrice, + "Fifth withdrawable balance at round + 2" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 3), + 20 - minReservePrice, + "Fifth withdrawable balance at round + 3" + ); + + vm.prank(bidders[0].addr); + auction.finalizeWithdrawal(); + + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 2), + 0, + "Sixth balance at round + 2" + ); + assertEq( + auction.balanceOfAtRound(bidders[0].addr, currentRound + 3), + 0, + "Sixth balance at round + 3" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 2), + 0, + "Sixth withdrawable balance at round + 2" + ); + assertEq( + auction.withdrawableBalanceAtRound(bidders[0].addr, currentRound + 3), + 0, + "Sixth withdrawable balance at round + 3" + ); + } + function testCurrentRound() public { (, IExpressLaneAuction auction) = deploy(); vm.warp(1); @@ -1743,7 +1991,6 @@ contract ExpressLaneAuctionTest is Test { vm.stopPrank(); address newBeneficiary = vm.addr(9090); - address newBeneficiary2 = vm.addr(9091); bytes memory revertString = abi.encodePacked( "AccessControl: account ", @@ -1812,16 +2059,15 @@ contract ExpressLaneAuctionTest is Test { }) ); - uint64 longDuration = 86401; (uint64 start, ) = auction.roundTimestamps(auction.currentRound() + 1); - int64 newOffset = int64(start - longDuration * 24); + int64 newOffset = int64(start - 86401 * 24); vm.prank(roundTimingSetter); - vm.expectRevert(abi.encodeWithSelector(RoundTooLong.selector, longDuration)); + vm.expectRevert(abi.encodeWithSelector(RoundTooLong.selector, 86401)); auction.setRoundTimingInfo( RoundTimingInfo({ offsetTimestamp: newOffset, - roundDurationSeconds: longDuration, + roundDurationSeconds: 86401, auctionClosingSeconds: 10, reserveSubmissionSeconds: 20 }) diff --git a/test/foundry/ExpressLaneBalance.t.sol b/test/foundry/ExpressLaneBalance.t.sol index 499418e4..0ac10288 100644 --- a/test/foundry/ExpressLaneBalance.t.sol +++ b/test/foundry/ExpressLaneBalance.t.sol @@ -122,6 +122,42 @@ contract ExpressLaneBalanceTest is Test { } } + function testInitiateWithdrawalBoundaries() public { + testInitiateWithdrawal(0, 0, 0); + testInitiateWithdrawal(0, 0, 10); + testInitiateWithdrawal(0, 0, type(uint64).max); + testInitiateWithdrawal(0, 10, 0); + testInitiateWithdrawal(0, 10, 5); + testInitiateWithdrawal(0, 10, 10); + testInitiateWithdrawal(0, 10, 15); + testInitiateWithdrawal(0, 10, type(uint64).max); + testInitiateWithdrawal(0, type(uint64).max, 0); + testInitiateWithdrawal(0, type(uint64).max, 10); + testInitiateWithdrawal(0, type(uint64).max, type(uint64).max); + testInitiateWithdrawal(10, 0, 0); + testInitiateWithdrawal(10, 0, 10); + testInitiateWithdrawal(10, 0, type(uint64).max); + testInitiateWithdrawal(10, 10, 0); + testInitiateWithdrawal(10, 10, 5); + testInitiateWithdrawal(10, 10, 10); + testInitiateWithdrawal(10, 10, 15); + testInitiateWithdrawal(10, 10, type(uint64).max); + testInitiateWithdrawal(10, type(uint64).max, 0); + testInitiateWithdrawal(10, type(uint64).max, 10); + testInitiateWithdrawal(10, type(uint64).max, type(uint64).max); + testInitiateWithdrawal(type(uint256).max, 0, 0); + testInitiateWithdrawal(type(uint256).max, 0, 10); + testInitiateWithdrawal(type(uint256).max, 0, type(uint64).max); + testInitiateWithdrawal(type(uint256).max, 10, 0); + testInitiateWithdrawal(type(uint256).max, 10, 5); + testInitiateWithdrawal(type(uint256).max, 10, 10); + testInitiateWithdrawal(type(uint256).max, 10, 15); + testInitiateWithdrawal(type(uint256).max, 10, type(uint64).max); + testInitiateWithdrawal(type(uint256).max, type(uint64).max, 0); + testInitiateWithdrawal(type(uint256).max, type(uint64).max, 10); + testInitiateWithdrawal(type(uint256).max, type(uint64).max, type(uint64).max); + } + function testInitiateWithdrawal( uint256 initialBalance, uint64 initialRound,