diff --git a/src/express-lane-auction/Burner.sol b/src/express-lane-auction/Burner.sol new file mode 100644 index 00000000..e3fe3fb8 --- /dev/null +++ b/src/express-lane-auction/Burner.sol @@ -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))); + } +} diff --git a/src/express-lane-auction/Errors.sol b/src/express-lane-auction/Errors.sol index c7066c1f..fb619acd 100644 --- a/src/express-lane-auction/Errors.sol +++ b/src/express-lane-auction/Errors.sol @@ -28,3 +28,4 @@ error RoundTooLong(uint64 roundDurationSeconds); error ZeroAuctionClosingSeconds(); error NegativeOffset(); error NegativeRoundStart(int64 roundStart); +error ZeroAddress(); diff --git a/test/foundry/ExpressLaneAuction.t.sol b/test/foundry/ExpressLaneAuction.t.sol index c8f0b168..f173faa0 100644 --- a/test/foundry/ExpressLaneAuction.t.sol +++ b/test/foundry/ExpressLaneAuction.t.sol @@ -3,13 +3,18 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "../../src/express-lane-auction/ExpressLaneAuction.sol"; -import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { + ERC20Burnable, + IERC20 +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "../../src/express-lane-auction/Burner.sol"; -contract MockERC20 is ERC20 { +contract MockERC20 is ERC20Burnable { constructor() ERC20("LANE", "LNE") { _mint(msg.sender, 1_000_000); } @@ -970,6 +975,86 @@ contract ExpressLaneAuctionTest is Test { rs.auction.flushBeneficiaryBalance(); } + function testFlushToBurner() public { + vm.chainId(137); + Bid memory bid0; + Bid memory bid1; + ExpressLaneAuction auction; + MockERC20 erc20 = new MockERC20(); + Burner burner = new Burner(address(erc20)); + { + ProxyAdmin proxyAdmin = new ProxyAdmin(); + ExpressLaneAuction impl = new ExpressLaneAuction(); + + auction = ExpressLaneAuction( + address(new TransparentUpgradeableProxy(address(impl), address(proxyAdmin), "")) + ); + InitArgs memory args = createArgs(address(erc20)); + args._beneficiary = address(burner); + auction.initialize(args); + + erc20.transfer(bidders[0].addr, bidders[0].amount); + erc20.transfer(bidders[1].addr, bidders[1].amount); + + vm.startPrank(bidders[0].addr); + erc20.approve(address(auction), bidders[0].amount); + auction.deposit(bidders[0].amount); + vm.stopPrank(); + + vm.startPrank(bidders[1].addr); + erc20.approve(address(auction), bidders[1].amount); + auction.deposit(bidders[1].amount); + vm.stopPrank(); + + (int64 o, uint64 roundDurationSeconds, uint64 auctionClosingSeconds, ) = auction + .roundTimingInfo(); + vm.warp( + uint64(o) + + (roundDurationSeconds * testRound) + + roundDurationSeconds - + auctionClosingSeconds + ); + uint64 biddingForRound = auction.currentRound() + 1; + + bytes32 h0 = auction.getBidHash(biddingForRound, bidders[0].elc, bidders[0].amount / 2); + bid0 = Bid({ + amount: bidders[0].amount / 2, + expressLaneController: bidders[0].elc, + signature: sign(bidders[0].privKey, h0) + }); + bytes32 h1 = auction.getBidHash(biddingForRound, bidders[1].elc, bidders[1].amount / 2); + bid1 = Bid({ + amount: bidders[1].amount / 2, + expressLaneController: bidders[1].elc, + signature: sign(bidders[1].privKey, h1) + }); + } + + vm.prank(auctioneer); + auction.resolveMultiBidAuction(bid1, bid0); + + assertFalse(auction.beneficiaryBalance() == 0, "bal before"); + uint256 auctionBalanceBefore = erc20.balanceOf(address(auction)); + uint256 beneficiaryBalanceBefore = erc20.balanceOf(auction.beneficiary()); + assertEq(erc20.balanceOf(address(burner)), 0); + + // any random address should be able to call this + vm.prank(vm.addr(34567890)); + auction.flushBeneficiaryBalance(); + + uint256 auctionBalanceAfter = erc20.balanceOf(address(auction)); + uint256 beneficiaryBalanceAfter = erc20.balanceOf(auction.beneficiary()); + assertTrue(auction.beneficiaryBalance() == 0, "bal after"); + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, bid0.amount); + assertEq(auctionBalanceBefore - auctionBalanceAfter, bid0.amount); + assertEq(erc20.balanceOf(address(burner)), bid0.amount); + + vm.prank(vm.addr(948765)); + burner.burn(); + + assertEq(erc20.balanceOf(address(burner)), 0); + } + function testCannotResolveNotAuctioneer() public { ResolveSetup memory rs = deployDepositAndBids(); vm.stopPrank(); diff --git a/test/foundry/ExpressLaneBurner.t.sol b/test/foundry/ExpressLaneBurner.t.sol new file mode 100644 index 00000000..e6bb2221 --- /dev/null +++ b/test/foundry/ExpressLaneBurner.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import { + ERC20BurnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import {Burner} from "../../src/express-lane-auction/Burner.sol"; +import "../../src/express-lane-auction/Errors.sol"; + +contract MockERC20 is ERC20BurnableUpgradeable { + function initialize() public initializer { + __ERC20_init("LANE", "LNE"); + _mint(msg.sender, 1_000_000); + } +} + +contract ExpressLaneBurner is Test { + event Transfer(address indexed from, address indexed to, uint256 value); + + function testBurn() public { + vm.expectRevert(ZeroAddress.selector); + new Burner(address(0)); + + MockERC20 erc20 = new MockERC20(); + erc20.initialize(); + Burner burner = new Burner(address(erc20)); + assertEq(address(burner.token()), address(erc20)); + + erc20.transfer(address(burner), 20); + + uint256 totalSupplyBefore = erc20.totalSupply(); + assertEq(erc20.balanceOf(address(burner)), 20); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(burner), address(0), 20); + vm.prank(vm.addr(137)); + burner.burn(); + + assertEq(totalSupplyBefore - erc20.totalSupply(), 20); + assertEq(erc20.balanceOf(address(burner)), 0); + + // can burn 0 if we want to + vm.expectEmit(true, true, true, true); + emit Transfer(address(burner), address(0), 0); + vm.prank(vm.addr(138)); + burner.burn(); + } +}