├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── docs ├── explainer │ ├── 1.png │ ├── 2.png │ └── 3.png ├── hero.png └── screenshots │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png ├── foundry.toml ├── script └── ERC20.s.sol ├── src ├── Auction.sol ├── ERC20X.sol └── Factory.sol └── test ├── Auction.t.sol └── Factory.t.sol /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | env: 9 | FOUNDRY_PROFILE: ci 10 | 11 | jobs: 12 | run-ci: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install Foundry 18 | uses: foundry-rs/foundry-toolchain@v1 19 | with: 20 | version: nightly 21 | 22 | - name: Install deps 23 | run: forge install 24 | 25 | - name: Check gas snapshots 26 | run: forge snapshot --check 27 | 28 | - name: Run tests 29 | run: forge test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | broadcast/ 2 | cache/ 3 | out/ 4 | 5 | .env 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/prb-math"] 5 | path = lib/prb-math 6 | url = https://github.com/paulrberg/prb-math 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hollander 2 | 3 | ![Heading](docs/hero.png) 4 | 5 | ## Introduction 6 | 7 | The repository contains smart contract code for Hollander. Hollander is a contract that allows anyone to create ducth auctions for a pair of ERC20 contracts. 8 | 9 | **Status**: the protocol code is complete, has full test coverage, and deployed on Goerli testnet; additionally, subgraph was created and deployed; the UI was created and deployed as well (see "Related repositories"). 10 | 11 | ## Problem 12 | 13 | It is frustrating to make large swap orders via DEXs. First, any large order creates significant price impact due to the lack of liquidity. Also, large orders are more vulnerable to sandwich attacks. Traders usually have to split the order into multiple small ones and set near 0 slippage tolerance (which increases the chances of tx being failed). Aggregators alleviate some of that, but they are inherently centralized and sometimes even pocket the positive slippage. 14 | 15 | ## Previous work 16 | 17 | There are some projects trying to solve similar or related problems: 18 | 19 | * Peg Stability Module (PSM): a contract to swap stablecoins at 1:1 rate. The major drawback is lack of free-floating price support 20 | * Prime Deals: a platform to execute OTC DAO to DAO deals at the predefined price. Unfortunetely, conducting swaps at the predefined price is not efficient (not equivalent to market price) and open for market manipulation. 21 | * MakerDao Liquidation module: a simple dutch auction implementation for collateral liquidations. The scope is limited to MakerDAO liquidations; also, partial buys are not possible. 22 | 23 | ## Solution 24 | 25 | Hollander solves that via dutch auctions. Any trader can create an custom auction tailored to their needs. The auction will require a single transaction instead of creating multiple smaller trades. It will guarantee near market price execution and lowest price impact possible via economical incentives (no token incentives needed). Order can be fullfilled all at once or partially, and is completely public for all traders. The possibility of sandwich attacks is also eliminated. 26 | 27 | *Also see "Visual explanation" section* 28 | 29 | ## MEV relevance 30 | 31 | Hollander utilizes MEV in two ways: 32 | 33 | 1. Mitigating bad MEV (sandwiching) via mechanism design 34 | 2. Taking advantadge of searcher network to do helpful work for the user (selling tokens at near market price) 35 | 36 | ## Use cases 37 | 38 | For the hackathon, I choose to focus on whale token swaps. There are, however, other potential uses of the system. 39 | 40 | First, it is possible to use Hollander for trustless . Currently, they are done either using predetermined price based on historical market rates (which is not efficient and open for price manipulation) or . Hollander can do treasure swaps both trustlessly and at market price. 41 | 42 | Second, we can use Hollander for protocol liquiditations. By selling the underwater collater via dutch auction, we are guaranteed to receive the best price while reducing reliance on centralized oracles. 43 | 44 | Finally, token buybacks (e.g. https://yearn.clinic) can also be conducted via Hollander, eliminating the dependency on oracles. 45 | 46 | ## Mechanism design 47 | 48 | The auction swaps token `A` to token `B`. The auction starts at the given price. As the time goes, the price will decrease. The decrease is exponential — the price will halve in `X` amount of time, then halve again in `X` amount of time, and so forth. Eventually, the current spot price will reach the market price, which will create an arbitrage opportunity. Anyone can sell any amount of tokens (within limit) to this auction, but the price will increase based on the amount sold. Small amount of tokens will increase the price slightly, but the large amount of token increases the price significantly. 49 | 50 | ### Parameters 51 | 52 | Each auction is initialized witha set of problems: 53 | 54 | - `tokenBase`: token to be sold via auction by creator 55 | - `tokenQuote`: token to be bought via auction 56 | - `amountBase`: amount of token to be sold 57 | - `initialPrice`: starting spot price 58 | - `halvingPeriod`: amount of blocks for price to halve 59 | - `swapPeriod`: target amount of blocks for auction to be held 60 | 61 | ## Related repositories 62 | 63 | - Smart contracts: https://github.com/Destiner/hollander-app 64 | - Subgraph: https://github.com/Destiner/hollander-subgraph 65 | 66 | ## Deployed contracts 67 | 68 | - Goerli: 0x26704df470f36A45592EcC07E9CAcC7aB795A094 ([etherscan](https://goerli.etherscan.io/address/0x26704df470f36A45592EcC07E9CAcC7aB795A094)) 69 | 70 | ## Further work 71 | 72 | There are many interesting ways for Hollander to improve 73 | 74 | 1. **UX**: offer sensible defaults when creating an auction, show historical price chart 75 | 2. **Broader usage**: as noted in "Use cases", explore 76 | 3. **Mechanism design**: explore other dutch auction designs (e.g. VRGDA, linear price decay) 77 | 4. **Advanced**: toppable auctions (allow to add more liquidity to auction over time), mutual/pooled auctions (allow anyone to add liquidity to a single auction) 78 | 5. **Gas optimizations**: use single contract to store all auctions, use router to reduce number of token approvals required 79 | 6. **Backstop mechanisms**: allow auction creator to define backstop rules (e.g. max auction duration, min selling price) 80 | 81 | ## Demo 82 | 83 | [![Hollander Demo](https://img.youtube.com/vi/6mm0sEYBFyo/0.jpg)](https://www.youtube.com/watch?v=6mm0sEYBFyo) 84 | 85 | ## Screenshots 86 | 87 | ### Home page 88 | 89 | ![Home page](docs/screenshots/1.png) 90 | 91 | ### Auction creation 92 | 93 | ![Auction creation](docs/screenshots/2.png) 94 | 95 | ### Active auction 96 | 97 | ![Active auction](docs/screenshots/3.png) 98 | 99 | ### Complete auction 100 | 101 | ![Complete auction](docs/screenshots/4.png) 102 | 103 | ## Visual explanation 104 | 105 | ![Step 1](docs/explainer/1.png) 106 | 107 | ![Step 2](docs/explainer/2.png) 108 | 109 | ![Step 3](docs/explainer/3.png) 110 | -------------------------------------------------------------------------------- /docs/explainer/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Destiner/hollander-core/7baa6edeb082b50b560859b0b3f00cc2b791e8b0/docs/explainer/1.png -------------------------------------------------------------------------------- /docs/explainer/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Destiner/hollander-core/7baa6edeb082b50b560859b0b3f00cc2b791e8b0/docs/explainer/2.png -------------------------------------------------------------------------------- /docs/explainer/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Destiner/hollander-core/7baa6edeb082b50b560859b0b3f00cc2b791e8b0/docs/explainer/3.png -------------------------------------------------------------------------------- /docs/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Destiner/hollander-core/7baa6edeb082b50b560859b0b3f00cc2b791e8b0/docs/hero.png -------------------------------------------------------------------------------- /docs/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Destiner/hollander-core/7baa6edeb082b50b560859b0b3f00cc2b791e8b0/docs/screenshots/1.png -------------------------------------------------------------------------------- /docs/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Destiner/hollander-core/7baa6edeb082b50b560859b0b3f00cc2b791e8b0/docs/screenshots/2.png -------------------------------------------------------------------------------- /docs/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Destiner/hollander-core/7baa6edeb082b50b560859b0b3f00cc2b791e8b0/docs/screenshots/3.png -------------------------------------------------------------------------------- /docs/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Destiner/hollander-core/7baa6edeb082b50b560859b0b3f00cc2b791e8b0/docs/screenshots/4.png -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.ci] 2 | fuzz-runs = 10_000 3 | -------------------------------------------------------------------------------- /script/ERC20.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "../src/ERC20X.sol"; 6 | 7 | contract MyScript is Script { 8 | function run() external { 9 | vm.startBroadcast(); 10 | 11 | ERC20X dai = new ERC20X("Dai Stablecoin", "DAI", 18, 100000 ether); 12 | ERC20X usdc = new ERC20X("USD Coin", "USDC", 6, 200000000000); 13 | ERC20X mkr = new ERC20X("Maker", "MKR", 18, 70 ether); 14 | ERC20X uni = new ERC20X("Uniswap", "UNI", 18, 7000 ether); 15 | ERC20X bal = new ERC20X("Balancer", "BAL", 18, 5000 ether); 16 | ERC20X weth = new ERC20X("Wrapped Ether", "WETH", 18, 50 ether); 17 | 18 | vm.stopBroadcast(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Auction.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.13; 3 | 4 | import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; 5 | import "prb-math/PRBMathSD59x18.sol"; 6 | 7 | contract Auction { 8 | event Init(uint256 blockStart); 9 | event Swap(address indexed buyer, uint256 amountBuy, uint256 amountSell); 10 | event Withdraw(uint256 amount); 11 | 12 | error AlreadyStarted(); 13 | error Inactive(); 14 | error Unauthorized(); 15 | 16 | using PRBMathSD59x18 for int256; 17 | 18 | address public owner; 19 | 20 | uint256 public blockStart; 21 | 22 | address public tokenBase; 23 | address public tokenQuote; 24 | uint256 public amountBase; 25 | uint256 public initialPrice; 26 | uint256 public halvingPeriod; 27 | uint256 public swapPeriod; 28 | 29 | modifier whenInactive() { 30 | if (blockStart > 0) { 31 | revert AlreadyStarted(); 32 | } 33 | _; 34 | } 35 | 36 | modifier whenActive() { 37 | if (blockStart == 0) { 38 | revert Inactive(); 39 | } 40 | _; 41 | } 42 | 43 | modifier onlyOwner() { 44 | if (owner != msg.sender) { 45 | revert Unauthorized(); 46 | } 47 | _; 48 | } 49 | 50 | constructor( 51 | address _owner, 52 | address _tokenBase, 53 | address _tokenQuote, 54 | uint256 _amountBase, 55 | uint256 _initialPrice, 56 | uint256 _halvingPeriod, 57 | uint256 _swapPeriod 58 | ) { 59 | owner = _owner; 60 | tokenBase = _tokenBase; 61 | tokenQuote = _tokenQuote; 62 | amountBase = _amountBase; 63 | initialPrice = _initialPrice; 64 | halvingPeriod = _halvingPeriod; 65 | swapPeriod = _swapPeriod; 66 | } 67 | 68 | function init() external onlyOwner whenInactive { 69 | IERC20(tokenBase).transferFrom(msg.sender, address(this), amountBase); 70 | blockStart = block.number; 71 | emit Init(blockStart); 72 | } 73 | 74 | function getPrice(uint256 amountIn) public view whenActive returns (uint256 amountOut) { 75 | uint256 boughtAmount = amountBase - IERC20(tokenBase).balanceOf(address(this)) + amountIn; 76 | int256 exponent = ( 77 | (int256(block.number) - int256(blockStart)) * 1 ether 78 | - (int256(boughtAmount) * 1 ether / int256(amountBase)) * int256(swapPeriod) 79 | ) / int256(halvingPeriod); 80 | amountOut = uint256(int256(initialPrice) * 1 ether / exponent.exp2()); 81 | } 82 | 83 | function buy(uint256 amountBuy) external whenActive returns (uint256 amountSell) { 84 | uint256 price = getPrice(amountBuy); 85 | amountSell = price * amountBuy / 1 ether; 86 | IERC20(tokenQuote).transferFrom(msg.sender, address(this), amountSell); 87 | IERC20(tokenBase).transfer(msg.sender, amountBuy); 88 | emit Swap(msg.sender, amountBuy, amountSell); 89 | } 90 | 91 | function withdraw() external onlyOwner returns (uint256 amount) { 92 | amount = IERC20(tokenQuote).balanceOf(address(this)); 93 | IERC20(tokenQuote).transfer(msg.sender, amount); 94 | emit Withdraw(amount); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/ERC20X.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.13; 3 | 4 | import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; 5 | import "prb-math/PRBMathSD59x18.sol"; 6 | 7 | // OZ's ERC20 with custom decimals and capped supply 8 | contract ERC20X is ERC20 { 9 | uint8 storedDecimals; 10 | 11 | constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _supply) ERC20(_name, _symbol) { 12 | storedDecimals = _decimals; 13 | super._mint(msg.sender, _supply); 14 | } 15 | 16 | function decimals() public view override returns (uint8) { 17 | return storedDecimals; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.13; 3 | 4 | import "./Auction.sol"; 5 | 6 | contract Factory { 7 | event NewAuction( 8 | address auction, 9 | address indexed owner, 10 | address indexed tokenBase, 11 | address indexed tokenQuote, 12 | uint256 amountBase, 13 | uint256 initialPrice, 14 | uint256 halvingPeriod, 15 | uint256 swapPeriod 16 | ); 17 | 18 | function createAuction( 19 | address tokenBase, 20 | address tokenQuote, 21 | uint256 amountBase, 22 | uint256 initialPrice, 23 | uint256 halvingPeriod, 24 | uint256 swapPeriod 25 | ) 26 | external 27 | returns (address auction) 28 | { 29 | require(tokenBase != tokenQuote); 30 | auction = 31 | address(new Auction(msg.sender, tokenBase, tokenQuote, amountBase, initialPrice, halvingPeriod, swapPeriod)); 32 | emit NewAuction( 33 | address(auction), msg.sender, tokenBase, tokenQuote, amountBase, initialPrice, halvingPeriod, swapPeriod 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Auction.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.13; 2 | 3 | import "forge-std/Test.sol"; 4 | import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; 5 | import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; 6 | 7 | import "../src/Auction.sol"; 8 | import "../src/Factory.sol"; 9 | 10 | contract AuctionTest is Test { 11 | event Init(uint256 blockStart); 12 | event Swap(address indexed buyer, uint256 amountBuy, uint256 amountSell); 13 | event Withdraw(uint256 amount); 14 | 15 | error AlreadyStarted(); 16 | error Unauthorized(); 17 | 18 | Auction auction; 19 | IERC20 baseToken; 20 | IERC20 quoteToken; 21 | 22 | address alice = 0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa; 23 | address bob = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; 24 | address carol = 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC; 25 | address dave = 0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd; 26 | 27 | function setUp() public { 28 | uint256 AMOUNT = 100 ether; 29 | uint256 PRICE = 3000 ether; 30 | uint256 HALVING_PERIOD = 1000; 31 | uint256 SWAP_PERIOD = 2000; 32 | 33 | baseToken = new ERC20('Wrapper Ether', 'WETH'); 34 | quoteToken = new ERC20('USD Coin', 'USDC'); 35 | Factory factory = new Factory(); 36 | vm.prank(alice); 37 | auction = Auction( 38 | factory.createAuction(address(baseToken), address(quoteToken), AMOUNT, PRICE, HALVING_PERIOD, SWAP_PERIOD) 39 | ); 40 | 41 | deal(address(baseToken), alice, AMOUNT); 42 | deal(address(baseToken), bob, AMOUNT); 43 | deal(address(quoteToken), bob, 60000 ether); 44 | deal(address(quoteToken), carol, 80000 ether); 45 | deal(address(quoteToken), dave, 20000 ether); 46 | 47 | vm.expectEmit(true, true, true, true); 48 | emit Init(block.number); 49 | 50 | vm.startPrank(bob); 51 | baseToken.approve(address(auction), AMOUNT); 52 | vm.expectRevert(Unauthorized.selector); 53 | auction.init(); 54 | vm.stopPrank(); 55 | 56 | vm.startPrank(alice); 57 | baseToken.approve(address(auction), AMOUNT); 58 | auction.init(); 59 | vm.stopPrank(); 60 | 61 | vm.startPrank(alice); 62 | vm.expectRevert(AlreadyStarted.selector); 63 | auction.init(); 64 | vm.stopPrank(); 65 | } 66 | 67 | function testInitialBalance() public { 68 | assertEq(baseToken.balanceOf(address(auction)), 100 ether); 69 | assertEq(quoteToken.balanceOf(address(auction)), 0); 70 | } 71 | 72 | function testPriceAmountImpact() public { 73 | assertEq(auction.getPrice(0), 3000 ether); 74 | vm.roll(block.number + 1000); 75 | assertEq(auction.getPrice(0), 3000 ether / 2); 76 | vm.roll(block.number + 1000); 77 | assertEq(auction.getPrice(0), 3000 ether / 4); 78 | vm.roll(block.number + 1000); 79 | assertEq(auction.getPrice(0), 3000 ether / 8); 80 | } 81 | 82 | function testPriceTimeImpact() public { 83 | assertEq(auction.getPrice(0), 3000 ether); 84 | assertEq(auction.getPrice(2 ether), 3084341479968199529696); 85 | assertEq(auction.getPrice(5 ether), 3215320387608879492574); 86 | assertEq(auction.getPrice(10 ether), 3446095064991105020935); 87 | assertEq(auction.getPrice(30 ether), 4547149699531194251776); 88 | } 89 | 90 | function testPriceTimeAmountImpact() public { 91 | vm.roll(block.number + 1000); 92 | assertEq(auction.getPrice(2 ether), 1542170739984099764055); 93 | vm.roll(block.number + 1000); 94 | assertEq(auction.getPrice(32 ether), 1168746869490749788699); 95 | vm.roll(block.number + 1000); 96 | assertEq(auction.getPrice(92 ether), 1342537606391958643527); 97 | vm.roll(block.number + 200); 98 | assertEq(auction.getPrice(100 ether), 1305825844944186209043); 99 | } 100 | 101 | function testBuy() public { 102 | vm.prank(bob); 103 | quoteToken.approve(address(auction), (2 ** 256) - 1); 104 | vm.prank(carol); 105 | quoteToken.approve(address(auction), (2 ** 256) - 1); 106 | vm.prank(dave); 107 | quoteToken.approve(address(auction), (2 ** 256) - 1); 108 | 109 | buy(bob, block.number + 800, 2 ether, 3542977984288591199678); 110 | buy(bob, block.number + 250, 5 ether, 7982776368400199161110); 111 | buy(carol, block.number + 150, 4 ether, 6083756878740174834652); 112 | buy(carol, block.number + 250, 9 ether, 13040140440485414943756); 113 | buy(bob, block.number + 250, 15 ether, 22500 ether); 114 | buy(dave, block.number + 200, 11 ether, 16730331416535480795293); 115 | buy(bob, block.number + 340, 15 ether, 22190235851100581394570); 116 | buy(carol, block.number + 540, 25 ether, 35972404474697414645650); 117 | buy(carol, block.number + 210, 12 ether, 17629565356564683684060); 118 | buy(dave, block.number + 60, 2 ether, 2897808986774536654168); 119 | } 120 | 121 | function testWithdraw() public { 122 | vm.prank(bob); 123 | quoteToken.approve(address(auction), (2 ** 256) - 1); 124 | vm.prank(carol); 125 | quoteToken.approve(address(auction), (2 ** 256) - 1); 126 | vm.prank(dave); 127 | quoteToken.approve(address(auction), (2 ** 256) - 1); 128 | 129 | uint256 sellAmountBob = 24148974395964370357370; 130 | buy(bob, 3000, 38 ether, sellAmountBob); 131 | vm.expectEmit(true, true, true, true); 132 | emit Withdraw(sellAmountBob); 133 | 134 | vm.prank(alice); 135 | uint256 amountA = auction.withdraw(); 136 | assertEq(amountA, sellAmountBob); 137 | 138 | uint256 sellAmountCarol = 63549932620958869374950; 139 | buy(carol, 3000, 50 ether, sellAmountCarol); 140 | uint256 sellAmountDave = 18012480974326451393280; 141 | buy(dave, 3000, 12 ether, sellAmountDave); 142 | vm.expectEmit(true, true, true, true); 143 | emit Withdraw(sellAmountCarol + sellAmountDave); 144 | 145 | vm.prank(alice); 146 | uint256 amountB = auction.withdraw(); 147 | assertEq(amountB, sellAmountCarol + sellAmountDave); 148 | 149 | vm.startPrank(bob); 150 | vm.expectRevert(Unauthorized.selector); 151 | auction.withdraw(); 152 | vm.stopPrank(); 153 | 154 | assertEq(baseToken.balanceOf(address(auction)), 0); 155 | assertEq(quoteToken.balanceOf(address(auction)), 0); 156 | assertEq(baseToken.balanceOf(alice), 0); 157 | assertEq(quoteToken.balanceOf(alice), sellAmountBob + sellAmountCarol + sellAmountDave); 158 | } 159 | 160 | function buy(address buyer, uint256 buyBlock, uint256 buyAmount, uint256 expectedSellAmount) internal { 161 | vm.roll(buyBlock); 162 | uint256 auctionBaseBalance = baseToken.balanceOf(address(auction)); 163 | uint256 auctionQuoteBalance = quoteToken.balanceOf(address(auction)); 164 | uint256 buyerBaseBalance = baseToken.balanceOf(buyer); 165 | uint256 buyerQuoteBalance = quoteToken.balanceOf(buyer); 166 | 167 | vm.expectEmit(true, true, true, true); 168 | emit Swap(buyer, buyAmount, expectedSellAmount); 169 | 170 | vm.prank(buyer); 171 | uint256 sellAmount = auction.buy(buyAmount); 172 | assertEq(sellAmount, expectedSellAmount); 173 | assertEq(baseToken.balanceOf(address(auction)), auctionBaseBalance - buyAmount); 174 | assertEq(quoteToken.balanceOf(address(auction)), auctionQuoteBalance + sellAmount); 175 | assertEq(baseToken.balanceOf(buyer), buyerBaseBalance + buyAmount); 176 | assertEq(quoteToken.balanceOf(buyer), buyerQuoteBalance - sellAmount); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /test/Factory.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.13; 2 | 3 | import "forge-std/Test.sol"; 4 | import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; 5 | import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; 6 | 7 | import "../src/Factory.sol"; 8 | 9 | contract FactoryTest is Test { 10 | event NewAuction( 11 | address auction, 12 | address indexed owner, 13 | address indexed tokenBase, 14 | address indexed tokenQuote, 15 | uint256 amountBase, 16 | uint256 initialPrice, 17 | uint256 halvingPeriod, 18 | uint256 swapPeriod 19 | ); 20 | 21 | address alice = 0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa; 22 | address auctionAddress = 0xdd36aa107BcA36Ba4606767D873B13B4770F3b12; 23 | 24 | function testCreation() public { 25 | uint256 AMOUNT = 100 ether; 26 | uint256 PRICE = 3000 ether; 27 | uint256 HALVING_PERIOD = 1000; 28 | uint256 SWAP_PERIOD = 2000; 29 | 30 | IERC20 baseToken = new ERC20('Wrapper Ether', 'WETH'); 31 | IERC20 quoteToken = new ERC20('USD Coin', 'USDC'); 32 | Factory factory = new Factory(); 33 | vm.expectEmit(true, true, true, true); 34 | emit NewAuction( 35 | auctionAddress, alice, address(baseToken), address(quoteToken), AMOUNT, PRICE, HALVING_PERIOD, SWAP_PERIOD 36 | ); 37 | vm.prank(alice); 38 | Auction auction = Auction( 39 | factory.createAuction(address(baseToken), address(quoteToken), AMOUNT, PRICE, HALVING_PERIOD, SWAP_PERIOD) 40 | ); 41 | 42 | assertEq(auction.owner(), alice); 43 | assertEq(auction.tokenBase(), address(baseToken)); 44 | assertEq(auction.tokenQuote(), address(quoteToken)); 45 | assertEq(auction.amountBase(), AMOUNT); 46 | assertEq(auction.initialPrice(), PRICE); 47 | assertEq(auction.halvingPeriod(), HALVING_PERIOD); 48 | assertEq(auction.swapPeriod(), SWAP_PERIOD); 49 | } 50 | } 51 | --------------------------------------------------------------------------------