diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml new file mode 100644 index 000000000..e49a6a576 --- /dev/null +++ b/.github/actions/install-dependencies/action.yml @@ -0,0 +1,37 @@ +name: Install Node and package dependencies +description: "Install Node dependencies with pnpm" + +runs: + using: "composite" + steps: + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v3 + with: + version: 8.3.1 + run_install: false + + - name: Get pnpm cache directory + id: pnpm-cache-dir + shell: bash + run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Cache PNPM + uses: actions/cache@v4 + id: pnpm-cache + with: + path: | + ${{ steps.pnpm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-node- + + - name: Install packages + shell: bash + run: pnpm install --frozen-lockfile + + - name: Build everything in the monorepo + shell: bash + run: pnpm build diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 000000000..12214da4f --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,16 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + clean: false + - name: Install Node and dependencies + uses: ./.github/actions/install-dependencies + - run: pnpm run lint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index e86b2adb8..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: CI -on: [ push ] - -jobs: - lint: - name: Lint - runs-on: ubuntu-20.04 - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v1 - with: - node-version: '16.14.0' - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: Install dependencies - run: yarn install --prefer-offline --frozen-lockfile - - name: Run lint - run: yarn lint - test: - name: Test - runs-on: ubuntu-20.04 - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v1 - with: - node-version: '16.14.0' - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: Install dependencies - run: yarn install --prefer-offline --frozen-lockfile - - name: Build - run: yarn build - - name: Run tests - run: yarn test - test-e2e: - name: E2E Test - runs-on: ubuntu-20.04 - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v1 - with: - node-version: '16.14.0' - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: Install dependencies - run: yarn install --prefer-offline --frozen-lockfile - - name: Build - run: yarn build - working-directory: packages/contracts - - name: Run E2E contract tests - run: yarn test:e2e - working-directory: packages/contracts - verify: - name: Formal verification - runs-on: ubuntu-20.04 - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: '3.9' - - name: Setup Node.js - uses: actions/setup-node@v2 - with: - node-version: '16.14.0' - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - run: pip install -r packages/contracts/spec/requirements.txt - - name: Install dependencies - run: yarn install --prefer-offline --frozen-lockfile - - name: Run FV - run: yarn verify - env: - CERTORAKEY: ${{ secrets.CERTORAKEY }} diff --git a/.gitignore b/.gitignore index cc502797a..d23500906 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ tsconfig.tsbuildinfo # Eslint cache .eslintcache +# Build files +.turbo + # Generated by Certora .certora_* .last_confs diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0fd25d453..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "backend"] - path = packages/backend - url = git@github.com:TrueFiEng/devcon-raffle-backend.git diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..bafea5a1a --- /dev/null +++ b/.npmrc @@ -0,0 +1,7 @@ +use-lockfile-v6=true +strict-peer-dependencies=false +auto-install-peers=false +dedupe-peer-dependents=false +resolve-peers-from-workspace-root=false +save-workspace-protocol=true +resolution-mode=highest \ No newline at end of file diff --git a/README.md b/README.md index e120e42cc..2cf4c861a 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,3 @@ -# Devcon Ticket Sale System - -## Running Hardhat node -Change directory to `packages/contracts` and execute: -```shell -yarn node:run -``` -This will start a Hardhat node, deploy the contracts and place initial bids using the first twenty auto-generated accounts. - -## Custom tasks -A number of custom Hardhat tasks were defined to aid testing. - -### Managing local node -- `yarn node:increase-time [--value ]` - increase block time by *value* seconds, defaults to six hour -- `yarn node:accounts` - print a list of available accounts - -### Interacting with AuctionRaffle contract - -#### Hardhat -- `yarn hardhat:bid --account --amount ` - using *account* place bid of *amount* ETH -- `yarn hardhat:bid-random --amount [--account ]` - using randomly generated accounts place *amount* of bids using funds from account with index *account* (defaults to `0`) -- `yarn hardhat:settle-auction` - settle auction -- `yarn hardhat:settle-raffle` - settle raffle using random numbers -- `yarn hardhat:settle` - increase time, settle auction and raffle - -#### Arbitrum Rinkeby -- `yarn rinkeby:generate-dotenv [--path ] [--count ]` - generate .env file needed for other tasks, *path* - output path, *count* - number of private keys to generate -- `yarn rinkeby:transfer-ether` - transfer ether from `DEPLOYER` to `PRIVATE_KEYS` accounts -- `yarn rinkeby:init-bids` - place initial bids using `PRIVATE_KEYS` accounts - -#### Ethereum Mainnet -- `yarn ethereum:generate-random-numbers --blocks --secret ` - generate random numbers for raffle settlement, *blocks* - array of block numbers from which extract block hash (e.g. "[1234, 5678]"), *secret* - secret number represented as 32 bytes hex string +# Devcon Ticket Sale System (Auction-Raffle) +This repository houses the Auction-Raffle ticket sale system used for Devcon. diff --git a/audits/Ethereum Foundation Devcon Auction-Raffle Summary Report.pdf b/audits/Ethereum Foundation Devcon Auction-Raffle Summary Report.pdf new file mode 100644 index 000000000..96096c959 Binary files /dev/null and b/audits/Ethereum Foundation Devcon Auction-Raffle Summary Report.pdf differ diff --git a/package.json b/package.json index 30740bc48..93cea9c11 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,14 @@ "name": "devcon-raffle", "private": true, "version": "0.0.1", + "packageManager": "pnpm@8.3.1", "scripts": { - "lint": "wsrun -c lint", - "build": "wsrun -te -c build", - "test": "wsrun -c test", - "verify": "wsrun -m -c verify", - "update": "git submodule update --remote" + "lint": "turbo lint --filter=frontend", + "build": "turbo build", + "test": "turbo test", + "verify": "turbo verify" }, - "workspaces": [ - "packages/contracts", - "packages/frontend" - ], "devDependencies": { - "wsrun": "^5.2.4" + "turbo": "^1.13.2" } } diff --git a/packages/backend b/packages/backend deleted file mode 160000 index 234ccfa59..000000000 --- a/packages/backend +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 234ccfa59f80c7819a4256cb4bda87a36d193f19 diff --git a/packages/contracts/.prettierrc.json b/packages/contracts/.prettierrc.json index bc6ebb6cd..d3eb337db 100644 --- a/packages/contracts/.prettierrc.json +++ b/packages/contracts/.prettierrc.json @@ -5,6 +5,15 @@ "options": { "printWidth": 120 } + }, + { + "files": "*.ts", + "options": { + "semi": false, + "singleQuote": true, + "printWidth": 120, + "bracketSpacing": true + } } ] } diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 97404c22c..b6afd87aa 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -1 +1,65 @@ # Contracts + +## High-level Overview + +`AuctionRaffle.sol` is a combined auction and raffle system for Devcon ticket sales. At a high level, the contract progresses through the following phases. + +1. Initial phase: This is the initial state of the contract, before bidding starts. + +2. Bidding phase: When bidding opens after a configurable timestamp, the contract accepts bids from eligible wallets through the `bid` function. Alternatively, eligible wallets may also increase their existing bids using the `bump` function. The eligibility of a wallet is verified by an attestation mechanism that is hosted by the operator (see `contracts/verifier/ScoreAttestationVerifier.sol`). For Devcon 7, eligibility is determined by a Gitcoin passport score (also, see the [Gitcoin Passport Score Attestation](#gitcoin-passport-score-attestation) section below). + +3. Auction phase: After the bidding period has ended, the operator settles the auction by calling the `settleAuction` function, which determines the auction winners (the number of auction winners is configurable). + +4. Raffle phase: After the auction has settled, the operator begins the raffling phase by requesting a verifiable random number from Chainlink VRF. When the random number is delivered to the contract, the raffle winners are determined. The raffle winners are lazily evaluated using a Feistel shuffle to minimise gas usage (see: [Improving the Devcon Raffle](https://docs.fairy.dev/case-studies/improving-the-devcon-raffle)). + +5. Claim phase: After the raffle has settled, some participants may withdraw none, some, or all of their bid amounts depending on the outcome of the Auction Raffle. + +- Auction winners are not entitled to claim any amount. +- The first raffle winner is the lucky winner of the _golden ticket_ and may use the `claim` function to withdraw their full bid amount. +- All other raffle winners may withdraw their bid amount minus the reserve price. +- Losers may withdraw their full bid amount (there was previously a non-refundable 2% fee, but that has now been removed). + +## Gitcoin Passport Score Attestation + +For this 2024's edition of Devcon, users must have possess a minimum score on Gitcoin Passport in order to bid for a ticket. The `frontend` package in this repository has a backend component that queries the Gitcoin Passport API and returns signed attestations that users must include with their bids. A sequence diagram of this attestation flow is supplied below. + +![passport_msd](./docs/auction_raffle_passport_msd.jpg) + +## Running Hardhat node + +Change directory to `packages/contracts` and execute: + +```shell +pnpm node:run +``` + +This will start a Hardhat node, deploy the contracts and place initial bids using the first twenty auto-generated accounts. + +## Custom tasks + +A number of custom Hardhat tasks were defined to aid testing. + +### Managing local node + +- `pnpm node:increase-time [--value ]` - increase block time by _value_ seconds, defaults to six hour +- `pnpm node:accounts` - print a list of available accounts + +### Interacting with AuctionRaffle contract + +#### Hardhat + +- `pnpm hardhat:bid --account --amount ` - using _account_ place bid of _amount_ ETH +- `pnpm hardhat:bid-random --amount [--account ]` - using randomly generated accounts place _amount_ of bids using funds from account with index _account_ (defaults to `0`) +- `pnpm hardhat:settle-auction` - settle auction +- `pnpm hardhat:settle-raffle` - settle raffle using random numbers +- `pnpm hardhat:settle` - increase time, settle auction and raffle + +#### Arbitrum Rinkeby + +- `pnpm rinkeby:generate-dotenv [--path ] [--count ]` - generate .env file needed for other tasks, _path_ - output path, _count_ - number of private keys to generate +- `pnpm rinkeby:transfer-ether` - transfer ether from `DEPLOYER` to `PRIVATE_KEYS` accounts +- `pnpm rinkeby:init-bids` - place initial bids using `PRIVATE_KEYS` accounts + +#### Ethereum Mainnet + +- `pnpm ethereum:generate-random-numbers --blocks --secret ` - generate random numbers for raffle settlement, _blocks_ - array of block numbers from which extract block hash (e.g. "[1234, 5678]"), _secret_ - secret number represented as 32 bytes hex string diff --git a/packages/contracts/contracts/AuctionRaffle.sol b/packages/contracts/contracts/AuctionRaffle.sol index 0afd14d36..4c602a16d 100644 --- a/packages/contracts/contracts/AuctionRaffle.sol +++ b/packages/contracts/contracts/AuctionRaffle.sol @@ -10,13 +10,16 @@ import "./Config.sol"; import "./models/BidModel.sol"; import "./models/StateModel.sol"; import "./libs/MaxHeap.sol"; +import "./libs/FeistelShuffleOptimised.sol"; +import "./libs/VRFRequester.sol"; +import "./verifier/IVerifier.sol"; /*** * @title Auction & Raffle * @notice Draws winners using a mixed auction & raffle scheme. * @author TrueFi Engineering team */ -contract AuctionRaffle is Ownable, Config, BidModel, StateModel { +contract AuctionRaffle is Ownable, Config, BidModel, StateModel, VRFRequester { using SafeERC20 for IERC20; using MaxHeap for uint256[]; @@ -32,12 +35,10 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { uint256[] _raffleParticipants; uint256[] _auctionWinners; - uint256[] _raffleWinners; bool _proceedsClaimed; - uint256 _claimedFeesIndex; - uint256[] _tempWinners; // temp array for sorting auction winners used by settleAuction method + uint256 public _randomSeed; /// @dev A new bid has been placed or an existing bid has been bumped event NewBid(address bidder, uint256 bidderID, uint256 bidAmount); @@ -45,11 +46,11 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { /// @dev A bidder has been drawn as auction winner event NewAuctionWinner(uint256 bidderID); - /// @dev A bidder has been drawn as raffle winner - event NewRaffleWinner(uint256 bidderID); + /// @dev Random number has been requested for the raffle + event RandomNumberRequested(uint256 requestId); - /// @dev A bidder has been drawn as the golden ticket winner - event NewGoldenTicketWinner(uint256 bidderID); + /// @dev Raffle winners have been drawn + event RaffleWinnersDrawn(uint256 randomSeed); modifier onlyInState(State requiredState) { require(getState() == requiredState, "AuctionRaffle: is in invalid state"); @@ -63,25 +64,9 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { constructor( address initialOwner, - uint256 biddingStartTime, - uint256 biddingEndTime, - uint256 claimingEndTime, - uint256 auctionWinnersCount, - uint256 raffleWinnersCount, - uint256 reservePrice, - uint256 minBidIncrement - ) - Config( - biddingStartTime, - biddingEndTime, - claimingEndTime, - auctionWinnersCount, - raffleWinnersCount, - reservePrice, - minBidIncrement - ) - Ownable() - { + ConfigParams memory configParams, + VRFRequesterParams memory vrfRequesterParams + ) Config(configParams) VRFRequester(vrfRequesterParams) Ownable() { if (initialOwner != msg.sender) { Ownable.transferOwnership(initialOwner); } @@ -96,26 +81,40 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { } /*** - * @notice Places a new bid or bumps an existing bid. + * @notice Places a new bid. * @dev Assigns a unique bidderID to the sender address. + * @param score The user's sybil resistance score + * @param proof Attestation signature of the score */ - function bid() external payable onlyExternalTransactions onlyInState(State.BIDDING_OPEN) { + function bid( + uint256 score, + bytes calldata proof + ) external payable onlyExternalTransactions onlyInState(State.BIDDING_OPEN) { + IVerifier(_bidVerifier).verify(abi.encode(msg.sender, score), proof); Bid storage bidder = _bids[msg.sender]; - if (bidder.amount == 0) { - require(msg.value >= _reservePrice, "AuctionRaffle: bid amount is below reserve price"); - bidder.amount = msg.value; - bidder.bidderID = _nextBidderID++; - _bidders[bidder.bidderID] = payable(msg.sender); - _raffleParticipants.push(bidder.bidderID); - - addBidToHeap(bidder.bidderID, bidder.amount); - } else { - require(msg.value >= _minBidIncrement, "AuctionRaffle: bid increment too low"); - uint256 oldAmount = bidder.amount; - bidder.amount += msg.value; + require(bidder.amount == 0, "AuctionRaffle: bid already exists"); + require(msg.value >= _reservePrice, "AuctionRaffle: bid amount is below reserve price"); + bidder.amount = msg.value; + bidder.bidderID = _nextBidderID++; + _bidders[bidder.bidderID] = payable(msg.sender); + bidder.raffleParticipantIndex = uint240(_raffleParticipants.length); + _raffleParticipants.push(bidder.bidderID); + + addBidToHeap(bidder.bidderID, bidder.amount); + emit NewBid(msg.sender, bidder.bidderID, bidder.amount); + } - updateHeapBid(bidder.bidderID, oldAmount, bidder.amount); - } + /*** + * @notice Bumps an existing bid. + */ + function bump() external payable onlyExternalTransactions onlyInState(State.BIDDING_OPEN) { + Bid storage bidder = _bids[msg.sender]; + require(bidder.amount != 0, "AuctionRaffle: bump nonexistent bid"); + require(msg.value >= _minBidIncrement, "AuctionRaffle: bid increment too low"); + uint256 oldAmount = bidder.amount; + bidder.amount += msg.value; + + updateHeapBid(bidder.bidderID, oldAmount, bidder.amount); emit NewBid(msg.sender, bidder.bidderID, bidder.amount); } @@ -126,7 +125,7 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { * This is done to efficiently remove auction winners from _raffleParticipants array as they no longer take part * in the raffle. */ - function settleAuction() external onlyOwner onlyInState(State.BIDDING_CLOSED) { + function settleAuction() external onlyInState(State.BIDDING_CLOSED) { _settleState = SettleState.AUCTION_SETTLED; uint256 biddersCount = getBiddersCount(); uint256 raffleWinnersCount = _raffleWinnersCount; @@ -144,74 +143,54 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { uint256 key = _heap.removeMax(); uint256 bidderID = extractBidderID(key); addAuctionWinner(bidderID); - _tempWinners.insert(bidderID); } delete _heap; delete _minKeyIndex; delete _minKeyValue; + } - for (uint256 i = 0; i < winnersLength; ++i) { - uint256 bidderID = _tempWinners.removeMax(); - removeRaffleParticipant(bidderID - 1); - } + /** + * @notice Initiate raffle draw by requesting a random number from Chainlink VRF. + */ + function settleRaffle() external onlyInState(State.AUCTION_SETTLED) returns (uint256) { + uint256 reqId = _getRandomNumber(); + emit RandomNumberRequested(reqId); + return reqId; } /** * @notice Draws raffle winners and changes contract state to RAFFLE_SETTLED. The first selected raffle winner * becomes the Golden Ticket winner. - * @dev Sets WinType of the first selected bid to GOLDEN_TICKET. Sets WinType to RAFFLE for the remaining selected - * bids. - * @param randomNumbers The source of randomness for the function. Each random number is used to draw at most - * `_winnersPerRandom` raffle winners. + * @param randomSeed A single 256-bit random seed. */ - function settleRaffle(uint256[] memory randomNumbers) external onlyOwner onlyInState(State.AUCTION_SETTLED) { - require(randomNumbers.length > 0, "AuctionRaffle: there must be at least one random number passed"); - + function _receiveRandomNumber(uint256 randomSeed) internal override onlyInState(State.AUCTION_SETTLED) { _settleState = SettleState.RAFFLE_SETTLED; + _randomSeed = randomSeed; - uint256 participantsLength = _raffleParticipants.length; - if (participantsLength == 0) { - return; - } - - (participantsLength, randomNumbers[0]) = selectGoldenTicketWinner(participantsLength, randomNumbers[0]); - - uint256 raffleWinnersCount = _raffleWinnersCount; - if (participantsLength < raffleWinnersCount) { - selectAllRaffleParticipantsAsWinners(participantsLength); - return; - } - - require( - randomNumbers.length == raffleWinnersCount / _winnersPerRandom, - "AuctionRaffle: passed incorrect number of random numbers" - ); - - selectRaffleWinners(participantsLength, randomNumbers); + emit RaffleWinnersDrawn(randomSeed); } /** * @notice Allows a bidder to claim their funds after the raffle is settled. * Golden Ticket winner can withdraw the full bid amount. * Raffle winner can withdraw the bid amount minus `_reservePrice`. - * Non-winning bidder can withdraw the bid amount minus 2% fee. + * Non-winning bidder can withdraw the full bid amount. * Auction winner pays the full bid amount and is not entitled to any withdrawal. */ function claim(uint256 bidderID) external onlyInState(State.RAFFLE_SETTLED) { address payable bidderAddress = getBidderAddress(bidderID); Bid storage bidder = _bids[bidderAddress]; require(!bidder.claimed, "AuctionRaffle: funds have already been claimed"); - require(bidder.winType != WinType.AUCTION, "AuctionRaffle: auction winners cannot claim funds"); - + require(!bidder.isAuctionWinner, "AuctionRaffle: auction winners cannot claim funds"); bidder.claimed = true; + + WinType winType = getBidWinType(bidderID); uint256 claimAmount; - if (bidder.winType == WinType.RAFFLE) { - claimAmount = bidder.amount - _reservePrice; - } else if (bidder.winType == WinType.GOLDEN_TICKET) { + if (winType == WinType.GOLDEN_TICKET || winType == WinType.LOSS) { claimAmount = bidder.amount; - } else if (bidder.winType == WinType.LOSS) { - claimAmount = (bidder.amount * 98) / 100; + } else if (winType == WinType.RAFFLE) { + claimAmount = bidder.amount - _reservePrice; } if (claimAmount > 0) { @@ -251,34 +230,6 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { payable(owner()).transfer(totalAmount); } - /** - * @notice Allows the owner to claim the 2% fees from non-winning bids after the raffle is settled. - * @dev This function is designed to be called multiple times, to split iteration though all non-winning bids across - * multiple transactions. - * @param bidsCount The number of bids to be processed at once. - */ - function claimFees(uint256 bidsCount) external onlyOwner onlyInState(State.RAFFLE_SETTLED) { - uint256 claimedFeesIndex = _claimedFeesIndex; - uint256 feesCount = _raffleParticipants.length; - require(feesCount > 0, "AuctionRaffle: there are no fees to claim"); - require(claimedFeesIndex < feesCount, "AuctionRaffle: fees have already been claimed"); - - uint256 endIndex = claimedFeesIndex + bidsCount; - if (endIndex > feesCount) { - endIndex = feesCount; - } - - uint256 fee = 0; - for (uint256 i = claimedFeesIndex; i < endIndex; ++i) { - address bidderAddress = getBidderAddress(_raffleParticipants[i]); - uint256 bidAmount = _bids[bidderAddress].amount; - fee += bidAmount - (bidAmount * 98) / 100; - } - - _claimedFeesIndex = endIndex; - payable(owner()).transfer(fee); - } - /** * @notice Allows the owner to withdraw all funds left in the contract by the participants. * Callable only after the claiming window is closed. @@ -300,6 +251,7 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { token.safeTransfer(owner(), balance); } + /// @return A list of raffle participants; including the winners (if settled) function getRaffleParticipants() external view returns (uint256[] memory) { return _raffleParticipants; } @@ -309,9 +261,22 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { return _auctionWinners; } - /// @return A list of raffle winner bidder IDs. - function getRaffleWinners() external view returns (uint256[] memory) { - return _raffleWinners; + /// @return winners A list of raffle winner bidder IDs. + function getRaffleWinners() external view onlyInState(State.RAFFLE_SETTLED) returns (uint256[] memory winners) { + uint256 participantsCount = _raffleParticipants.length; + uint256 raffleWinnersCount = _raffleWinnersCount; + uint256 n = participantsCount < raffleWinnersCount ? participantsCount : raffleWinnersCount; + uint256 randomSeed = _randomSeed; + + winners = new uint256[](n); + for (uint256 i; i < n; ++i) { + // Map inverse `i`th place winner -> original index + uint256 winnerIndex = FeistelShuffleOptimised.deshuffle(i, participantsCount, randomSeed, 4); + // Map original participant index -> bidder id + uint256 winningBidderId = _raffleParticipants[winnerIndex]; + // Record winner in storage + winners[i] = winningBidderId; + } } function getBid(address bidder) external view returns (Bid memory) { @@ -412,11 +377,7 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { * @param oldAmount Previous bid amount * @param newAmount New bid amount */ - function updateHeapBid( - uint256 bidderID, - uint256 oldAmount, - uint256 newAmount - ) private { + function updateHeapBid(uint256 bidderID, uint256 oldAmount, uint256 newAmount) private { bool isHeapFull = getBiddersCount() >= _auctionWinnersCount; uint256 key = getKey(bidderID, newAmount); uint256 minKeyValue = _minKeyValue; @@ -439,99 +400,47 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { (_minKeyIndex, _minKeyValue) = _heap.findMin(); } + /** + * Record auction winner, and additionally remove them from the raffle + * participants list. + * @param bidderID Unique bidder ID + */ function addAuctionWinner(uint256 bidderID) private { - setBidWinType(bidderID, WinType.AUCTION); + address bidderAddress = getBidderAddress(bidderID); + _bids[bidderAddress].isAuctionWinner = true; _auctionWinners.push(bidderID); emit NewAuctionWinner(bidderID); - } - - function addRaffleWinner(uint256 bidderID) private { - setBidWinType(bidderID, WinType.RAFFLE); - _raffleWinners.push(bidderID); - emit NewRaffleWinner(bidderID); - } - - function addGoldenTicketWinner(uint256 bidderID) private { - setBidWinType(bidderID, WinType.GOLDEN_TICKET); - _raffleWinners.push(bidderID); - emit NewGoldenTicketWinner(bidderID); - } - - function setBidWinType(uint256 bidderID, WinType winType) private { - address bidderAddress = getBidderAddress(bidderID); - _bids[bidderAddress].winType = winType; + removeRaffleParticipant(_bids[bidderAddress].raffleParticipantIndex); } /** - * @dev Selects one Golden Ticket winner from a random number. - * Saves the winner at the beginning of _raffleWinners array and sets bidder WinType to GOLDEN_TICKET. - * @param participantsLength The length of current participants array - * @param randomNumber The random number to select raffle winner from - * @return participantsLength New participants array length - * @return randomNumber Shifted random number by `_randomMaskLength` bits to the right + * Determine the WinType of a bid, i.e. whether a bid is an auction winner, + * a golden ticket winner, a raffle winner, or a loser. + * @param bidderID Monotonically-increasing unique bidder identifier */ - function selectGoldenTicketWinner(uint256 participantsLength, uint256 randomNumber) - private - returns (uint256, uint256) - { - uint256 winnerIndex = winnerIndexFromRandomNumber(participantsLength, randomNumber); - - uint256 bidderID = _raffleParticipants[winnerIndex]; - addGoldenTicketWinner(bidderID); - - removeRaffleParticipant(winnerIndex); - return (participantsLength - 1, randomNumber >> _randomMaskLength); - } - - function selectAllRaffleParticipantsAsWinners(uint256 participantsLength) private { - for (uint256 i = 0; i < participantsLength; ++i) { - uint256 bidderID = _raffleParticipants[i]; - addRaffleWinner(bidderID); + function getBidWinType(uint256 bidderID) public view returns (WinType) { + if (uint8(getState()) < uint8(State.AUCTION_SETTLED)) { + return WinType.LOSS; } - delete _raffleParticipants; - } - /** - * @dev Selects `_winnersPerRandom` - 1 raffle winners for the first random number -- it assumes that one bidder - * was selected before as the Golden Ticket winner. Then it selects `_winnersPerRandom` winners for each remaining - * random number. - * @param participantsLength The length of current participants array - * @param randomNumbers The array of random numbers to select raffle winners from - */ - function selectRaffleWinners(uint256 participantsLength, uint256[] memory randomNumbers) private { - participantsLength = selectRandomRaffleWinners(participantsLength, randomNumbers[0], _winnersPerRandom - 1); - for (uint256 i = 1; i < randomNumbers.length; ++i) { - participantsLength = selectRandomRaffleWinners(participantsLength, randomNumbers[i], _winnersPerRandom); + address bidderAddress = getBidderAddress(bidderID); + Bid memory bid_ = _bids[bidderAddress]; + if (bid_.isAuctionWinner) { + return WinType.AUCTION; } - } - /** - * @notice Selects a number of raffle winners from _raffleParticipants array. Saves the winners in _raffleWinners - * array and sets their WinType to RAFFLE. - * @dev Divides passed randomNumber into `_randomMaskLength` bit numbers and then selects one raffle winner using - * each small number. - * @param participantsLength The length of current participants array - * @param randomNumber The random number used to select raffle winners - * @param winnersCount The number of raffle winners to select from a single random number - * @return New participants length - */ - function selectRandomRaffleWinners( - uint256 participantsLength, - uint256 randomNumber, - uint256 winnersCount - ) private returns (uint256) { - for (uint256 i = 0; i < winnersCount; ++i) { - uint256 winnerIndex = winnerIndexFromRandomNumber(participantsLength, randomNumber); - - uint256 bidderID = _raffleParticipants[winnerIndex]; - addRaffleWinner(bidderID); - - removeRaffleParticipant(winnerIndex); - --participantsLength; - randomNumber = randomNumber >> _randomMaskLength; + uint256 participantsCount = _raffleParticipants.length; + uint256 raffleWinnersCount = _raffleWinnersCount; + uint256 n = participantsCount < raffleWinnersCount ? participantsCount : raffleWinnersCount; + // Map original index -> inverse `i`th place winner + uint256 place = FeistelShuffleOptimised.shuffle(bid_.raffleParticipantIndex, participantsCount, _randomSeed, 4); + if (place == 0) { + return WinType.GOLDEN_TICKET; + } else if (place < n) { + return WinType.RAFFLE; + } else { + return WinType.LOSS; } - - return participantsLength; } /** @@ -542,7 +451,9 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { function removeRaffleParticipant(uint256 index) private { uint256 participantsLength = _raffleParticipants.length; require(index < participantsLength, "AuctionRaffle: invalid raffle participant index"); - _raffleParticipants[index] = _raffleParticipants[participantsLength - 1]; + uint256 lastBidderID = _raffleParticipants[participantsLength - 1]; + _raffleParticipants[index] = lastBidderID; + _bids[_bidders[lastBidderID]].raffleParticipantIndex = uint240(index); _raffleParticipants.pop(); } @@ -567,20 +478,4 @@ contract AuctionRaffle is Ownable, Config, BidModel, StateModel { function extractBidderID(uint256 key) private pure returns (uint256) { return _bidderMask - (key & _bidderMask); } - - /** - * @notice Calculates winner index - * @dev Calculates modulo of `_randomMaskLength` lower bits of randomNumber and participantsLength - * @param participantsLength The length of current participants array - * @param randomNumber The random number to select raffle winner from - * @return Winner index - */ - function winnerIndexFromRandomNumber(uint256 participantsLength, uint256 randomNumber) - private - pure - returns (uint256) - { - uint256 smallRandom = randomNumber & _randomMask; - return smallRandom % participantsLength; - } } diff --git a/packages/contracts/contracts/Config.sol b/packages/contracts/contracts/Config.sol index 3187fb918..fc5d7d1c5 100644 --- a/packages/contracts/contracts/Config.sol +++ b/packages/contracts/contracts/Config.sol @@ -7,6 +7,17 @@ pragma solidity 0.8.10; * @author TrueFi Engineering team */ abstract contract Config { + struct ConfigParams { + uint256 biddingStartTime; + uint256 biddingEndTime; + uint256 claimingEndTime; + uint256 auctionWinnersCount; + uint256 raffleWinnersCount; + uint256 reservePrice; + uint256 minBidIncrement; + address bidVerifier; + } + // The use of _randomMask and _bidderMask introduces an assumption on max number of participants: 2^32. // The use of _bidderMask also introduces an assumption on max bid amount: 2^224 wei. // Both of these values are fine for our use case. @@ -24,15 +35,18 @@ abstract contract Config { uint256 immutable _reservePrice; uint256 immutable _minBidIncrement; - constructor( - uint256 biddingStartTime_, - uint256 biddingEndTime_, - uint256 claimingEndTime_, - uint256 auctionWinnersCount_, - uint256 raffleWinnersCount_, - uint256 reservePrice_, - uint256 minBidIncrement_ - ) { + address immutable _bidVerifier; + + constructor(ConfigParams memory params) { + uint256 biddingStartTime_ = params.biddingStartTime; + uint256 biddingEndTime_ = params.biddingEndTime; + uint256 claimingEndTime_ = params.claimingEndTime; + uint256 auctionWinnersCount_ = params.auctionWinnersCount; + uint256 raffleWinnersCount_ = params.raffleWinnersCount; + uint256 reservePrice_ = params.reservePrice; + uint256 minBidIncrement_ = params.minBidIncrement; + address bidVerifier_ = params.bidVerifier; + require(auctionWinnersCount_ > 0, "Config: auction winners count must be greater than 0"); require(raffleWinnersCount_ > 0, "Config: raffle winners count must be greater than 0"); require(raffleWinnersCount_ % _winnersPerRandom == 0, "Config: invalid raffle winners count"); @@ -56,6 +70,9 @@ abstract contract Config { _raffleWinnersCount = raffleWinnersCount_; _reservePrice = reservePrice_; _minBidIncrement = minBidIncrement_; + + require(bidVerifier_ != address(0), "Config: invalid verifier"); + _bidVerifier = bidVerifier_; } function biddingStartTime() external view returns (uint256) { @@ -85,4 +102,8 @@ abstract contract Config { function minBidIncrement() external view returns (uint256) { return _minBidIncrement; } + + function bidVerifier() external view returns (address) { + return _bidVerifier; + } } diff --git a/packages/contracts/contracts/libs/FeistelShuffle.sol b/packages/contracts/contracts/libs/FeistelShuffle.sol new file mode 100644 index 000000000..1450fd27a --- /dev/null +++ b/packages/contracts/contracts/libs/FeistelShuffle.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.10; + +/// @title FeistelShuffle (Reference) +/// @author kevincharm +/// @notice Lazy shuffling using generalised Feistel ciphers. +library FeistelShuffle { + /// @notice Integer sqrt (rounding down), adapted from uniswap/v2-core + /// @param s integer to sqrt + /// @return z sqrt(s), rounding to zero + function sqrt(uint256 s) private pure returns (uint256 z) { + if (s > 3) { + z = s; + uint256 x = s / 2 + 1; + while (x < z) { + z = x; + x = (s / x + x) / 2; + } + } else if (s != 0) { + z = 1; + } + } + + /// @notice Feistel round function + /// @param x index of element in the list + /// @param i hash iteration index + /// @param seed random seed + /// @param modulus cardinality of list + /// @return hashed hash of x (mod `modulus`) + function f( + uint256 x, + uint256 i, + uint256 seed, + uint256 modulus + ) private pure returns (uint256 hashed) { + return uint256(keccak256(abi.encodePacked(x, i, seed, modulus))); + } + + /// @notice Next perfect square + /// @param n Number to get next perfect square of, unless it's already a + /// perfect square. + function nextPerfectSquare(uint256 n) private pure returns (uint256) { + uint256 sqrtN = sqrt(n); + if (sqrtN ** 2 == n) { + return n; + } + return (sqrtN + 1) ** 2; + } + + /// @notice Compute a Feistel shuffle mapping for index `x` + /// @param x index of element in the list + /// @param domain Number of elements in the list + /// @param seed Random seed; determines the permutation + /// @param rounds Number of Feistel rounds to perform + /// @return resulting shuffled index + function shuffle( + uint256 x, + uint256 domain, + uint256 seed, + uint256 rounds + ) internal pure returns (uint256) { + require(domain != 0, "modulus must be > 0"); + require(x < domain, "x too large"); + require((rounds & 1) == 0, "rounds must be even"); + + uint256 h = sqrt(nextPerfectSquare(domain)); + do { + uint256 L = x % h; + uint256 R = x / h; + for (uint256 i = 0; i < rounds; ++i) { + uint256 nextR = (L + f(R, i, seed, domain)) % h; + L = R; + R = nextR; + } + x = h * R + L; + } while (x >= domain); + return x; + } + + /// @notice Compute the inverse Feistel shuffle mapping for the shuffled + /// index `xPrime` + /// @param xPrime shuffled index of element in the list + /// @param domain Number of elements in the list + /// @param seed Random seed; determines the permutation + /// @param rounds Number of Feistel rounds that was performed in the + /// original shuffle. + /// @return resulting shuffled index + function deshuffle( + uint256 xPrime, + uint256 domain, + uint256 seed, + uint256 rounds + ) internal pure returns (uint256) { + require(domain != 0, "modulus must be > 0"); + require(xPrime < domain, "x too large"); + require((rounds & 1) == 0, "rounds must be even"); + + uint256 h = sqrt(nextPerfectSquare(domain)); + do { + uint256 L = xPrime % h; + uint256 R = xPrime / h; + for (uint256 i = 0; i < rounds; ++i) { + uint256 nextL = (R + + h - + (f(L, rounds - i - 1, seed, domain) % h)) % h; + R = L; + L = nextL; + } + xPrime = h * R + L; + } while (xPrime >= domain); + return xPrime; + } +} diff --git a/packages/contracts/contracts/libs/FeistelShuffleOptimised.sol b/packages/contracts/contracts/libs/FeistelShuffleOptimised.sol new file mode 100644 index 000000000..520359dfc --- /dev/null +++ b/packages/contracts/contracts/libs/FeistelShuffleOptimised.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.10; + +/// @title FeistelShuffleOptimised +/// @author kevincharm +/// @notice Feistel shuffle implemented in Yul. +library FeistelShuffleOptimised { + error InvalidInputs(); + + /// @notice Compute a Feistel shuffle mapping for index `x` + /// @param x index of element in the list + /// @param domain Number of elements in the list + /// @param seed Random seed; determines the permutation + /// @param rounds Number of Feistel rounds to perform + /// @return resulting shuffled index + function shuffle( + uint256 x, + uint256 domain, + uint256 seed, + uint256 rounds + ) internal pure returns (uint256) { + // (domain != 0): domain must be non-zero (value of 1 also doesn't really make sense) + // (xPrime < domain): index to be permuted must lie within the domain of [0, domain) + // (rounds is even): we only handle even rounds to make the code simpler + if (domain == 0 || x >= domain || rounds & 1 == 1) { + revert InvalidInputs(); + } + + assembly { + // Calculate sqrt(s) using Babylonian method + function sqrt(s) -> z { + switch gt(s, 3) + // if (s > 3) + case 1 { + z := s + let r := add(div(s, 2), 1) + for { + + } lt(r, z) { + + } { + z := r + r := div(add(div(s, r), r), 2) + } + } + default { + if and(not(iszero(s)), 1) { + // else if (s != 0) + z := 1 + } + } + } + + // nps <- nextPerfectSquare(domain) + let sqrtN := sqrt(domain) + let nps + switch eq(exp(sqrtN, 2), domain) + case 1 { + nps := domain + } + default { + let sqrtN1 := add(sqrtN, 1) + // pre-check for square overflow + if gt(sqrtN1, sub(exp(2, 128), 1)) { + // overflow + revert(0, 0) + } + nps := exp(sqrtN1, 2) + } + // h <- sqrt(nps) + let h := sqrt(nps) + // Allocate scratch memory for inputs to keccak256 + let packed := mload(0x40) + mstore(0x40, add(packed, 0x80)) // 128B + // When calculating hashes for Feistel rounds, seed and domain + // do not change. So we can set them here just once. + mstore(add(packed, 0x40), seed) + mstore(add(packed, 0x60), domain) + // Loop until x < domain + for { + + } 1 { + + } { + let L := mod(x, h) + let R := div(x, h) + // Loop for desired number of rounds + for { + let i := 0 + } lt(i, rounds) { + i := add(i, 1) + } { + // Load R and i for next keccak256 round + mstore(packed, R) + mstore(add(packed, 0x20), i) + // roundHash <- keccak256([R, i, seed, domain]) + let roundHash := keccak256(packed, 0x80) + // nextR <- (L + roundHash) % h + let nextR := mod(add(L, roundHash), h) + L := R + R := nextR + } + // x <- h * R + L + x := add(mul(h, R), L) + if lt(x, domain) { + break + } + } + } + return x; + } + + /// @notice Compute the inverse Feistel shuffle mapping for the shuffled + /// index `xPrime` + /// @param xPrime shuffled index of element in the list + /// @param domain Number of elements in the list + /// @param seed Random seed; determines the permutation + /// @param rounds Number of Feistel rounds that was performed in the + /// original shuffle. + /// @return resulting shuffled index + function deshuffle( + uint256 xPrime, + uint256 domain, + uint256 seed, + uint256 rounds + ) internal pure returns (uint256) { + // (domain != 0): domain must be non-zero (value of 1 also doesn't really make sense) + // (xPrime < domain): index to be permuted must lie within the domain of [0, domain) + // (rounds is even): we only handle even rounds to make the code simpler + if (domain == 0 || xPrime >= domain || rounds & 1 == 1) { + revert InvalidInputs(); + } + + assembly { + // Calculate sqrt(s) using Babylonian method + function sqrt(s) -> z { + switch gt(s, 3) + // if (s > 3) + case 1 { + z := s + let r := add(div(s, 2), 1) + for { + + } lt(r, z) { + + } { + z := r + r := div(add(div(s, r), r), 2) + } + } + default { + if and(not(iszero(s)), 1) { + // else if (s != 0) + z := 1 + } + } + } + + // nps <- nextPerfectSquare(domain) + let sqrtN := sqrt(domain) + let nps + switch eq(exp(sqrtN, 2), domain) + case 1 { + nps := domain + } + default { + let sqrtN1 := add(sqrtN, 1) + // pre-check for square overflow + if gt(sqrtN1, sub(exp(2, 128), 1)) { + // overflow + revert(0, 0) + } + nps := exp(sqrtN1, 2) + } + // h <- sqrt(nps) + let h := sqrt(nps) + // Allocate scratch memory for inputs to keccak256 + let packed := mload(0x40) + mstore(0x40, add(packed, 0x80)) // 128B + // When calculating hashes for Feistel rounds, seed and domain + // do not change. So we can set them here just once. + mstore(add(packed, 0x40), seed) + mstore(add(packed, 0x60), domain) + // Loop until x < domain + for { + + } 1 { + + } { + let L := mod(xPrime, h) + let R := div(xPrime, h) + // Loop for desired number of rounds + for { + let i := 0 + } lt(i, rounds) { + i := add(i, 1) + } { + // Load L and i for next keccak256 round + mstore(packed, L) + mstore(add(packed, 0x20), sub(sub(rounds, i), 1)) + // roundHash <- keccak256([L, rounds - i - 1, seed, domain]) + // NB: extra arithmetic to avoid underflow + let roundHash := mod(keccak256(packed, 0x80), h) + // nextL <- (R - roundHash) % h + // NB: extra arithmetic to avoid underflow + let nextL := mod(sub(add(R, h), roundHash), h) + R := L + L := nextL + } + // x <- h * R + L + xPrime := add(mul(h, R), L) + if lt(xPrime, domain) { + break + } + } + } + return xPrime; + } +} diff --git a/packages/contracts/contracts/libs/VRFRequester.sol b/packages/contracts/contracts/libs/VRFRequester.sol new file mode 100644 index 000000000..a810c007e --- /dev/null +++ b/packages/contracts/contracts/libs/VRFRequester.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; +import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol"; +import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; + +/// @title VRFRequester +/// @notice Consume Chainlink's subscription-managed VRFv2 wrapper to return a +/// random number. +abstract contract VRFRequester is VRFConsumerBaseV2 { + struct VRFRequesterParams { + address vrfCoordinator; + address linkToken; + uint256 linkPremium; + bytes32 gasLaneKeyHash; + uint32 callbackGasLimit; + uint16 minConfirmations; + uint64 subId; + } + + /// @notice VRF Coordinator (V2) + /// @dev https://docs.chain.link/vrf/v2/subscription/supported-networks + address public immutable vrfCoordinator; + /// @notice LINK token (make sure it's the ERC-677 one) + /// @dev PegSwap: https://pegswap.chain.link + address public immutable linkToken; + /// @notice LINK token unit + uint256 public immutable juels; + /// @dev VRF Coordinator LINK premium per request + uint256 public immutable linkPremium; + /// @notice Each gas lane has a different key hash; each gas lane + /// determines max gwei that will be used for the callback + bytes32 public immutable gasLaneKeyHash; + /// @notice Absolute gas limit for callbacks + uint32 public immutable callbackGasLimit; + /// @notice Minimum number of block confirmations before VRF fulfilment + uint16 public immutable minConfirmations; + /// @notice VRF subscription ID; created during deployment + uint64 public subId; + /// @notice Inflight requestId + uint256 public requestId; + + constructor(VRFRequesterParams memory params) VRFConsumerBaseV2(params.vrfCoordinator) { + vrfCoordinator = params.vrfCoordinator; + linkToken = params.linkToken; + juels = 10 ** LinkTokenInterface(params.linkToken).decimals(); + linkPremium = params.linkPremium; + gasLaneKeyHash = params.gasLaneKeyHash; + callbackGasLimit = params.callbackGasLimit; + minConfirmations = params.minConfirmations; + // NB: This contract must be added as a consumer to this subscription + subId = params.subId; + } + + /// @notice Update VRF subscription id + /// @param newSubId New subscription id + function _updateSubId(uint64 newSubId) internal { + subId = newSubId; + } + + /// @notice Request a random number + function _getRandomNumber() internal returns (uint256) { + require(requestId == 0, "Request already inflight"); + uint256 requestId_ = VRFCoordinatorV2Interface(vrfCoordinator).requestRandomWords( + gasLaneKeyHash, + subId, + minConfirmations, + callbackGasLimit, + 1 + ); + requestId = requestId_; + return requestId_; + } + + /// @notice Callback to receive a random number from the VRF fulfiller + /// @dev Override this function + /// @param randomNumber Random number + function _receiveRandomNumber(uint256 randomNumber) internal virtual {} + + /// @notice Callback function used by VRF Coordinator + /// @dev DO NOT OVERRIDE! + function fulfillRandomWords(uint256 requestId_, uint256[] memory randomness) internal override { + require(requestId_ == requestId, "Unexpected requestId"); + require(randomness.length > 0, "Unexpected empty randomness"); + requestId = 0; + _receiveRandomNumber(randomness[0]); + } +} diff --git a/packages/contracts/contracts/mocks/AuctionRaffleMock.sol b/packages/contracts/contracts/mocks/AuctionRaffleMock.sol index 6479338f5..f34c08285 100644 --- a/packages/contracts/contracts/mocks/AuctionRaffleMock.sol +++ b/packages/contracts/contracts/mocks/AuctionRaffleMock.sol @@ -7,25 +7,9 @@ import "../AuctionRaffle.sol"; contract AuctionRaffleMock is AuctionRaffle { constructor( address initialOwner, - uint256 biddingStartTime, - uint256 biddingEndTime, - uint256 claimingEndTime, - uint256 auctionWinnersCount, - uint256 raffleWinnersCount, - uint256 reservePrice, - uint256 minBidIncrement - ) - AuctionRaffle( - initialOwner, - biddingStartTime, - biddingEndTime, - claimingEndTime, - auctionWinnersCount, - raffleWinnersCount, - reservePrice, - minBidIncrement - ) - {} + ConfigParams memory configParams, + VRFRequesterParams memory vrfRequesterParams + ) AuctionRaffle(initialOwner, configParams, vrfRequesterParams) {} function getHeap() external view returns (uint256[] memory) { return _heap; diff --git a/packages/contracts/contracts/mocks/MockLinkToken.sol b/packages/contracts/contracts/mocks/MockLinkToken.sol new file mode 100644 index 000000000..0cd71d9bb --- /dev/null +++ b/packages/contracts/contracts/mocks/MockLinkToken.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; + +import {LinkToken} from "@chainlink/contracts/src/v0.8/shared/token/ERC677/LinkToken.sol"; + +contract MockLinkToken is LinkToken {} diff --git a/packages/contracts/contracts/mocks/VRFCoordinatorV2Mock.sol b/packages/contracts/contracts/mocks/VRFCoordinatorV2Mock.sol new file mode 100644 index 000000000..7e648035e --- /dev/null +++ b/packages/contracts/contracts/mocks/VRFCoordinatorV2Mock.sol @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: MIT +// A mock for testing code that relies on VRFCoordinatorV2. +pragma solidity ^0.8.4; + +import "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; +import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; +import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol"; + +contract VRFCoordinatorV2Mock is VRFCoordinatorV2Interface { + uint96 public immutable BASE_FEE; + uint96 public immutable GAS_PRICE_LINK; + uint16 public immutable MAX_CONSUMERS = 100; + + error InvalidSubscription(); + error InsufficientBalance(); + error MustBeSubOwner(address owner); + error TooManyConsumers(); + error InvalidConsumer(); + error InvalidRandomWords(); + + event RandomWordsRequested( + bytes32 indexed keyHash, + uint256 requestId, + uint256 preSeed, + uint64 indexed subId, + uint16 minimumRequestConfirmations, + uint32 callbackGasLimit, + uint32 numWords, + address indexed sender + ); + event RandomWordsFulfilled(uint256 indexed requestId, uint256 outputSeed, uint96 payment, bool success); + event SubscriptionCreated(uint64 indexed subId, address owner); + event SubscriptionFunded(uint64 indexed subId, uint256 oldBalance, uint256 newBalance); + event SubscriptionCanceled(uint64 indexed subId, address to, uint256 amount); + event ConsumerAdded(uint64 indexed subId, address consumer); + event ConsumerRemoved(uint64 indexed subId, address consumer); + + uint64 s_currentSubId; + uint256 s_nextRequestId = 1; + uint256 s_nextPreSeed = 100; + struct Subscription { + address owner; + uint96 balance; + } + mapping(uint64 => Subscription) s_subscriptions; /* subId */ /* subscription */ + mapping(uint64 => address[]) s_consumers; /* subId */ /* consumers */ + + struct Request { + uint64 subId; + uint32 callbackGasLimit; + uint32 numWords; + } + mapping(uint256 => Request) s_requests; /* requestId */ /* request */ + + constructor(uint96 _baseFee, uint96 _gasPriceLink) { + BASE_FEE = _baseFee; + GAS_PRICE_LINK = _gasPriceLink; + } + + function consumerIsAdded(uint64 _subId, address _consumer) public view returns (bool) { + address[] memory consumers = s_consumers[_subId]; + for (uint256 i = 0; i < consumers.length; i++) { + if (consumers[i] == _consumer) { + return true; + } + } + return false; + } + + modifier onlyValidConsumer(uint64 _subId, address _consumer) { + if (!consumerIsAdded(_subId, _consumer)) { + revert InvalidConsumer(); + } + _; + } + + /** + * @notice fulfillRandomWords fulfills the given request, sending the random words to the supplied + * @notice consumer. + * + * @dev This mock uses a simplified formula for calculating payment amount and gas usage, and does + * @dev not account for all edge cases handled in the real VRF coordinator. When making requests + * @dev against the real coordinator a small amount of additional LINK is required. + * + * @param _requestId the request to fulfill + * @param _consumer the VRF randomness consumer to send the result to + */ + function fulfillRandomWords( + uint256 _requestId, + address _consumer, + uint256[] calldata randomness + ) external { + fulfillRandomWordsWithOverride(_requestId, _consumer, randomness); + } + + /** + * @notice fulfillRandomWordsWithOverride allows the user to pass in their own random words. + * + * @param _requestId the request to fulfill + * @param _consumer the VRF randomness consumer to send the result to + * @param _words user-provided random words + */ + function fulfillRandomWordsWithOverride( + uint256 _requestId, + address _consumer, + uint256[] memory _words + ) public { + uint256 startGas = gasleft(); + if (s_requests[_requestId].subId == 0) { + revert("nonexistent request"); + } + Request memory req = s_requests[_requestId]; + + if (_words.length == 0) { + _words = new uint256[](req.numWords); + for (uint256 i = 0; i < req.numWords; i++) { + _words[i] = uint256(keccak256(abi.encode(_requestId, i))); + } + } else if (_words.length != req.numWords) { + revert InvalidRandomWords(); + } + + VRFConsumerBaseV2 v; + bytes memory callReq = abi.encodeWithSelector(v.rawFulfillRandomWords.selector, _requestId, _words); + (bool success, ) = _consumer.call{gas: req.callbackGasLimit}(callReq); + + uint96 payment = uint96(BASE_FEE + ((startGas - gasleft()) * GAS_PRICE_LINK)); + if (s_subscriptions[req.subId].balance < payment) { + revert InsufficientBalance(); + } + s_subscriptions[req.subId].balance -= payment; + delete (s_requests[_requestId]); + emit RandomWordsFulfilled(_requestId, _requestId, payment, success); + } + + /** + * @notice fundSubscription allows funding a subscription with an arbitrary amount for testing. + * + * @param _subId the subscription to fund + * @param _amount the amount to fund + */ + function fundSubscription(uint64 _subId, uint96 _amount) public { + if (s_subscriptions[_subId].owner == address(0)) { + revert InvalidSubscription(); + } + uint96 oldBalance = s_subscriptions[_subId].balance; + s_subscriptions[_subId].balance += _amount; + emit SubscriptionFunded(_subId, oldBalance, oldBalance + _amount); + } + + function requestRandomWords( + bytes32 _keyHash, + uint64 _subId, + uint16 _minimumRequestConfirmations, + uint32 _callbackGasLimit, + uint32 _numWords + ) external override onlyValidConsumer(_subId, msg.sender) returns (uint256) { + if (s_subscriptions[_subId].owner == address(0)) { + revert InvalidSubscription(); + } + + uint256 requestId = s_nextRequestId++; + uint256 preSeed = s_nextPreSeed++; + + s_requests[requestId] = Request({subId: _subId, callbackGasLimit: _callbackGasLimit, numWords: _numWords}); + + emit RandomWordsRequested( + _keyHash, + requestId, + preSeed, + _subId, + _minimumRequestConfirmations, + _callbackGasLimit, + _numWords, + msg.sender + ); + return requestId; + } + + function createSubscription() external override returns (uint64 _subId) { + s_currentSubId++; + s_subscriptions[s_currentSubId] = Subscription({owner: msg.sender, balance: 0}); + emit SubscriptionCreated(s_currentSubId, msg.sender); + return s_currentSubId; + } + + function getSubscription(uint64 _subId) + external + view + override + returns ( + uint96 balance, + uint64 reqCount, + address owner, + address[] memory consumers + ) + { + if (s_subscriptions[_subId].owner == address(0)) { + revert InvalidSubscription(); + } + return (s_subscriptions[_subId].balance, 0, s_subscriptions[_subId].owner, s_consumers[_subId]); + } + + function cancelSubscription(uint64 _subId, address _to) external virtual override onlySubOwner(_subId) { + emit SubscriptionCanceled(_subId, _to, s_subscriptions[_subId].balance); + delete (s_subscriptions[_subId]); + } + + modifier onlySubOwner(uint64 _subId) { + address owner = s_subscriptions[_subId].owner; + if (owner == address(0)) { + revert InvalidSubscription(); + } + if (msg.sender != owner) { + revert MustBeSubOwner(owner); + } + _; + } + + function getRequestConfig() + external + pure + override + returns ( + uint16, + uint32, + bytes32[] memory + ) + { + return (3, 2000000, new bytes32[](0)); + } + + function addConsumer(uint64 _subId, address _consumer) external override onlySubOwner(_subId) { + if (s_consumers[_subId].length == MAX_CONSUMERS) { + revert TooManyConsumers(); + } + + if (consumerIsAdded(_subId, _consumer)) { + return; + } + + s_consumers[_subId].push(_consumer); + emit ConsumerAdded(_subId, _consumer); + } + + function removeConsumer(uint64 _subId, address _consumer) + external + override + onlySubOwner(_subId) + onlyValidConsumer(_subId, _consumer) + { + address[] storage consumers = s_consumers[_subId]; + for (uint256 i = 0; i < consumers.length; i++) { + if (consumers[i] == _consumer) { + address last = consumers[consumers.length - 1]; + consumers[i] = last; + consumers.pop(); + break; + } + } + + emit ConsumerRemoved(_subId, _consumer); + } + + function getConfig() + external + view + returns ( + uint16 minimumRequestConfirmations, + uint32 maxGasLimit, + uint32 stalenessSeconds, + uint32 gasAfterPaymentCalculation + ) + { + return (4, 2_500_000, 2_700, 33285); + } + + function getFeeConfig() + external + view + returns ( + uint32 fulfillmentFlatFeeLinkPPMTier1, + uint32 fulfillmentFlatFeeLinkPPMTier2, + uint32 fulfillmentFlatFeeLinkPPMTier3, + uint32 fulfillmentFlatFeeLinkPPMTier4, + uint32 fulfillmentFlatFeeLinkPPMTier5, + uint24 reqsForTier2, + uint24 reqsForTier3, + uint24 reqsForTier4, + uint24 reqsForTier5 + ) + { + return ( + 100000, // 0.1 LINK + 100000, // 0.1 LINK + 100000, // 0.1 LINK + 100000, // 0.1 LINK + 100000, // 0.1 LINK + 0, + 0, + 0, + 0 + ); + } + + function getFallbackWeiPerUnitLink() external view returns (int256) { + return 4000000000000000; // 0.004 Ether + } + + function requestSubscriptionOwnerTransfer(uint64 _subId, address _newOwner) external pure override { + revert("not implemented"); + } + + function acceptSubscriptionOwnerTransfer(uint64 _subId) external pure override { + revert("not implemented"); + } + + function pendingRequestExists(uint64 subId) public view override returns (bool) { + revert("not implemented"); + } +} diff --git a/packages/contracts/contracts/mocks/VRFCoordinatorV2MockWithERC677.sol b/packages/contracts/contracts/mocks/VRFCoordinatorV2MockWithERC677.sol new file mode 100644 index 000000000..f4c98a2f0 --- /dev/null +++ b/packages/contracts/contracts/mocks/VRFCoordinatorV2MockWithERC677.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; + +import {VRFCoordinatorV2Mock} from "./VRFCoordinatorV2Mock.sol"; +import {IERC677Receiver} from "@chainlink/contracts/src/v0.8/shared/interfaces/IERC677Receiver.sol"; +import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; + +contract VRFCoordinatorV2MockWithERC677 is VRFCoordinatorV2Mock, IERC677Receiver { + address public immutable LINK; + + bool internal __entered; + + modifier nonReentrant() { + require(!__entered, "Re-entrance not allowed"); + __entered = true; + _; + __entered = false; + } + + error OnlyCallableFromLink(); + error InvalidCalldata(); + + constructor( + uint96 _baseFee, + uint96 _gasPriceLink, + address linkToken + ) VRFCoordinatorV2Mock(_baseFee, _gasPriceLink) { + LINK = linkToken; + } + + function cancelSubscription(uint64 _subId, address _to) external override onlySubOwner(_subId) { + uint256 balance = s_subscriptions[_subId].balance; + emit SubscriptionCanceled(_subId, _to, balance); + delete (s_subscriptions[_subId]); + LinkTokenInterface(LINK).transfer(_to, balance); + } + + function onTokenTransfer( + address, /* sender */ + uint256 amount, + bytes calldata data + ) external override nonReentrant { + if (msg.sender != address(LINK)) { + revert OnlyCallableFromLink(); + } + if (data.length != 32) { + revert InvalidCalldata(); + } + uint64 subId = abi.decode(data, (uint64)); + if (s_subscriptions[subId].owner == address(0)) { + revert InvalidSubscription(); + } + // We do not check that the msg.sender is the subscription owner, + // anyone can fund a subscription. + uint256 oldBalance = s_subscriptions[subId].balance; + s_subscriptions[subId].balance += uint96(amount); + emit SubscriptionFunded(subId, oldBalance, oldBalance + amount); + } +} diff --git a/packages/contracts/contracts/models/BidModel.sol b/packages/contracts/contracts/models/BidModel.sol index b3e64c9ff..a262d276f 100644 --- a/packages/contracts/contracts/models/BidModel.sol +++ b/packages/contracts/contracts/models/BidModel.sol @@ -10,8 +10,9 @@ abstract contract BidModel { struct Bid { uint256 bidderID; uint256 amount; - WinType winType; + bool isAuctionWinner; bool claimed; + uint240 raffleParticipantIndex; } struct BidWithAddress { diff --git a/packages/contracts/contracts/models/StateModel.sol b/packages/contracts/contracts/models/StateModel.sol index 9907430cd..1679068b1 100644 --- a/packages/contracts/contracts/models/StateModel.sol +++ b/packages/contracts/contracts/models/StateModel.sol @@ -7,6 +7,7 @@ pragma solidity 0.8.10; * @author TrueFi Engineering team */ abstract contract StateModel { + // NB: This is ordered according to the expected sequence. Do not reorder! enum State { AWAITING_BIDDING, BIDDING_OPEN, diff --git a/packages/contracts/contracts/test/FeistelShuffleConsumer.sol b/packages/contracts/contracts/test/FeistelShuffleConsumer.sol new file mode 100644 index 000000000..488445ae9 --- /dev/null +++ b/packages/contracts/contracts/test/FeistelShuffleConsumer.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.10; + +import {FeistelShuffle} from "../libs/FeistelShuffle.sol"; +import {FeistelShuffleOptimised} from "../libs/FeistelShuffleOptimised.sol"; + +contract FeistelShuffleConsumer { + function shuffle( + uint256 x, + uint256 domain, + uint256 seed, + uint256 rounds + ) public pure returns (uint256) { + return FeistelShuffle.shuffle(x, domain, seed, rounds); + } + + function deshuffle( + uint256 xPrime, + uint256 domain, + uint256 seed, + uint256 rounds + ) public pure returns (uint256) { + return FeistelShuffle.deshuffle(xPrime, domain, seed, rounds); + } + + function shuffle__OPT( + uint256 x, + uint256 domain, + uint256 seed, + uint256 rounds + ) public pure returns (uint256) { + return FeistelShuffleOptimised.shuffle(x, domain, seed, rounds); + } + + function deshuffle__OPT( + uint256 xPrime, + uint256 domain, + uint256 seed, + uint256 rounds + ) public pure returns (uint256) { + return FeistelShuffleOptimised.deshuffle(xPrime, domain, seed, rounds); + } +} diff --git a/packages/contracts/contracts/verifier/IVerifier.sol b/packages/contracts/contracts/verifier/IVerifier.sol new file mode 100644 index 000000000..0ec6558cc --- /dev/null +++ b/packages/contracts/contracts/verifier/IVerifier.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; + +interface IVerifier { + function verify(bytes memory payload, bytes memory proof) external; +} diff --git a/packages/contracts/contracts/verifier/ScoreAttestationVerifier.sol b/packages/contracts/contracts/verifier/ScoreAttestationVerifier.sol new file mode 100644 index 000000000..2e7d4d422 --- /dev/null +++ b/packages/contracts/contracts/verifier/ScoreAttestationVerifier.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8; + +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IVerifier} from "./IVerifier.sol"; + +contract ScoreAttestationVerifier is IVerifier, EIP712, Ownable { + /// @notice EIP-712 typehash for Score message + bytes32 public constant EIP712_SCORE_TYPEHASH = keccak256("Score(address subject,uint256 score)"); + + /// @notice Address of the authorised attestor + address public attestor; + /// @notice Minimum required GTC passport score (8 decimals) + uint256 public requiredScore; + + event RequiredScoreUpdated(uint256 oldScore, uint256 newScore); + event AttestorUpdated(address oldAttestor, address newAttestor); + + constructor( + string memory version, + address initialAttestor, + uint256 initialRequiredScore + ) EIP712("ScoreAttestationVerifier", version) Ownable() { + requiredScore = initialRequiredScore; + attestor = initialAttestor; + } + + function verify(bytes memory payload, bytes memory proof) external view { + (address subject, uint256 score) = abi.decode(payload, (address, uint256)); + bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(EIP712_SCORE_TYPEHASH, subject, score))); + address signer = ECDSA.recover(digest, proof); + require(signer == attestor, "Unauthorised attestor"); + require(score >= requiredScore, "Score too low"); + } + + function setRequiredScore(uint256 newRequiredScore) external onlyOwner { + emit RequiredScoreUpdated(requiredScore, newRequiredScore); + requiredScore = newRequiredScore; + } + + function setAttestor(address newAttestor) external onlyOwner { + emit AttestorUpdated(attestor, newAttestor); + attestor = newAttestor; + } +} diff --git a/packages/contracts/deployments.json b/packages/contracts/deployments.json index 79b6a07ae..05636e235 100644 --- a/packages/contracts/deployments.json +++ b/packages/contracts/deployments.json @@ -1,16 +1,8 @@ { - "arbitrum_rinkeby": { - "auctionRaffle": { - "txHash": "0xcf8d36d310dab30aae2d8502d9a29c4239015138986a54b92c82b29dc58b41e6", - "address": "0x2d7435A78010bB613E1f22E0A8018733dd0C1Cfe", - "multisig": false - } + "42161": { + "address": "0x1E7aC276CBdae55689Df1d99108d69Fff444cB88" }, - "arbitrum": { - "auctionRaffle": { - "txHash": "0x0c0d784575a9bd91d256ae2c51b44190be27bf8da3a6c8f8fc2c74c5151022a9", - "address": "0xF53d383525117d1f51BF234966E39bD1508a5948", - "multisig": false - } + "421614": { + "address": "0x2cc7f85730052dA7389e276115975a5dB1e7A668" } -} +} \ No newline at end of file diff --git a/packages/contracts/docs/auction_raffle_passport_msd.jpg b/packages/contracts/docs/auction_raffle_passport_msd.jpg new file mode 100644 index 000000000..19971ccd0 Binary files /dev/null and b/packages/contracts/docs/auction_raffle_passport_msd.jpg differ diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index dd77b4c3a..d7d38e54d 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -8,6 +8,8 @@ import 'scripts/tasks/auctionRaffle/ethereumTasks' import 'scripts/tasks/auctionRaffle/hardhatTasks' import 'scripts/tasks/auctionRaffle/rinkebyTasks' import 'scripts/tasks/auctionRaffle/arbitrumTasks' +import 'hardhat-tracer' +import '@nomicfoundation/hardhat-verify' import mocharc from './.mocharc.json' import compiler from './.compiler.json' @@ -20,12 +22,12 @@ module.exports = { paths: { sources: './contracts', artifacts: './build', - cache: './cache' + cache: './cache', }, abiExporter: { path: './build', flat: true, - spacing: 2 + spacing: 2, }, defaultNetwork: 'hardhat', networks: { @@ -33,30 +35,39 @@ module.exports = { initialDate: '2022-01-01T00:00:00', allowUnlimitedContractSize: true, accounts: { - count: 120 + count: 120, }, }, - rinkeby: { - url: 'https://rinkeby.arbitrum.io/rpc', - accounts: [process.env.DEPLOYER || zeroPrivateKey] + arbSepolia: { + url: 'https://sepolia-rollup.arbitrum.io/rpc', + accounts: [process.env.DEPLOYER || zeroPrivateKey], }, ethereum: { - url: 'https://eth-mainnet.alchemyapi.io/v2/j_dccrP25UjZv5uYxh1mcjEl5o8nWZaf' + url: 'https://eth-mainnet.alchemyapi.io/v2/j_dccrP25UjZv5uYxh1mcjEl5o8nWZaf', }, arbitrum: { url: `https://arb1.arbitrum.io/rpc`, - accounts: [process.env.DEPLOYER || zeroPrivateKey] - } + accounts: [process.env.DEPLOYER || zeroPrivateKey], + }, }, typechain: { outDir: 'build/types', - target: 'ethers-v5' + target: 'ethers-v5', }, solidity: { - compilers: [compiler] + compilers: [compiler], }, mocha: { ...mocharc, - timeout: 400000 - } + timeout: 400000, + }, + etherscan: { + apiKey: { + arbitrumSepolia: process.env.ARBISCAN_API_KEY, + arbitrumOne: process.env.ARBISCAN_API_KEY, + }, + }, + sourcify: { + enabled: true, + }, } diff --git a/packages/contracts/package.json b/packages/contracts/package.json index eba3a3b88..856f7f1c4 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -5,47 +5,46 @@ "main": "build/types/index.ts", "types": "build/index.ts", "scripts": { + "preinstall": "npx only-allow pnpm", "clean": "rm -rf ./build", "typecheck": "tsc --noEmit", "lint:sol": "solhint 'contracts/**/*.sol' && prettylint 'contracts/**/*.sol'", "lint:ts": "eslint '{test,scripts}/**/*.ts' -c .eslintrc.typescript.js --cache", - "lint": "yarn run lint:sol && yarn run lint:ts", - "lint:fix": "prettier 'contracts/**/*.sol' --write --loglevel error && yarn run lint:ts --fix", + "lint": "pnpm run lint:sol && pnpm run lint:ts", + "lint:fix": "prettier 'contracts/**/*.sol' --write --loglevel error && pnpm run lint:ts --fix", "build:sol": "waffle .waffle.json", "build:types": "typechain --target ethers-v5 --outDir build/types 'build/*.json' > /dev/null && echo 'Typechain generated'", - "build:waffle": "yarn run clean && yarn run build:sol && yarn run build:types && bash ./scripts/indexBuild.sh", + "build:waffle": "pnpm run clean && pnpm run build:sol && pnpm run build:types && bash ./scripts/indexBuild.sh", "build": "hardhat compile && bash ./scripts/indexBuild.sh", - "build-test": "yarn run build && yarn run test", + "build-test": "pnpm run build && pnpm run test", "test": "mocha 'test/**/*.test.ts'", "test:e2e": "mocha 'test/**/*.e2e.ts'", "test:hardhat": "hardhat test", "test:gas-reporter": "REPORT_GAS=true hardhat test", - "verify": "bash spec/verify.sh -f", "verify:sanity": "bash spec/verify.sh -sf", - "node:run": "hardhat run scripts/node/run.ts", "node:increase-time": "hardhat --network localhost increase-time", "node:accounts": "hardhat --network localhost accounts", - "hardhat:bid": "hardhat --network localhost bid", "hardhat:bid-random": "hardhat --network localhost bid-random", "hardhat:settle-auction": "hardhat --network localhost settle-auction", "hardhat:settle-raffle": "hardhat --network localhost settle-raffle", - "hardhat:settle": "yarn node:increase-time && yarn hardhat:settle-auction && yarn hardhat:settle-raffle", - + "hardhat:fulfill-vrf": "hardhat --network localhost fulfill-vrf", + "hardhat:transfer-eth": "hardhat --network localhost transfer-eth", + "hardhat:settle": "pnpm node:increase-time && pnpm hardhat:settle-auction && pnpm hardhat:settle-raffle && pnpm hardhat:fulfill-vrf", "rinkeby:generate-dotenv": "hardhat generate-dotenv", "rinkeby:transfer-ether": "hardhat --network rinkeby transfer-ether", "rinkeby:init-bids": "hardhat --network rinkeby init-bids", - "ethereum:generate-random-numbers": "hardhat --network ethereum generate-random-numbers", - "arbitrum:withdrawals-stats": "hardhat --network arbitrum withdrawals-stats", - - "mars:deploy": "bash scripts/mars/marsDeploy.sh scripts/mars/deploy.ts -n arbitrum" + "deploy:arb-sepolia": "hardhat run --network arbSepolia scripts/deploy.ts", + "deploy:arb1": "hardhat --network arbitrum run scripts/deploy.ts" }, "devDependencies": { "@ethereum-waffle/chai": "^3.4.3", + "@kevincharm/gfc-fpe": "^1.1.0", + "@nomicfoundation/hardhat-verify": "^2.0.5", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-waffle": "^2.0.1", "@openzeppelin/contracts": "^4.4.0", @@ -62,8 +61,9 @@ "ethereum-mars": "^0.2.5", "ethereum-waffle": "^3.4.0", "ethers": "^5.6.2", - "hardhat": "^2.2.0", + "hardhat": "^2.19.4", "hardhat-gas-reporter": "^1.0.7", + "hardhat-tracer": "^2.7.0", "hardhat-typechain": "^0.3.5", "mocha": "^8.2.1", "prettier": "^2.4.1", @@ -75,5 +75,8 @@ "typechain": "^3.0.0", "typescript": "~4.5.4", "write-file-atomic": "^4.0.1" + }, + "dependencies": { + "@chainlink/contracts": "^0.8.0" } } diff --git a/packages/contracts/scripts/deploy.ts b/packages/contracts/scripts/deploy.ts new file mode 100644 index 000000000..71e75e86a --- /dev/null +++ b/packages/contracts/scripts/deploy.ts @@ -0,0 +1,11 @@ +import { deployAuctionRaffle } from './deployAuctionRaffle' + +deployAuctionRaffle() + .then(() => { + console.log('Done') + process.exit(0) + }) + .catch((err) => { + console.error(err) + process.exit(1) + }) diff --git a/packages/contracts/scripts/deployAuctionRaffle.ts b/packages/contracts/scripts/deployAuctionRaffle.ts new file mode 100644 index 000000000..9b99e0cf6 --- /dev/null +++ b/packages/contracts/scripts/deployAuctionRaffle.ts @@ -0,0 +1,100 @@ +import fs from 'fs' +import path from 'path' +import { ethers, run } from 'hardhat' +import { AuctionRaffle__factory, ScoreAttestationVerifier__factory } from '../build/types' +import { config, scoreAttestationVerifierConfig, vrfConfig } from './deploymentConfig' +import { BigNumberish } from 'ethers' + +export type RaffleConfig = Partial[1]> +export type VrfConfig = Partial[2]> + +// TODO: Ideally we use hh ignition, but it requires upgrading ethers. Can't get mars to work. +export async function deployAuctionRaffle(opts?: { + isLocalNetwork?: boolean + initialOwner?: string + scoreAttestationConfig?: { + version?: string + initialAttestor?: string + initialRequiredScore?: BigNumberish + } + raffleConfig?: RaffleConfig + vrfConfig?: VrfConfig +}) { + const chainId = await ethers.provider.getNetwork().then((network) => network.chainId) + const [deployer] = await ethers.getSigners() + + const deploymentsPath = path.resolve(__dirname, '../deployments.json') + const deployments = JSON.parse( + fs.readFileSync(deploymentsPath, { + encoding: 'utf-8', + }) + ) + if (!opts?.isLocalNetwork && deployments[chainId]) { + console.log(`Already deployed to chainId ${chainId}:`) + console.log(deployments[chainId]) + process.exit(1) + } + + // Deploy + const scoreAttestationVerifierArgs: Parameters = [ + opts?.scoreAttestationConfig.version || scoreAttestationVerifierConfig.version, + opts?.scoreAttestationConfig.initialAttestor || scoreAttestationVerifierConfig.initialAttestor, + opts?.scoreAttestationConfig.initialRequiredScore || scoreAttestationVerifierConfig.initialRequiredScore, + ] + const scoreAttestationVerifier = await new ScoreAttestationVerifier__factory(deployer).deploy( + ...scoreAttestationVerifierArgs + ) + console.log(`Deployed ScoreAttestationVerifier: ${scoreAttestationVerifier.address}`) + const auctionRaffleArgs: Parameters = [ + opts?.initialOwner || config.initialOwner, + { + biddingStartTime: config.biddingStartTime, + biddingEndTime: config.biddingEndTime, + claimingEndTime: config.claimingEndTime, + auctionWinnersCount: config.auctionWinnersCount, + raffleWinnersCount: config.raffleWinnersCount, + reservePrice: config.reservePrice, + minBidIncrement: config.minBidIncrement, + bidVerifier: scoreAttestationVerifier.address, + ...opts?.raffleConfig, + }, + { + ...vrfConfig, + ...opts?.vrfConfig, + }, + { + gasLimit: 10_000_000, + }, + ] + + const auctionRaffle = await new AuctionRaffle__factory(deployer).deploy(...auctionRaffleArgs) + console.log(`Deployed AuctionRaffle: ${auctionRaffle.address}`) + + if (!opts?.isLocalNetwork) { + // Record + deployments[chainId] = { + address: auctionRaffle.address, + } + fs.writeFileSync(deploymentsPath, JSON.stringify(deployments, null, 2), { + encoding: 'utf-8', + }) + + // Verify + await new Promise((resolve) => { + setTimeout(resolve, 30_000) + }) + await run('verify:verify', { + address: scoreAttestationVerifier.address, + constructorArguments: scoreAttestationVerifierArgs, + }) + await run('verify:verify', { + address: auctionRaffle.address, + constructorArguments: auctionRaffleArgs.slice(0, -1), + }) + } + + return { + auctionRaffle, + scoreAttestationVerifier, + } +} diff --git a/packages/contracts/scripts/deploymentConfig.ts b/packages/contracts/scripts/deploymentConfig.ts new file mode 100644 index 000000000..813add7e3 --- /dev/null +++ b/packages/contracts/scripts/deploymentConfig.ts @@ -0,0 +1,41 @@ +import { BigNumberish, utils } from 'ethers' +import { parseEther } from 'ethers/lib/utils' + +interface DeploymentConfig { + initialOwner: string + biddingStartTime: BigNumberish + biddingEndTime: BigNumberish + claimingEndTime: BigNumberish + auctionWinnersCount: BigNumberish + raffleWinnersCount: BigNumberish + reservePrice: BigNumberish + minBidIncrement: BigNumberish +} + +export const scoreAttestationVerifierConfig = { + version: '1', + initialAttestor: '0x527974D1270283BA158934b64f8a91C0E1F62024', + initialRequiredScore: 20, +} + +export const vrfConfig = { + // arb1, VRF 2.0 (not 2.5!) + vrfCoordinator: '0x41034678D6C633D8a95c75e1138A360a28bA15d1', + linkToken: '0xf97f4df75117a78c1a5a0dbb814af92458539fb4', + linkPremium: parseEther('0.005'), + gasLaneKeyHash: '0x68d24f9a037a649944964c2a1ebd0b2918f4a243d2a99701cc22b548cf2daff0', // 150 gwei + callbackGasLimit: 2_500_000, // maximum + minConfirmations: 1, // minimum + subId: 150, +} + +export const config: DeploymentConfig = { + initialOwner: '0x8AE57CE9eC11a53Ca655E02B36482C0cB406ACee', + biddingStartTime: 1718721000, // 2024-06-18T14:30:00Z + biddingEndTime: 1720569540, // 2024-07-09T23:59:00Z + claimingEndTime: 1722470340, // 2024-07-31T23:59:00Z + auctionWinnersCount: 20, + raffleWinnersCount: 184, + reservePrice: utils.parseEther('0.08'), + minBidIncrement: utils.parseEther('0.003'), +} diff --git a/packages/contracts/scripts/mars/deploy.ts b/packages/contracts/scripts/mars/deploy.ts deleted file mode 100644 index c12ad7ce9..000000000 --- a/packages/contracts/scripts/mars/deploy.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { contract, deploy } from 'ethereum-mars' -import { AuctionRaffle } from '../../build/artifacts' -import { config } from './deploymentConfig' - -deploy({ verify: true }, deployAuctionRaffle) - -function deployAuctionRaffle() { - contract(AuctionRaffle, [ - config.initialOwner, - config.biddingStartTime, - config.biddingEndTime, - config.claimingEndTime, - config.auctionWinnersCount, - config.raffleWinnersCount, - config.reservePrice, - config.minBidIncrement, - ], { gasLimit: 60_000_000 }, - ) -} diff --git a/packages/contracts/scripts/mars/deploymentConfig.ts b/packages/contracts/scripts/mars/deploymentConfig.ts deleted file mode 100644 index 2497c2642..000000000 --- a/packages/contracts/scripts/mars/deploymentConfig.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { AddressLike, NumberLike } from 'ethereum-mars' -import { utils } from 'ethers' - -interface DeploymentConfig { - initialOwner: AddressLike, - biddingStartTime: NumberLike, - biddingEndTime: NumberLike, - claimingEndTime: NumberLike, - auctionWinnersCount: NumberLike, - raffleWinnersCount: NumberLike, - reservePrice: NumberLike, - minBidIncrement: NumberLike, -} - -export const config: DeploymentConfig = { - initialOwner: '0x511ECC4c955626DDaD88f20493E39E71be8133B6', - biddingStartTime: 1657008000, // Jul 05 2022 08:00:00 UTC - biddingEndTime: 1657785540, // Jul 14 2022 07:59:00 UTC - claimingEndTime: 1673683140, // Jan 14 2023 07:59:00 UTC - auctionWinnersCount: 20, - raffleWinnersCount: 80, - reservePrice: utils.parseEther('0.25'), - minBidIncrement: utils.parseEther('0.01'), -} diff --git a/packages/contracts/scripts/mars/marsDeploy.sh b/packages/contracts/scripts/mars/marsDeploy.sh deleted file mode 100644 index 1ce518f7e..000000000 --- a/packages/contracts/scripts/mars/marsDeploy.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -set -eu - -# Setting Infura or Alchemy key to use for convenience here -export ALCHEMY_KEY="j_dccrP25UjZv5uYxh1mcjEl5o8nWZaf" - -# Setting Etherscan key to use for convenience here -export ETHERSCAN_KEY="FJG9XKWZC5ECE6T86IGM8XI35DME5KYU1E" - -# Consume the first argument as a path to the Mars deploy script. -# All other command line arguments get forwarded to Mars. -DEPLOY_SCRIPT="$1" -shift 1 - -network='arbitrum' -args="$@" - -while [[ "$@" ]]; do - case "$1" in - --network) - if [ "$2" ]; then - network="$2" - shift 1 - fi - ;; - -?) - # ignore - ;; - esac - shift 1 -done - -if [[ "$(git status --porcelain)" ]]; then - echo "Error: git working directory must be empty to run deploy script." - exit 1 -fi - -if [[ "$(git log --pretty=format:'%H' -n 1)" != "$(cat ./build/canary.hash)" ]]; then - echo "Error: Build canary does not match current commit hash. Please run yarn build." - exit 1 -fi - -# Skip prompt if PRIVATE_KEY variable already exists -if [[ -z "${PRIVATE_KEY:-}" ]]; then - # Prompt the user for a PRIVATE_KEY without echoing to bash output. - # Then export PRIVATE_KEY to an environment variable that won't get - # leaked to bash history. - # - # WARNING: environment variables are still leaked to the process table - # while a process is running, and hence visible in a call to `ps -E`. - echo "Enter a private key (0x{64 hex chars}) for contract deployment." - read -s -p "PRIVATE_KEY=" PRIVATE_KEY - export PRIVATE_KEY -fi - -# Log file name -network_log="-${network}" -target_file_name="$(basename -- ${DEPLOY_SCRIPT})" -target_log="-${target_file_name%.*}" -timestamp_log="-$(date +%s)" - -yarn mars -ts-node ${DEPLOY_SCRIPT} \ - --waffle-config ./.waffle.json \ - ${args} \ - --log "./cache/deploy${network_log}${target_log}${timestamp_log}.log" diff --git a/packages/contracts/scripts/node/config.ts b/packages/contracts/scripts/node/config.ts new file mode 100644 index 000000000..370153f91 --- /dev/null +++ b/packages/contracts/scripts/node/config.ts @@ -0,0 +1,7 @@ +import { utils } from 'ethers' +import { HOUR } from 'scripts/utils/consts' + +export const reservePrice = utils.parseEther('0.15') +export const minBidIncrement = utils.parseEther('0.01') +export const minStateDuration = 6 * HOUR +export const initialRequiredScore = 20 * 10 ** 8 // 20.0 diff --git a/packages/contracts/scripts/node/deploy.ts b/packages/contracts/scripts/node/deploy.ts deleted file mode 100644 index 372d691e8..000000000 --- a/packages/contracts/scripts/node/deploy.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Contract, utils } from 'ethers' -import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { HOUR } from 'scripts/utils/consts' -import { auctionRaffleArtifactName, multicallArtifactName } from 'scripts/utils/auctionRaffle' - -export const reservePrice = utils.parseEther('0.15') -export const minBidIncrement = utils.parseEther('0.01') -export const minStateDuration = 6 * HOUR - -export async function deploy(biddingStartTime: number, deployer: SignerWithAddress, hre: HardhatRuntimeEnvironment): Promise { - const auctionRaffle = await deployAuctionRaffle(biddingStartTime, deployer, hre) - - const multicallFactory = await hre.ethers.getContractFactory(multicallArtifactName) - const multicall = await multicallFactory.connect(deployer).deploy() - - console.log('\nAuctionRaffle address: ', auctionRaffle.address) - console.log('\nMulticall address: ', multicall.address) - - return auctionRaffle -} - -export async function deployAuctionRaffle(biddingStartTime: number, deployer: SignerWithAddress, hre: HardhatRuntimeEnvironment): Promise { - const biddingEndTime = biddingStartTime + minStateDuration - const claimingEndTime = biddingEndTime + minStateDuration - - const auctionRaffleFactory = await hre.ethers.getContractFactory(auctionRaffleArtifactName) - return auctionRaffleFactory.connect(deployer).deploy( - deployer.address, - biddingStartTime, - biddingEndTime, - claimingEndTime, - 20, - 80, - reservePrice, - minBidIncrement, - ) -} diff --git a/packages/contracts/scripts/node/run.ts b/packages/contracts/scripts/node/run.ts index 7ca1d6cb1..b194dfa6b 100644 --- a/packages/contracts/scripts/node/run.ts +++ b/packages/contracts/scripts/node/run.ts @@ -1,9 +1,16 @@ -import { deploy, minBidIncrement } from './deploy' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { bidAsSigner } from 'scripts/utils/bid' import * as hre from 'hardhat' -import { parseEther } from 'ethers/lib/utils' -import { Contract } from 'ethers' +import { parseEther, parseUnits } from 'ethers/lib/utils' +import { utils } from 'ethers' +import { deployAuctionRaffle } from '../deployAuctionRaffle' +import { + AuctionRaffle, + MockLinkToken__factory, + ScoreAttestationVerifier__factory, + VrfCoordinatorV2MockWithErc677__factory, +} from 'build/types' +import { minStateDuration, reservePrice, minBidIncrement, initialRequiredScore } from './config' const SECOND = 1000 @@ -15,27 +22,105 @@ async function run() { console.log('Deploying contracts...') - const now = Math.floor((new Date()).valueOf() / SECOND) + const deployer = signers[0] + + // Mock VRF + const { vrfCoordinator, vrfRequesterParams } = await setupMockVrf(deployer) + + const now = Math.floor(new Date().valueOf() / SECOND) await hre.network.provider.send('evm_setNextBlockTimestamp', [now]) - const deployer = signers[0] - const auctionRaffle = await deploy(now, deployer, hre) + const biddingStartTime = now + const biddingEndTime = biddingStartTime + minStateDuration + const claimingEndTime = biddingEndTime + minStateDuration + + const { auctionRaffle } = await deployAuctionRaffle({ + isLocalNetwork: true, + initialOwner: deployer.address, + scoreAttestationConfig: { + version: '1', + initialAttestor: deployer.address, + initialRequiredScore, + }, + raffleConfig: { + biddingStartTime, + biddingEndTime, + claimingEndTime, + auctionWinnersCount: 20, + raffleWinnersCount: 80, + reservePrice, + minBidIncrement, + }, + vrfConfig: vrfRequesterParams, + }) + await vrfCoordinator.addConsumer(vrfRequesterParams.subId, auctionRaffle.address) console.log('Contracts deployed\n') - await bid(auctionRaffle, signers.slice(0, 20)) + await bid(auctionRaffle, signers.slice(0, 20), deployer) await nodeProcess } -async function bid(auctionRaffle: Contract, signers: SignerWithAddress[]) { +async function bid(auctionRaffle: AuctionRaffle, signers: SignerWithAddress[], attestor: SignerWithAddress) { const initialBidAmount = parseEther('0.20') + // Score attestation + const scoreVerifier = await auctionRaffle.bidVerifier() + const scoreAttestationVerifier = ScoreAttestationVerifier__factory.connect(scoreVerifier, attestor) + const score = 21 * 10 ** 8 // 21.0 for (let i = 0; i < signers.length; i++) { - await bidAsSigner(auctionRaffle, signers[i], initialBidAmount.add(minBidIncrement.mul(i))) + await bidAsSigner( + auctionRaffle, + signers[i], + initialBidAmount.add(minBidIncrement.mul(i)), + score, + attestor, + scoreAttestationVerifier + ) + } +} + +async function setupMockVrf(deployer: SignerWithAddress) { + // Mock mintable LINK token + const linkToken = await new MockLinkToken__factory(deployer).deploy() + await linkToken.grantMintAndBurnRoles(deployer.address) + await linkToken.mint(deployer.address, parseEther('1000')) + + const vrfCoordinator = await new VrfCoordinatorV2MockWithErc677__factory(deployer).deploy( + parseEther('0.005'), + parseUnits('1', 'gwei'), + linkToken.address + ) + // Create sub + const subId = await vrfCoordinator.callStatic.createSubscription() + await vrfCoordinator.createSubscription() + + // Fund sub + await linkToken.transferAndCall( + vrfCoordinator.address, + parseEther('100'), + utils.defaultAbiCoder.encode(['uint64'], [subId]) + ) + + const vrfRequesterParams = { + vrfCoordinator: vrfCoordinator.address, + linkToken: linkToken.address, + linkPremium: parseEther('0.005'), + gasLaneKeyHash: '0x72d2b016bb5b62912afea355ebf33b91319f828738b111b723b78696b9847b63', // 30 gwei + callbackGasLimit: 10_000_000, // maximum + minConfirmations: 1, // minimum + subId, + } + + return { + vrfCoordinator, + vrfRequesterParams, + linkToken, + subId, } } function delay(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)) + return new Promise((resolve) => setTimeout(resolve, ms)) } run() diff --git a/packages/contracts/scripts/tasks/auctionRaffle/hardhatTasks.ts b/packages/contracts/scripts/tasks/auctionRaffle/hardhatTasks.ts index b6ba383e2..07d8d26b1 100644 --- a/packages/contracts/scripts/tasks/auctionRaffle/hardhatTasks.ts +++ b/packages/contracts/scripts/tasks/auctionRaffle/hardhatTasks.ts @@ -1,42 +1,43 @@ import { task, types } from 'hardhat/config' import { connectToAuctionRaffle } from 'scripts/utils/auctionRaffle' -import { BigNumber, BigNumberish, constants, Contract, utils } from 'ethers' +import { BigNumberish, constants, Contract, utils } from 'ethers' import { parseEther } from 'ethers/lib/utils' import { HardhatRuntimeEnvironment } from 'hardhat/types' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { randomBigNumbers } from 'scripts/utils/random' +import { randomBN } from 'scripts/utils/random' import { generateRandomAccounts } from 'scripts/utils/generateRandomAccounts' import { fundAccounts } from 'scripts/utils/fundAccounts' import { bidAsSigner } from 'scripts/utils/bid' -import { minBidIncrement, reservePrice } from 'scripts/node/deploy' +import { initialRequiredScore, minBidIncrement, reservePrice } from 'scripts/node/config' +import { connectToScoreAttestationVerifier } from 'scripts/utils/scoreAttestationVerifier' +import { connectToMockVrfCoordinator } from 'scripts/utils/mockVrfCoordinator' -const auctionRaffleAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3' +const mockVrfCoordinatorAddress = '0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9' +const scoreAttestationVerifierAddress = '0x0165878a594ca255338adfa4d48449f69242eb8f' +const auctionRaffleAddress = '0xa513e6e4b8f2a923d98304ec87f64353c4d5c853' task('bid', 'Places bid for given account with provided amount') .addParam('account', 'Hardhat account to use') - .addParam('amount', 'The bid\'s amount in ETH', undefined, types.string) - .setAction(async ( - { account, amount }: { account: string, amount: string }, - hre, - ) => { + .addParam('amount', "The bid's amount in ETH", undefined, types.string) + .setAction(async ({ account, amount }: { account: string; amount: string }, hre) => { const signer = await hre.ethers.getSigner(account) const auctionRaffle = await connectToAuctionRaffle(hre, auctionRaffleAddress) - const auctionRaffleAsSigner = auctionRaffle.connect(signer) + const [attestor] = await hre.ethers.getSigners() + const scoreAttestationVerifier = await connectToScoreAttestationVerifier(hre, scoreAttestationVerifierAddress) const ethAmount = parseEther(amount) - await auctionRaffleAsSigner.bid({ value: ethAmount }) + await bidAsSigner(auctionRaffle, signer, ethAmount, initialRequiredScore, attestor, scoreAttestationVerifier) logBid(account, ethAmount) }) task('bid-random', 'Bids X times using randomly generated accounts') .addParam('amount', 'Amount of bids', undefined, types.int, false) .addParam('account', 'Index of hardhat account to take funds from', 0, types.int) - .setAction(async ( - { amount, account }: { amount: number, account: number }, - hre, - ) => { + .setAction(async ({ amount, account }: { amount: number; account: number }, hre) => { const signers = await hre.ethers.getSigners() const auctionRaffle = await connectToAuctionRaffle(hre, auctionRaffleAddress) + const attestor = signers[0] + const scoreAttestationVerifier = await connectToScoreAttestationVerifier(hre, scoreAttestationVerifierAddress) console.log('Generating accounts...') const randomAccounts = generateRandomAccounts(amount, hre.ethers.provider) @@ -47,27 +48,54 @@ task('bid-random', 'Bids X times using randomly generated accounts') console.log('Starting bidding...') for (let i = 0; i < randomAccounts.length; i++) { console.log(`Bidding with random account #${i + 1} out of ${randomAccounts.length}`) - await bidAsSigner(auctionRaffle, randomAccounts[i], reservePrice.add(minBidIncrement.mul(i))) + await bidAsSigner( + auctionRaffle, + randomAccounts[i], + reservePrice.add(minBidIncrement.mul(i)), + initialRequiredScore, + attestor, + scoreAttestationVerifier + ) } }) -task('settle-auction', 'Settles auction') - .setAction(async (taskArgs, hre) => { - const auctionRaffle = await auctionRaffleAsOwner(hre) - - await auctionRaffle.settleAuction() - console.log('Auction settled!') - }) - -task('settle-raffle', 'Settles raffle') - .setAction(async (taskArgs, hre) => { - const auctionRaffle = await auctionRaffleAsOwner(hre) - - const raffleWinnersCount = await auctionRaffle.raffleWinnersCount() - const randomNumbersCount = BigNumber.from(raffleWinnersCount).div(8).toNumber() - - await auctionRaffle.settleRaffle(randomBigNumbers(randomNumbersCount)) - console.log('Raffle settled!') +task('settle-auction', 'Settles auction').setAction(async (taskArgs, hre) => { + const auctionRaffle = await auctionRaffleAsOwner(hre) + + await auctionRaffle.settleAuction() + console.log('Auction settled!') +}) + +task('settle-raffle', 'Settles raffle').setAction(async (taskArgs, hre) => { + const auctionRaffle = await auctionRaffleAsOwner(hre) + + await auctionRaffle.settleRaffle() + console.log('Raffle settled!') +}) + +task('fulfill-vrf', 'Fulfill VRF request').setAction(async (taskArgs, hre) => { + const auctionRaffle = await auctionRaffleAsOwner(hre) + const requestId = await auctionRaffle.requestId() + const mockVrfCoordinator = await connectToMockVrfCoordinator(hre, mockVrfCoordinatorAddress) + const randomWords = [randomBN()] + await mockVrfCoordinator.fulfillRandomWords(requestId, auctionRaffle.address, randomWords) + console.log(`Fulfilled VRF request with: ${randomWords}`) +}) + +task('transfer-eth', 'Transfers ether from DEPLOYER account to desired account') + .addParam('amount', 'ETH amount to send', '1', types.string, true) + .addParam('account', 'ETH amount to send', undefined, types.string, false) + .setAction(async ({ amount, account }: { amount: string, account: string }, hre) => { + const [deployer] = await hre.ethers.getSigners() + console.log('account: ', account) + + const amountToSend = parseEther(amount) + const tx = await deployer.sendTransaction({ + to: account, + value: amountToSend, + }) + await tx.wait() + console.log(`Sent ${formatEther(amountToSend)} to ${account}`) }) function logBid(address: string, bidAmount: BigNumberish) { diff --git a/packages/contracts/scripts/tasks/nodeTasks.ts b/packages/contracts/scripts/tasks/nodeTasks.ts index 6c59d984f..19bf4b913 100644 --- a/packages/contracts/scripts/tasks/nodeTasks.ts +++ b/packages/contracts/scripts/tasks/nodeTasks.ts @@ -1,5 +1,5 @@ import { task, types } from 'hardhat/config' -import { minStateDuration } from 'scripts/node/deploy' +import { minStateDuration } from 'scripts/node/config' task('increase-time', 'Increases block time') .addParam('value', 'Time in seconds to increase', minStateDuration, types.int, true) @@ -11,11 +11,7 @@ task('increase-time', 'Increases block time') await provider.send('evm_mine') }) -task('accounts', 'Prints available accounts') - .setAction(async ( - taskArgs, - hre, - ) => { - const signers = await hre.ethers.getSigners() - signers.forEach((signer, index) => console.log(`Account #${index} ${signer.address}`)) - }) +task('accounts', 'Prints available accounts').setAction(async (taskArgs, hre) => { + const signers = await hre.ethers.getSigners() + signers.forEach((signer, index) => console.log(`Account #${index} ${signer.address}`)) +}) diff --git a/packages/contracts/scripts/utils/bid.ts b/packages/contracts/scripts/utils/bid.ts index 874df7154..f7a63e2db 100644 --- a/packages/contracts/scripts/utils/bid.ts +++ b/packages/contracts/scripts/utils/bid.ts @@ -1,5 +1,19 @@ -import { BigNumberish, Contract, Signer } from 'ethers' +import { BigNumberish, Contract, Signer, Wallet } from 'ethers' +import { attestScore } from 'utils/attestScore' -export async function bidAsSigner(auctionRaffle: Contract, signer: Signer, value: BigNumberish) { - await auctionRaffle.connect(signer).bid({ value, gasLimit: 1_000_000 }) +export async function bidAsSigner( + auctionRaffle: Contract, + signer: Signer, + value: BigNumberish, + score: BigNumberish, + attestor: Signer, + scoreAttestationVerifier: Contract +) { + const { signature: proof } = await attestScore( + await signer.getAddress(), + score, + attestor as unknown as Wallet, + scoreAttestationVerifier.address + ) + await auctionRaffle.connect(signer).bid(score, proof, { value, gasLimit: 1_000_000 }) } diff --git a/packages/contracts/scripts/utils/mockVrfCoordinator.ts b/packages/contracts/scripts/utils/mockVrfCoordinator.ts new file mode 100644 index 000000000..e3845c57a --- /dev/null +++ b/packages/contracts/scripts/utils/mockVrfCoordinator.ts @@ -0,0 +1,9 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +export const mockVrfCoordinatorArtifactName = + 'contracts/mocks/VRFCoordinatorV2MockWithERC677.sol:VRFCoordinatorV2MockWithERC677' + +export async function connectToMockVrfCoordinator(hre: HardhatRuntimeEnvironment, mockVrfCoordinator: string) { + const mockVrfCoordinatorFactory = await hre.ethers.getContractFactory(mockVrfCoordinatorArtifactName) + return mockVrfCoordinatorFactory.attach(mockVrfCoordinator) +} diff --git a/packages/contracts/scripts/utils/random.ts b/packages/contracts/scripts/utils/random.ts index d56264b14..c9abb39be 100644 --- a/packages/contracts/scripts/utils/random.ts +++ b/packages/contracts/scripts/utils/random.ts @@ -8,6 +8,6 @@ export function randomBigNumbers(amount: number): BigNumber[] { return randomNumbers } -function randomBN(): BigNumber { +export function randomBN(): BigNumber { return BigNumber.from(utils.randomBytes(32)) } diff --git a/packages/contracts/scripts/utils/scoreAttestationVerifier.ts b/packages/contracts/scripts/utils/scoreAttestationVerifier.ts new file mode 100644 index 000000000..bce223fab --- /dev/null +++ b/packages/contracts/scripts/utils/scoreAttestationVerifier.ts @@ -0,0 +1,12 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +export const scoreAttestationVerifierArtifactName = + 'contracts/verifier/ScoreAttestationVerifier.sol:ScoreAttestationVerifier' + +export async function connectToScoreAttestationVerifier( + hre: HardhatRuntimeEnvironment, + scoreAttestationVerifierAddress: string +) { + const scoreAttestationVerifierFactory = await hre.ethers.getContractFactory(scoreAttestationVerifierArtifactName) + return scoreAttestationVerifierFactory.attach(scoreAttestationVerifierAddress) +} diff --git a/packages/contracts/test/contracts/AuctionRaffle.e2e.ts b/packages/contracts/test/contracts/AuctionRaffle.e2e.ts index 748936333..dbc5bf12b 100644 --- a/packages/contracts/test/contracts/AuctionRaffle.e2e.ts +++ b/packages/contracts/test/contracts/AuctionRaffle.e2e.ts @@ -1,20 +1,21 @@ import { setupFixtureLoader } from '../setup' import { auctionRaffleE2EFixture, minBidIncrement, reservePrice } from 'fixtures/auctionRaffleFixture' -import { AuctionRaffleMock } from 'contracts' +import { AuctionRaffleMock, ScoreAttestationVerifier, VrfCoordinatorV2MockWithErc677 } from 'contracts' import { Provider } from '@ethersproject/providers' -import { BigNumber, constants, Wallet } from 'ethers' -import { randomBigNumbers } from 'scripts/utils/random' +import { BigNumber, BigNumberish, constants, Wallet } from 'ethers' +import { randomBN, randomBigNumbers } from 'scripts/utils/random' import { expect } from 'chai' import { heapKey } from 'utils/heapKey' import { network } from 'hardhat' import { compareBids } from 'utils/compareBids' import { HOUR } from 'scripts/utils/consts' +import { attestScore } from 'utils/attestScore' interface Bid { - bidderID: number, - amount: BigNumber, - bumpAmount: BigNumber, - wallet: Wallet, + bidderID: number + amount: BigNumber + bumpAmount: BigNumber + wallet: Wallet } describe('AuctionRaffle - E2E', function () { @@ -24,6 +25,9 @@ describe('AuctionRaffle - E2E', function () { let auctionRaffle: AuctionRaffleMock let auctionRaffleAsOwner: AuctionRaffleMock let wallets: Wallet[] + let vrfCoordinator: VrfCoordinatorV2MockWithErc677 + let scoreAttestationVerifier: ScoreAttestationVerifier + let attestor: Wallet let bids: Bid[] let sortedBids: Bid[] @@ -32,17 +36,25 @@ describe('AuctionRaffle - E2E', function () { this.timeout(60_000) before('prepare contracts', async function () { - ({ provider, auctionRaffle, wallets } = await loadFixture(auctionRaffleE2EFixture)) + ;({ + provider: provider as any, + auctionRaffle, + wallets, + vrfCoordinator, + scoreAttestationVerifier, + attestor, + } = await loadFixture(auctionRaffleE2EFixture)) auctionRaffleAsOwner = auctionRaffle.connect(owner()) }) before('prepare bids', function () { - bids = randomBigNumbers(120).map((bn, index): Bid => ({ - bidderID: index + 1, - amount: bn.shr(192).add(reservePrice), - bumpAmount: index % 2 === 0 ? bn.shr(240).add(minBidIncrement) : constants.Zero, - wallet: wallets[index], - }), + bids = randomBigNumbers(120).map( + (bn, index): Bid => ({ + bidderID: index + 1, + amount: bn.shr(192).add(reservePrice), + bumpAmount: index % 2 === 0 ? bn.shr(240).add(minBidIncrement) : constants.Zero, + wallet: wallets[index], + }) ) // introduce some duplicate amounts @@ -55,7 +67,7 @@ describe('AuctionRaffle - E2E', function () { it('lets 120 participants place bids', async function () { for (const { wallet, amount } of bids) { - await auctionRaffle.connect(wallet).bid({ value: amount }) + await bidAsEligibleWallet(amount, wallet) } expect(await auctionRaffle.getRaffleParticipants()).to.have.lengthOf(120) @@ -67,11 +79,11 @@ describe('AuctionRaffle - E2E', function () { it('lets some participants bump bids', async function () { for (const { wallet, bumpAmount } of bids) { if (!bumpAmount.isZero()) { - await auctionRaffle.connect(wallet).bid({ value: bumpAmount }) + await bidAsEligibleWallet(bumpAmount, wallet) } } - sortedBids = bids.map(bid => ({ ...bid, amount: bid.amount.add(bid.bumpAmount) })).sort(compareBids) + sortedBids = bids.map((bid) => ({ ...bid, amount: bid.amount.add(bid.bumpAmount) })).sort(compareBids) }) it('lets the owner settle the auction', async function () { @@ -80,16 +92,25 @@ describe('AuctionRaffle - E2E', function () { expect(await auctionRaffle.getHeap()).to.be.empty - const expectedAuctionWinners = sortedBids.slice(0, 20).map(bid => BigNumber.from(bid.bidderID)) + const expectedAuctionWinners = sortedBids.slice(0, 20).map((bid) => BigNumber.from(bid.bidderID)) expect(await auctionRaffle.getAuctionWinners()).to.deep.eq(expectedAuctionWinners) expect(await auctionRaffle.getRaffleParticipants()).to.have.lengthOf(100) }) + // NB: This test depends on the previous test (settle auction) it('lets the owner settle the raffle', async function () { - await auctionRaffleAsOwner.settleRaffle(randomBigNumbers(10)) - - expect(await auctionRaffle.getRaffleWinners()).to.have.lengthOf(80) - expect(await auctionRaffle.getRaffleParticipants()).to.have.lengthOf(20) + await settleAndFulfillRaffle(randomBN()) + + const raffleWinners = await auctionRaffle.getRaffleWinners() + expect(raffleWinners).to.have.lengthOf(80) + // NB: `getRaffleParticipants` includes raffle winners + const raffleParticipants = await auctionRaffle.getRaffleParticipants() + expect(raffleParticipants).to.have.lengthOf(100) + const losers = setDifferenceOf( + new Set(raffleParticipants.map((bn) => bn.toString())), + new Set(raffleWinners.map((bn) => bn.toString())) + ) + expect(losers.size).to.eq(20) }) it('lets everyone claim their funds', async function () { @@ -100,27 +121,30 @@ describe('AuctionRaffle - E2E', function () { } await auctionRaffleAsOwner.claimProceeds() - await auctionRaffleAsOwner.claimFees(20) + // NB: The `claimFees` function has been removed. + // await auctionRaffleAsOwner.claimFees(20) for (const { wallet, bidderID } of nonAuctionBids.slice(50, 100)) { await auctionRaffle.connect(wallet).claim(bidderID) } + // The only way to claim the 2% fees of non-winning bids now is to + // wait until claims have ended, and then invoking the + // `withdrawUnclaimedFunds` function. + await endClaiming(auctionRaffle) + await auctionRaffleAsOwner.withdrawUnclaimedFunds() + expect(await provider.getBalance(auctionRaffle.address)).to.eq(0) }) - it('divides bidders into 3 disjoint sets', async function () { - const bidders = [ - ...await auctionRaffle.getAuctionWinners(), - ...await auctionRaffle.getRaffleWinners(), - ...await auctionRaffle.getRaffleParticipants(), - ] + it('divides bidders into 2 disjoint sets', async function () { + const bidders = [...(await auctionRaffle.getAuctionWinners()), ...(await auctionRaffle.getRaffleParticipants())] let bids = [] for (const bidder of bidders) { bids.push(await auctionRaffle.getBidByID(bidder)) } - bids = bids.sort(compareBids).map(bid => ({ + bids = bids.sort(compareBids).map((bid) => ({ bidderID: bid.bidderID.toNumber(), amount: bid.amount, })) @@ -138,4 +162,43 @@ describe('AuctionRaffle - E2E', function () { await network.provider.send('evm_setNextBlockTimestamp', [endTime.add(HOUR).toNumber()]) await network.provider.send('evm_mine') } + + async function endClaiming(auctionRaffle: AuctionRaffleMock) { + const endTime = await auctionRaffle.claimingEndTime() + await network.provider.send('evm_setNextBlockTimestamp', [endTime.add(HOUR).toNumber()]) + await network.provider.send('evm_mine') + } + + async function settleAndFulfillRaffle(randomNumber: BigNumberish) { + await auctionRaffleAsOwner.settleRaffle() + const requestId = await auctionRaffleAsOwner.requestId() + return vrfCoordinator.fulfillRandomWords(requestId, auctionRaffleAsOwner.address, [randomNumber], { + gasLimit: 2_500_000, + }) + } + + /** + * Compute A \ B + */ + function setDifferenceOf(a: Set, b: Set) { + const result = new Set(a) + for (const element of b) { + result.delete(element) + } + return result + } + + async function bidAsEligibleWallet(value: BigNumberish, wallet?: Wallet) { + // Create attestation that this wallet is eligible + expect(await scoreAttestationVerifier.attestor()).to.eq(attestor.address, 'Unexpected attestor') + const score = 21 * 10 ** 8 // 21.0 + if (wallet) { + const { signature } = await attestScore(wallet.address, score, attestor, scoreAttestationVerifier.address) + return auctionRaffle.connect(wallet).bid(score, signature, { value }) + } else { + const subject = await auctionRaffle.signer.getAddress() + const { signature } = await attestScore(subject, score, attestor, scoreAttestationVerifier.address) + return auctionRaffle.bid(score, signature, { value }) + } + } }) diff --git a/packages/contracts/test/contracts/AuctionRaffle.test.ts b/packages/contracts/test/contracts/AuctionRaffle.test.ts index 0c7c7ba65..244c69f9f 100644 --- a/packages/contracts/test/contracts/AuctionRaffle.test.ts +++ b/packages/contracts/test/contracts/AuctionRaffle.test.ts @@ -7,21 +7,28 @@ import { minBidIncrement, reservePrice, } from 'fixtures/auctionRaffleFixture' -import { AuctionRaffleMock, ExampleToken } from 'contracts' +import { + AuctionRaffleMock, + ExampleToken, + ScoreAttestationVerifier, + VrfCoordinatorV2MockWithErc677, + VrfCoordinatorV2MockWithErc677__factory, +} from 'contracts' import { getLatestBlockTimestamp } from 'utils/getLatestBlockTimestamp' import { Provider } from '@ethersproject/providers' import { Zero } from '@ethersproject/constants' import { HOUR, MINUTE } from 'scripts/utils/consts' import { network } from 'hardhat' -import { BigNumber, BigNumberish, ContractTransaction, Wallet } from 'ethers' +import { BigNumber, BigNumberish, ContractTransaction, VoidSigner, Wallet } from 'ethers' import { State } from './state' import { WinType } from './winType' import { bigNumberArrayFrom } from 'utils/bigNumber' import { randomAddress } from 'utils/randomAddress' import { Bid } from './bid' import { parseEther } from 'ethers/lib/utils' -import { randomBigNumbers } from 'scripts/utils/random' +import { randomBN } from 'scripts/utils/random' import { heapKey } from 'utils/heapKey' +import { attestScore } from 'utils/attestScore' describe('AuctionRaffle', function () { const loadFixture = setupFixtureLoader() @@ -29,75 +36,128 @@ describe('AuctionRaffle', function () { let provider: Provider let auctionRaffle: AuctionRaffleMock let auctionRaffleAsOwner: AuctionRaffleMock + let vrfCoordinator: VrfCoordinatorV2MockWithErc677 let bidderAddress: string let wallets: Wallet[] + let attestor: Wallet + let scoreAttestationVerifier: ScoreAttestationVerifier beforeEach(async function () { - ({ provider, auctionRaffle, wallets } = await loadFixture(auctionRaffleFixture)) + ;({ + provider: provider as any, + auctionRaffle, + wallets, + attestor, + scoreAttestationVerifier, + vrfCoordinator, + } = await loadFixture(auctionRaffleFixture)) auctionRaffleAsOwner = auctionRaffle.connect(owner()) + vrfCoordinator = new VrfCoordinatorV2MockWithErc677__factory(owner()).attach( + await auctionRaffleAsOwner.vrfCoordinator() + ) bidderAddress = await auctionRaffle.signer.getAddress() }) describe('bid', function () { + it('reverts when bidding with invalid attestation', async function () { + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture(auctionRaffleFixture)) + const subject = await auctionRaffle.signer.getAddress() + const score = '2000000000' // 20.0 + const wrongAttestor = Wallet.createRandom() + const { signature } = await attestScore(subject, score, wrongAttestor, scoreAttestationVerifier.address, { + chainId: '31337', + version: '1', + }) + await expect(auctionRaffle.bid(score, signature)).to.be.revertedWith('Unauthorised attestor') + }) + + it('reverts when attested score is too low', async function () { + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture(auctionRaffleFixture)) + const subject = await auctionRaffle.signer.getAddress() + const score = '1900000000' // 19.0 + const { signature } = await attestScore(subject, score, attestor, scoreAttestationVerifier.address, { + chainId: '31337', + version: '1', + }) + await expect(auctionRaffle.bid(score, signature)).to.be.revertedWith('Score too low') + }) + + it('reverts if bidding on an existing bid', async function () { + await bidWithAttestation(reservePrice) + await expect(bidWithAttestation(minBidIncrement)).to.be.revertedWith('AuctionRaffle: bid already exists') + // bump should succeed + await auctionRaffle.bump({ value: minBidIncrement }) + }) + + it('reverts if bumping a nonexistend bid', async function () { + await expect(auctionRaffle.bump({ value: minBidIncrement })).to.be.revertedWith( + 'AuctionRaffle: bump nonexistent bid' + ) + }) + it('reverts if bidding is not opened yet', async function () { - const currentTime = await getLatestBlockTimestamp(provider); - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ biddingStartTime: currentTime + MINUTE }))) + const currentTime = await getLatestBlockTimestamp(provider) + ;({ auctionRaffle, attestor } = await loadFixture( + configuredAuctionRaffleFixture({ biddingStartTime: currentTime + MINUTE }) + )) - await expect(auctionRaffle.bid()).to.be.revertedWith('AuctionRaffle: is in invalid state') + await expect(auctionRaffle.bid(10, '0x')).to.be.revertedWith('AuctionRaffle: is in invalid state') }) it('reverts if bidding is already closed', async function () { const endTime = await auctionRaffle.biddingEndTime() await network.provider.send('evm_setNextBlockTimestamp', [endTime.add(HOUR).toNumber()]) - await expect(auctionRaffle.bid()).to.be.revertedWith('AuctionRaffle: is in invalid state') + await expect(auctionRaffle.bid(10, '0x')).to.be.revertedWith('AuctionRaffle: is in invalid state') }) it('reverts if bid increase is too low', async function () { - await auctionRaffle.bid({ value: reservePrice }) - await expect(auctionRaffle.bid({ value: minBidIncrement.sub(100) })) - .to.be.revertedWith('AuctionRaffle: bid increment too low') + await bidOrBumpWithAttestation(reservePrice) + await expect(auctionRaffle.bump({ value: minBidIncrement.sub(100) })).to.be.revertedWith( + 'AuctionRaffle: bid increment too low' + ) }) it('increases bid amount', async function () { - await auctionRaffle.bid({ value: reservePrice }) - await expect(auctionRaffle.bid({ value: minBidIncrement })).to.be.not.reverted + await bidOrBumpWithAttestation(reservePrice) + await expect(auctionRaffle.bump({ value: minBidIncrement })).to.be.not.reverted const bid = await auctionRaffle.getBid(bidderAddress) expect(bid.amount).to.be.equal(reservePrice.add(minBidIncrement)) }) it('reverts if bid amount is below reserve price', async function () { - await expect(auctionRaffle.bid({ value: reservePrice.sub(100) })) - .to.be.revertedWith('AuctionRaffle: bid amount is below reserve price') + await expect(bidOrBumpWithAttestation(reservePrice.sub(100))).to.be.revertedWith( + 'AuctionRaffle: bid amount is below reserve price' + ) }) it('saves bid', async function () { - await expect(auctionRaffle.bid({ value: reservePrice })).to.be.not.reverted + await expect(bidOrBumpWithAttestation(reservePrice)).to.be.not.reverted const bid = await auctionRaffle.getBid(bidderAddress) expect(bid.bidderID).to.be.equal(1) expect(bid.amount).to.be.equal(reservePrice) - expect(bid.winType).to.be.equal(WinType.loss) + expect(await auctionRaffle.getBidWinType(bid.bidderID)).to.be.equal(WinType.loss) expect(bid.claimed).to.be.false }) it('saves bidder address', async function () { - await auctionRaffle.bid({ value: reservePrice }) + await bidOrBumpWithAttestation(reservePrice) const savedBidderAddress = await auctionRaffle.getBidderAddress(1) expect(savedBidderAddress).to.be.equal(bidderAddress) }) it('saves bidder as raffle participant', async function () { - await auctionRaffle.bid({ value: reservePrice }) + await bidOrBumpWithAttestation(reservePrice) expect(await auctionRaffle.getRaffleParticipants()).to.deep.eq([BigNumber.from(1)]) }) it('increases bidders count', async function () { - await auctionRaffle.bid({ value: reservePrice }) + await bidOrBumpWithAttestation(reservePrice) expect(await auctionRaffle.getBiddersCount()).to.be.equal(1) }) @@ -105,11 +165,13 @@ describe('AuctionRaffle', function () { describe('when heap is full', function () { describe('when bid < min auction bid', function () { it('does not add bid to heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) - await bidAsWallet(wallets[0], reservePrice.add(100)) - await bidAsWallet(wallets[1], reservePrice.add(200)) - await bidAsWallet(wallets[2], reservePrice.add(50)) + await bidOrBumpWithAttestation(reservePrice.add(100), wallets[0]) + await bidOrBumpWithAttestation(reservePrice.add(200), wallets[1]) + await bidOrBumpWithAttestation(reservePrice.add(50), wallets[2]) expect(await auctionRaffle.getHeap()).to.deep.equal([ heapKey(2, reservePrice.add(200)), @@ -122,14 +184,18 @@ describe('AuctionRaffle', function () { describe('when bid > min auction bid', function () { it('replaces minimum auction bid', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) await bid(2) - await bidAsWallet(wallets[2], reservePrice.add(100)) - await bidAsWallet(wallets[3], reservePrice.add(120)) + await bidOrBumpWithAttestation(reservePrice.add(100), wallets[2]) + await bidOrBumpWithAttestation(reservePrice.add(120), wallets[3]) - expect(await auctionRaffle.getHeap()) - .to.deep.equal([heapKey(4, reservePrice.add(120)), heapKey(3, reservePrice.add(100))]) + expect(await auctionRaffle.getHeap()).to.deep.equal([ + heapKey(4, reservePrice.add(120)), + heapKey(3, reservePrice.add(100)), + ]) expect(await auctionRaffle.getMinKeyIndex()).to.eq(1) expect(await auctionRaffle.getMinKeyValue()).to.eq(heapKey(3, reservePrice.add(100))) }) @@ -137,13 +203,15 @@ describe('AuctionRaffle', function () { describe('when bumped bid < min auction bid', function () { it('does not add bid to heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) - await bidAsWallet(wallets[0], reservePrice.add(minBidIncrement).add(100)) - await bidAsWallet(wallets[1], reservePrice.add(minBidIncrement).add(200)) - await bidAsWallet(wallets[2], reservePrice) + await bidOrBumpWithAttestation(reservePrice.add(minBidIncrement).add(100), wallets[0]) + await bidOrBumpWithAttestation(reservePrice.add(minBidIncrement).add(200), wallets[1]) + await bidOrBumpWithAttestation(reservePrice, wallets[2]) - await bidAsWallet(wallets[2], minBidIncrement) + await auctionRaffle.connect(wallets[2]).bump({ value: minBidIncrement }) expect(await auctionRaffle.getHeap()).to.deep.equal([ heapKey(2, reservePrice.add(minBidIncrement).add(200)), @@ -157,13 +225,15 @@ describe('AuctionRaffle', function () { describe('when bumped bid > min auction bid', function () { describe('when old bid < min auction bid', function () { it('adds bid to heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) - await bidAsWallet(wallets[0], reservePrice) - await bidAsWallet(wallets[1], reservePrice.add(minBidIncrement).add(200)) - await bidAsWallet(wallets[2], reservePrice.add(minBidIncrement)) + await bidOrBumpWithAttestation(reservePrice, wallets[0]) + await bidOrBumpWithAttestation(reservePrice.add(minBidIncrement).add(200), wallets[1]) + await bidOrBumpWithAttestation(reservePrice.add(minBidIncrement), wallets[2]) - await bidAsWallet(wallets[0], minBidIncrement.add(100)) + await auctionRaffle.connect(wallets[0]).bump({ value: minBidIncrement.add(100) }) expect(await auctionRaffle.getHeap()).to.deep.equal([ heapKey(2, reservePrice.add(minBidIncrement).add(200)), @@ -176,12 +246,14 @@ describe('AuctionRaffle', function () { describe('when old bid == min auction bid', function () { it('updates bid in heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) - await bidAsWallet(wallets[0], reservePrice) - await bidAsWallet(wallets[1], reservePrice.add(minBidIncrement).add(200)) + await bidOrBumpWithAttestation(reservePrice, wallets[0]) + await bidOrBumpWithAttestation(reservePrice.add(minBidIncrement).add(200), wallets[1]) - await bidAsWallet(wallets[0], minBidIncrement.add(100)) + await auctionRaffle.connect(wallets[0]).bump({ value: minBidIncrement.add(100) }) expect(await auctionRaffle.getHeap()).to.deep.equal([ heapKey(2, reservePrice.add(minBidIncrement).add(200)), @@ -194,12 +266,14 @@ describe('AuctionRaffle', function () { describe('when old bid > min auction bid', function () { it('updates bid in heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) - await bidAsWallet(wallets[0], reservePrice) - await bidAsWallet(wallets[1], reservePrice.add(200)) + await bidOrBumpWithAttestation(reservePrice, wallets[0]) + await bidOrBumpWithAttestation(reservePrice.add(200), wallets[1]) - await bidAsWallet(wallets[1], minBidIncrement) + await auctionRaffle.connect(wallets[1]).bump({ value: minBidIncrement }) expect(await auctionRaffle.getHeap()).to.deep.equal([ heapKey(2, reservePrice.add(minBidIncrement).add(200)), @@ -215,14 +289,15 @@ describe('AuctionRaffle', function () { describe('when heap is not full', function () { describe('when bid < min auction bid', function () { it('adds bid to heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) const auctionWinnerBid = reservePrice.add(100) - await bidAsWallet(wallets[0], auctionWinnerBid) - await bidAsWallet(wallets[1], reservePrice) + await bidOrBumpWithAttestation(auctionWinnerBid, wallets[0]) + await bidOrBumpWithAttestation(reservePrice, wallets[1]) - expect(await auctionRaffle.getHeap()) - .to.deep.equal([heapKey(1, auctionWinnerBid), heapKey(2, reservePrice)]) + expect(await auctionRaffle.getHeap()).to.deep.equal([heapKey(1, auctionWinnerBid), heapKey(2, reservePrice)]) expect(await auctionRaffle.getMinKeyIndex()).to.eq(1) expect(await auctionRaffle.getMinKeyValue()).to.eq(heapKey(2, reservePrice)) }) @@ -230,14 +305,15 @@ describe('AuctionRaffle', function () { describe('when bid > min auction bid', function () { it('adds bid to heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) const auctionWinnerBid = reservePrice.add(100) - await bidAsWallet(wallets[0], reservePrice) - await bidAsWallet(wallets[1], auctionWinnerBid) + await bidOrBumpWithAttestation(reservePrice, wallets[0]) + await bidOrBumpWithAttestation(auctionWinnerBid, wallets[1]) - expect(await auctionRaffle.getHeap()) - .to.deep.equal([heapKey(2, auctionWinnerBid), heapKey(1, reservePrice)]) + expect(await auctionRaffle.getHeap()).to.deep.equal([heapKey(2, auctionWinnerBid), heapKey(1, reservePrice)]) expect(await auctionRaffle.getMinKeyIndex()).to.eq(1) expect(await auctionRaffle.getMinKeyValue()).to.eq(heapKey(1, reservePrice)) }) @@ -245,13 +321,15 @@ describe('AuctionRaffle', function () { describe('when bumped bid == min auction bid', function () { it('updates old bid in heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 4 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 4 }) + )) - await bidAsWallet(wallets[0], reservePrice.add(200)) - await bidAsWallet(wallets[1], reservePrice.add(minBidIncrement)) - await bidAsWallet(wallets[2], reservePrice.add(minBidIncrement).add(100)) + await bidOrBumpWithAttestation(reservePrice.add(200), wallets[0]) + await bidOrBumpWithAttestation(reservePrice.add(minBidIncrement), wallets[1]) + await bidOrBumpWithAttestation(reservePrice.add(minBidIncrement).add(100), wallets[2]) - await bidAsWallet(wallets[0], minBidIncrement) + await auctionRaffle.connect(wallets[0]).bump({ value: minBidIncrement }) expect(await auctionRaffle.getHeap()).to.deep.equal([ heapKey(1, reservePrice.add(minBidIncrement).add(200)), @@ -265,16 +343,17 @@ describe('AuctionRaffle', function () { describe('when bumped bid > min auction bid', function () { it('updates old bid in heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 3 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 3 }) + )) let auctionWinnerBid = reservePrice.add(100) - await bidAsWallet(wallets[0], auctionWinnerBid) - await bidAsWallet(wallets[1], reservePrice) - await bidAsWallet(wallets[0], minBidIncrement) + await bidOrBumpWithAttestation(auctionWinnerBid, wallets[0]) + await bidOrBumpWithAttestation(reservePrice, wallets[1]) + await auctionRaffle.connect(wallets[0]).bump({ value: minBidIncrement }) auctionWinnerBid = auctionWinnerBid.add(minBidIncrement) - expect(await auctionRaffle.getHeap()) - .to.deep.equal([heapKey(1, auctionWinnerBid), heapKey(2, reservePrice)]) + expect(await auctionRaffle.getHeap()).to.deep.equal([heapKey(1, auctionWinnerBid), heapKey(2, reservePrice)]) expect(await auctionRaffle.getMinKeyIndex()).to.eq(1) expect(await auctionRaffle.getMinKeyValue()).to.eq(heapKey(2, reservePrice)) }) @@ -282,15 +361,15 @@ describe('AuctionRaffle', function () { }) it('emits event on bid increase', async function () { - await auctionRaffle.bid({ value: reservePrice }) + await bidOrBumpWithAttestation(reservePrice) - await expect(auctionRaffle.bid({ value: minBidIncrement })) + await expect(auctionRaffle.bump({ value: minBidIncrement })) .to.emit(auctionRaffle, 'NewBid') .withArgs(bidderAddress, 1, reservePrice.add(minBidIncrement)) }) it('emits event on bid', async function () { - await expect(auctionRaffle.bid({ value: reservePrice })) + await expect(bidOrBumpWithAttestation(reservePrice)) .to.emit(auctionRaffle, 'NewBid') .withArgs(bidderAddress, 1, reservePrice) }) @@ -301,25 +380,23 @@ describe('AuctionRaffle', function () { await bid(9) }) - it('reverts if called not by owner', async function () { - await expect(auctionRaffle.settleAuction()) - .to.be.revertedWith('Ownable: caller is not the owner') + it('can be called by anyone', async function () { + await endBidding(auctionRaffleAsOwner) + await expect(auctionRaffle.settleAuction()).to.not.be.reverted }) it('reverts if bidding is in progress', async function () { - await expect(settleAuction()) - .to.be.revertedWith('AuctionRaffle: is in invalid state') + await expect(settleAuction()).to.be.revertedWith('AuctionRaffle: is in invalid state') }) it('reverts if called twice', async function () { await endBidding(auctionRaffleAsOwner) await settleAuction() - await expect(settleAuction()) - .to.be.revertedWith('AuctionRaffle: is in invalid state') + await expect(settleAuction()).to.be.revertedWith('AuctionRaffle: is in invalid state') }) it('changes state if number of bidders is less than raffleWinnersCount', async function () { - ({ auctionRaffle } = await loadFixture(auctionRaffleFixture)) + ;({ auctionRaffle } = await loadFixture(auctionRaffleFixture)) auctionRaffleAsOwner = auctionRaffle.connect(owner()) await bid(1) @@ -331,7 +408,9 @@ describe('AuctionRaffle', function () { }) it('chooses auction winners when there are not enough participants for entire auction', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 5 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 5 }) + )) auctionRaffleAsOwner = auctionRaffle.connect(owner()) await bid(9) @@ -349,7 +428,7 @@ describe('AuctionRaffle', function () { await settleAuction() const bid = await getBidByID(1) - expect(bid.winType).to.deep.equal(WinType.auction) + expect(await auctionRaffle.getBidWinType(bid.bidderID)).to.deep.equal(WinType.auction) }) it('saves auction winners', async function () { @@ -360,7 +439,9 @@ describe('AuctionRaffle', function () { }) it('deletes heap', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 5 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 5 }) + )) auctionRaffleAsOwner = auctionRaffle.connect(owner()) await bid(10) @@ -371,7 +452,9 @@ describe('AuctionRaffle', function () { }) it('removes winners from raffle participants', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) auctionRaffleAsOwner = auctionRaffle.connect(owner()) await bid(10) @@ -379,11 +462,13 @@ describe('AuctionRaffle', function () { await endBidding(auctionRaffleAsOwner) await settleAuction() - expect(await auctionRaffle.getRaffleParticipants()).to.deep.eq(bigNumberArrayFrom([9, 10, 3, 4, 5, 6, 7, 8])) + expect(await auctionRaffle.getRaffleParticipants()).to.deep.eq(bigNumberArrayFrom([10, 9, 3, 4, 5, 6, 7, 8])) }) it('emits events', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2 }) + )) auctionRaffleAsOwner = auctionRaffle.connect(owner()) await bid(10) @@ -399,40 +484,21 @@ describe('AuctionRaffle', function () { await bid(9) }) - it('reverts if called not by owner', async function () { - await expect(auctionRaffle.settleRaffle([1])) - .to.be.revertedWith('Ownable: caller is not the owner') - }) - - it('reverts if raffle is not settled', async function () { - await expect(auctionRaffleAsOwner.settleRaffle([1])) - .to.be.revertedWith('AuctionRaffle: is in invalid state') - }) - - it('reverts if called with zero random numbers', async function () { + it('can be called by anyone', async function () { await endBidding(auctionRaffleAsOwner) - await settleAuction() - - await expect(auctionRaffleAsOwner.settleRaffle([])) - .to.be.revertedWith('AuctionRaffle: there must be at least one random number passed') + await auctionRaffle.settleAuction() + await expect(auctionRaffle.settleRaffle()).to.not.be.reverted }) - it('reverts if called with incorrect amount of random numbers', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ raffleWinnersCount: 16 }))) - auctionRaffleAsOwner = auctionRaffle.connect(owner()) - - await bid(20) - await endBidding(auctionRaffleAsOwner) - await settleAuction() - - // Reverts because it expects 2 random numbers - await expect(auctionRaffleAsOwner.settleRaffle(randomBigNumbers(3))) - .to.be.revertedWith('AuctionRaffle: passed incorrect number of random numbers') + it('reverts if raffle is not settled', async function () { + await expect(auctionRaffleAsOwner.settleRaffle()).to.be.revertedWith('AuctionRaffle: is in invalid state') }) describe('when bidders count is less than raffleWinnersCount', function () { it('picks all participants as winners', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ raffleWinnersCount: 16 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ raffleWinnersCount: 16 }) + )) auctionRaffleAsOwner = auctionRaffle.connect(owner()) await bid(4) @@ -441,16 +507,19 @@ describe('AuctionRaffle', function () { await settleAuction() // Golden ticket winner participant index generated from this number: 2, bidderID: 3 - const randomNumber = BigNumber.from('65155287986987035700835155359065462427392489128550609102552042044410661181326') - await auctionRaffleAsOwner.settleRaffle([randomNumber]) + const randomNumber = BigNumber.from( + '65155287986987035700835155359065462427392489128550609102552042044410661181326' + ) + await settleAndFulfillRaffle(randomNumber) for (let i = 1; i <= 4; i++) { const bid = await getBidByID(i) - if (bid.bidderID.eq(3)) { - expect(bid.winType).to.be.eq(WinType.goldenTicket) + const winType = await auctionRaffle.getBidWinType(bid.bidderID) + if (bid.bidderID.eq(4)) { + expect(winType).to.be.eq(WinType.goldenTicket) } else { - expect(bid.winType).to.be.eq(WinType.raffle) + expect(winType).to.be.eq(WinType.raffle) } } }) @@ -460,14 +529,15 @@ describe('AuctionRaffle', function () { await verifyRaffleWinners() }) - it('removes raffle participants', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ raffleWinnersCount: 16 }))) - auctionRaffleAsOwner = auctionRaffle.connect(wallets[1]) + // NB: No longer necessary; raffle is settled atomically + // it('removes raffle participants', async function () { + // ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ raffleWinnersCount: 16 }))) + // auctionRaffleAsOwner = auctionRaffle.connect(wallets[1]) - await bidAndSettleRaffle(4) - const raffleParticipants = await auctionRaffleAsOwner.getRaffleParticipants() - expect(raffleParticipants.length).to.be.equal(0) - }) + // await bidAndSettleRaffle(4) + // const raffleParticipants = await auctionRaffleAsOwner.getRaffleParticipants() + // expect(raffleParticipants.length).to.be.equal(0) + // }) }) describe('when bidders count is greater than raffleWinnersCount', function () { @@ -482,8 +552,10 @@ describe('AuctionRaffle', function () { await endBidding(auctionRaffleAsOwner) await settleAuction() - const randomNumber = BigNumber.from('65155287986987035700835155359065462427392489128550609102552042044410661181326') - await auctionRaffleAsOwner.settleRaffle([randomNumber]) + const randomNumber = BigNumber.from( + '65155287986987035700835155359065462427392489128550609102552042044410661181326' + ) + await settleAndFulfillRaffle(randomNumber) const raffleWinners = await getAllBidsByWinType(10, WinType.raffle) const goldenWinners = await getAllBidsByWinType(10, WinType.goldenTicket) @@ -493,7 +565,9 @@ describe('AuctionRaffle', function () { }) it('selects random winners', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ raffleWinnersCount: 16 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ raffleWinnersCount: 16 }) + )) auctionRaffleAsOwner = auctionRaffle.connect(owner()) await bid(20) @@ -501,28 +575,26 @@ describe('AuctionRaffle', function () { await endBidding(auctionRaffleAsOwner) await settleAuction() - // Participant indexes generated from this number: - // [[16, 16, 6, 7, 4, 9, 0, 1], [6, 3, 6, 7, 1, 3, 2, 2]] - const randomNumbers = [ - BigNumber.from('112726022748934390014388827089462711312944969753614146584009694773482609536945'), - BigNumber.from('105047327762739474822912977776629330956455721538092382425528863739595553862604'), - ] + const randomNumber = BigNumber.from( + '112726022748934390014388827089462711312944969753614146584009694773482609536945' + ) - await auctionRaffleAsOwner.settleRaffle(randomNumbers) + await settleAndFulfillRaffle(randomNumber) - const winnersBidderIDs = [17, 19, 7, 8, 5, 10, 20, 2, 18, 4, 14, 16, 12, 10, 3, 15] + const winnersBidderIDs = [20, 9, 16, 13, 2, 12, 15, 4, 19, 10, 8, 18, 11, 3, 7, 14] for (let i = 0; i < winnersBidderIDs.length; i++) { const winningBid = await getBidByID(winnersBidderIDs[i]) + const winType = await auctionRaffle.getBidWinType(winningBid.bidderID) if (i === 0) { - expect(winningBid.winType).to.be.eq(WinType.goldenTicket) + expect(winType).to.be.eq(WinType.goldenTicket) continue } - expect(winningBid.winType).to.be.eq(WinType.raffle) + expect(winType).to.be.eq(WinType.raffle) } }) it('works if there are no participants', async function () { - ({ auctionRaffle } = await loadFixture(auctionRaffleFixture)) + ;({ auctionRaffle } = await loadFixture(auctionRaffleFixture)) auctionRaffleAsOwner = auctionRaffle.connect(owner()) await bidAndSettleRaffle(0) @@ -533,33 +605,19 @@ describe('AuctionRaffle', function () { await settleAuction() - await auctionRaffleAsOwner.settleRaffle(randomBigNumbers(1)) + await settleAndFulfillRaffle(randomBN()) expect(await auctionRaffleAsOwner.getState()).to.be.eq(State.raffleSettled) }) - describe('when golden ticket winner has been selected', function () { - it('emits event', async function () { - const tx = await bidAndSettleRaffle(0) - - const goldenBid = await getBidByWinType(9, WinType.goldenTicket) - await emitsEvents(tx, 'NewGoldenTicketWinner', [goldenBid.bidderID]) - }) - }) - - describe('when raffle winners have been selected', function () { + describe('when raffle winners have been selected (including golden ticket winner)', function () { it('emits events', async function () { await endBidding(auctionRaffleAsOwner) await settleAuction() // auction winner bidderID: 1 // Golden ticket winner participant index generated from this number: 7, bidderID: 8 - const tx = await auctionRaffleAsOwner.settleRaffle([7]) - - const raffleWinners: number[][] = [[9]] - for (let i = 2; i < 8; i++) { - raffleWinners.push([i]) - } - await emitsEvents(tx, 'NewRaffleWinner', ...raffleWinners) + const tx = await settleAndFulfillRaffle(7) + await expect(tx).to.emit(auctionRaffle, 'RaffleWinnersDrawn').withArgs(7) }) }) @@ -568,12 +626,13 @@ describe('AuctionRaffle', function () { for (let i = 0; i < raffleWinners.length; i++) { const winnerBid = await getBidByID(raffleWinners[i].toNumber()) + const winType = await auctionRaffle.getBidWinType(winnerBid.bidderID) if (i === 0) { - expect(winnerBid.winType).to.be.equal(WinType.goldenTicket) + expect(winType).to.be.equal(WinType.goldenTicket) continue } - expect(winnerBid.winType).to.be.equal(WinType.raffle) + expect(winType).to.be.equal(WinType.raffle) } } }) @@ -583,30 +642,26 @@ describe('AuctionRaffle', function () { await endBidding(auctionRaffleAsOwner) await settleAuction() - await expect(auctionRaffle.claim(4)) - .to.be.revertedWith('AuctionRaffle: is in invalid state') + await expect(auctionRaffle.claim(4)).to.be.revertedWith('AuctionRaffle: is in invalid state') }) it('reverts if bidder does not exist', async function () { await bidAndSettleRaffle(2) - await expect(auctionRaffle.claim(20)) - .to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') + await expect(auctionRaffle.claim(20)).to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') }) it('reverts if funds have been already claimed', async function () { await bidAndSettleRaffle(4) await auctionRaffle.claim(4) - await expect(auctionRaffle.claim(4)) - .to.be.revertedWith('AuctionRaffle: funds have already been claimed') + await expect(auctionRaffle.claim(4)).to.be.revertedWith('AuctionRaffle: funds have already been claimed') }) it('reverts if auction winner wants to claim funds', async function () { await bidAndSettleRaffle(9) - await expect(auctionRaffle.claim(1)) - .to.be.revertedWith('AuctionRaffle: auction winners cannot claim funds') + await expect(auctionRaffle.claim(1)).to.be.revertedWith('AuctionRaffle: auction winners cannot claim funds') }) it('sets bid as claimed', async function () { @@ -620,7 +675,7 @@ describe('AuctionRaffle', function () { it('transfers remaining funds for raffle winner', async function () { await bid(9) // place 9 bids = reservePrice - await bidAsWallet(owner(), reservePrice) // bumps owner bid to become auction winner + await bidOrBumpWithAttestation(reservePrice, owner()) // bumps owner bid to become auction winner await bidAndSettleRaffle(9) // bumps all 9 bids const raffleBid = await getBidByWinType(9, WinType.raffle) // get any raffle winner const bidderAddress = await auctionRaffleAsOwner.getBidderAddress(raffleBid.bidderID) @@ -632,7 +687,7 @@ describe('AuctionRaffle', function () { }) it('transfers bid funds for golden ticket winner', async function () { - await bidAsWallet(owner(), reservePrice) + await bidOrBumpWithAttestation(reservePrice, owner()) await bidAndSettleRaffle(10) const goldenBid = await getBidByWinType(10, WinType.goldenTicket) @@ -647,14 +702,15 @@ describe('AuctionRaffle', function () { }) it('transfers bid funds for non-winning bidder', async function () { - await bidAsWallet(owner(), reservePrice) + await bidOrBumpWithAttestation(reservePrice, owner()) await bidAndSettleRaffle(10) const lostBid = await getBidByWinType(10, WinType.loss) const bidderAddress = await auctionRaffleAsOwner.getBidderAddress(lostBid.bidderID) const bidderBalance = await provider.getBalance(bidderAddress) - const expectedBidderBalance = bidderBalance.add(reservePrice.mul(98).div(100)) + // NB: There is no longer a 2% non-winner fee retained + const expectedBidderBalance = bidderBalance.add(reservePrice) await auctionRaffleAsOwner.claim(lostBid.bidderID) @@ -665,8 +721,7 @@ describe('AuctionRaffle', function () { describe('claimProceeds', function () { describe('when called not by owner', function () { it('reverts', async function () { - await expect(auctionRaffle.claimProceeds()) - .to.be.revertedWith('Ownable: caller is not the owner') + await expect(auctionRaffle.claimProceeds()).to.be.revertedWith('Ownable: caller is not the owner') }) }) @@ -675,15 +730,16 @@ describe('AuctionRaffle', function () { await bidAndSettleRaffle(2) await auctionRaffleAsOwner.claimProceeds() - await expect(auctionRaffleAsOwner.claimProceeds()) - .to.be.revertedWith('AuctionRaffle: proceeds have already been claimed') + await expect(auctionRaffleAsOwner.claimProceeds()).to.be.revertedWith( + 'AuctionRaffle: proceeds have already been claimed' + ) }) }) describe('when biddersCount > (auctionWinnersCount + raffleWinnersCount)', function () { it('transfers correct amount', async function () { const auctionBidAmount = reservePrice.add(100) - await bidAsWallet(wallets[10], auctionBidAmount) + await bidOrBumpWithAttestation(auctionBidAmount, wallets[10]) await bidAndSettleRaffle(10) const claimAmount = auctionBidAmount.add(reservePrice.mul(7)) @@ -693,12 +749,14 @@ describe('AuctionRaffle', function () { describe('when biddersCount == (auctionWinnersCount + raffleWinnersCount)', function () { it('transfers correct amount', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2, raffleWinnersCount: 8 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2, raffleWinnersCount: 8 }) + )) auctionRaffleAsOwner = auctionRaffle.connect(owner()) const auctionBidAmount = reservePrice.add(100) - await bidAsWallet(wallets[8], auctionBidAmount) - await bidAsWallet(wallets[9], auctionBidAmount) + await bidOrBumpWithAttestation(auctionBidAmount, wallets[8]) + await bidOrBumpWithAttestation(auctionBidAmount, wallets[9]) await bidAndSettleRaffle(8) const claimAmount = auctionBidAmount.mul(2).add(reservePrice.mul(7)) @@ -708,11 +766,13 @@ describe('AuctionRaffle', function () { describe('when raffleWinnersCount < biddersCount < (auctionWinnersCount + raffleWinnersCount)', function () { it('transfers correct amount', async function () { - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ auctionWinnersCount: 2, raffleWinnersCount: 8 }))) + ;({ auctionRaffle, attestor, scoreAttestationVerifier } = await loadFixture( + configuredAuctionRaffleFixture({ auctionWinnersCount: 2, raffleWinnersCount: 8 }) + )) auctionRaffleAsOwner = auctionRaffle.connect(owner()) const auctionBidAmount = reservePrice.add(100) - await bidAsWallet(wallets[8], auctionBidAmount) + await bidOrBumpWithAttestation(auctionBidAmount, wallets[8]) await bidAndSettleRaffle(8) const claimAmount = auctionBidAmount.add(reservePrice.mul(7)) @@ -760,15 +820,15 @@ describe('AuctionRaffle', function () { describe('withdrawUnclaimedFunds', function () { it('reverts if called not by owner', async function () { - await expect(auctionRaffle.withdrawUnclaimedFunds()) - .to.be.revertedWith('Ownable: caller is not the owner') + await expect(auctionRaffle.withdrawUnclaimedFunds()).to.be.revertedWith('Ownable: caller is not the owner') }) it('reverts if claiming has not been closed yet', async function () { await bidAndSettleRaffle(2) - await expect(auctionRaffleAsOwner.withdrawUnclaimedFunds()) - .to.be.revertedWith('AuctionRaffle: is in invalid state') + await expect(auctionRaffleAsOwner.withdrawUnclaimedFunds()).to.be.revertedWith( + 'AuctionRaffle: is in invalid state' + ) }) it('transfers unclaimed funds', async function () { @@ -808,21 +868,23 @@ describe('AuctionRaffle', function () { let exampleToken: ExampleToken beforeEach(async function () { - ({ exampleToken, auctionRaffle, provider } = await loadFixture(auctionRaffleFixtureWithToken)) + ;({ exampleToken, auctionRaffle, provider: provider as any } = await loadFixture(auctionRaffleFixtureWithToken)) auctionRaffleAsOwner = auctionRaffle.connect(owner()) }) describe('when called not by owner', function () { it('reverts', async function () { - await expect(auctionRaffle.rescueTokens(exampleToken.address)) - .to.be.revertedWith('Ownable: caller is not the owner') + await expect(auctionRaffle.rescueTokens(exampleToken.address)).to.be.revertedWith( + 'Ownable: caller is not the owner' + ) }) }) describe('when balance for given token equals zero', function () { it('reverts', async function () { - await expect(auctionRaffleAsOwner.rescueTokens(exampleToken.address)) - .to.be.revertedWith('AuctionRaffle: no tokens for given address') + await expect(auctionRaffleAsOwner.rescueTokens(exampleToken.address)).to.be.revertedWith( + 'AuctionRaffle: no tokens for given address' + ) }) }) @@ -838,8 +900,9 @@ describe('AuctionRaffle', function () { describe('fallback', function () { describe('when transfers ether without calldata', function () { it('reverts', async function () { - await expect(owner().sendTransaction({ to: auctionRaffle.address, value: parseEther('1') })) - .to.be.revertedWith('AuctionRaffle: contract accepts ether transfers only by bid method') + await expect(owner().sendTransaction({ to: auctionRaffle.address, value: parseEther('1') })).to.be.revertedWith( + 'AuctionRaffle: contract accepts ether transfers only by bid method' + ) }) }) @@ -850,113 +913,28 @@ describe('AuctionRaffle', function () { value: parseEther('1'), data: '0x7D86687F980A56b832e9378952B738b614A99dc6', } - await expect(owner().sendTransaction(params)) - .to.be.revertedWith('AuctionRaffle: contract accepts ether transfers only by bid method') - }) - }) - }) - - describe('claimFees', function () { - describe('when called not by owner', function () { - it('reverts', async function () { - await expect(auctionRaffle.claimFees(10)) - .to.be.revertedWith('Ownable: caller is not the owner') - }) - }) - - describe('when raffle has not been settled yet', function () { - it('reverts', async function () { - await bid(2) - await expect(auctionRaffleAsOwner.claimFees(2)) - .to.be.revertedWith('AuctionRaffle: is in invalid state') - }) - }) - - describe('when fees have already been claimed', function () { - it('reverts', async function () { - await bidAndSettleRaffle(10) - await auctionRaffleAsOwner.claimFees(1) - - await expect(auctionRaffleAsOwner.claimFees(1)) - .to.be.revertedWith('AuctionRaffle: fees have already been claimed') - }) - }) - - describe('when there are no non-winning bids', function () { - it('reverts', async function () { - await bidAndSettleRaffle(6) - - await expect(auctionRaffleAsOwner.claimFees(6)) - .to.be.revertedWith('AuctionRaffle: there are no fees to claim') - }) - }) - - describe('when claiming using multiple transactions', function () { - it('transfers correct amount', async function () { - await bidAndSettleRaffle(15) - const singleBidFee = calculateFee(reservePrice) - - let claimAmount = singleBidFee.mul(2) - expect(await claimFees(2)).to.be.equal(claimAmount) - - claimAmount = singleBidFee.mul(4) - expect(await claimFees(4)).to.be.equal(claimAmount) - }) - }) - - describe('when claiming using single transactions', function () { - it('transfers correct amount', async function () { - const additionalBidAmount = parseEther('0.1') - for (let i = 0; i < 16; i++) { - await bidAsWallet(wallets[i], reservePrice.add(additionalBidAmount.mul(i))) - } - await bidAndSettleRaffle(0) - - const bids = await getAllBidsByWinType(16, WinType.loss) - let claimAmount = Zero - bids.forEach((bid) => { - claimAmount = claimAmount.add(calculateFee(bid.amount)) - }) - - expect(await claimFees(bids.length)).to.be.equal(claimAmount) + await expect(owner().sendTransaction(params)).to.be.revertedWith( + 'AuctionRaffle: contract accepts ether transfers only by bid method' + ) }) }) - - describe('when bid amount is not divisible by 100', function () { - it('transfers correct amount with remainder', async function () { - const bidAmount = reservePrice.add(21) - await bid(8) - await bidAsWallet(wallets[9], bidAmount) - await bidAsWallet(wallets[10], reservePrice.mul(2)) - - // Non-winning bidderID from random number: 9 - await bidAndSettleRaffle(0, [10]) - - expect(await claimFees(1)).to.be.equal(calculateFee(bidAmount)) - }) - }) - - // Returns amount transferred to owner by claimFees method - async function claimFees(bidsNumber: number): Promise { - return calculateTransferredAmount(() => auctionRaffleAsOwner.claimFees(bidsNumber)) - } - - function calculateFee(bidAmount: BigNumber): BigNumber { - return bidAmount.sub(bidAmount.mul(98).div(100)) - } }) describe('getState', function () { it('waiting for bidding', async function () { - const currentTime = await getLatestBlockTimestamp(provider); - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ biddingStartTime: currentTime + MINUTE }))) + const currentTime = await getLatestBlockTimestamp(provider) + ;({ auctionRaffle } = await loadFixture( + configuredAuctionRaffleFixture({ biddingStartTime: currentTime + MINUTE }) + )) expect(await auctionRaffle.getState()).to.be.equal(State.awaitingBidding) }) it('bidding open', async function () { - const currentTime = await getLatestBlockTimestamp(provider); - ({ auctionRaffle } = await loadFixture(configuredAuctionRaffleFixture({ biddingStartTime: currentTime - MINUTE }))) + const currentTime = await getLatestBlockTimestamp(provider) + ;({ auctionRaffle } = await loadFixture( + configuredAuctionRaffleFixture({ biddingStartTime: currentTime - MINUTE }) + )) expect(await auctionRaffle.getState()).to.be.equal(State.biddingOpen) }) @@ -980,58 +958,57 @@ describe('AuctionRaffle', function () { describe('getBid', function () { it('reverts for unknown bidder address', async function () { - await expect(auctionRaffle.getBid(randomAddress())) - .to.be.revertedWith('AuctionRaffle: no bid by given address') + await expect(auctionRaffle.getBid(randomAddress())).to.be.revertedWith('AuctionRaffle: no bid by given address') }) it('returns bid details', async function () { await bid(1) - const { bidderID, amount, winType, claimed } = await auctionRaffle.getBid(wallets[0].address) + const { bidderID, amount, claimed } = await auctionRaffle.getBid(wallets[0].address) expect(bidderID).to.eq(1) expect(amount).to.eq(reservePrice) - expect(winType).to.eq(0) + expect(await auctionRaffle.getBidWinType(bidderID)).to.eq(0) expect(claimed).to.be.false }) }) describe('getBidByID', function () { it('reverts for zero bidder ID', async function () { - await expect(auctionRaffle.getBidByID(0)) - .to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') + await expect(auctionRaffle.getBidByID(0)).to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') }) it('reverts for invalid bidder ID', async function () { await bid(1) - await expect(auctionRaffle.getBidByID(2)) - .to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') + await expect(auctionRaffle.getBidByID(2)).to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') }) it('returns bidder address', async function () { await bid(1) - const { bidderID, amount, winType, claimed } = await auctionRaffle.getBidByID(1) + const { bidderID, amount, claimed } = await auctionRaffle.getBidByID(1) expect(bidderID).to.eq(1) expect(amount).to.eq(reservePrice) - expect(winType).to.eq(0) + expect(await auctionRaffle.getBidWinType(bidderID)).to.eq(0) expect(claimed).to.be.false }) }) describe('getBidWithAddress', function () { it('reverts for zero bidder ID', async function () { - await expect(auctionRaffle.getBidWithAddress(0)) - .to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') + await expect(auctionRaffle.getBidWithAddress(0)).to.be.revertedWith( + 'AuctionRaffle: bidder with given ID does not exist' + ) }) it('reverts for invalid bidder ID', async function () { await bid(1) - await expect(auctionRaffle.getBidWithAddress(2)) - .to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') + await expect(auctionRaffle.getBidWithAddress(2)).to.be.revertedWith( + 'AuctionRaffle: bidder with given ID does not exist' + ) }) it('returns correct bid with bidder address', async function () { await bid(1) const bidWithAddress = await auctionRaffle.getBidWithAddress(1) - validateBidsWithAddresses([bidWithAddress]) + await validateBidsWithAddresses([bidWithAddress]) }) }) @@ -1044,20 +1021,22 @@ describe('AuctionRaffle', function () { await bid(3) const bids = await auctionRaffle.getBidsWithAddresses() expect(bids).to.be.of.length(3) - validateBidsWithAddresses(bids) + await validateBidsWithAddresses(bids) }) }) describe('getBidderAddress', function () { it('reverts for zero bidder ID', async function () { - await expect(auctionRaffle.getBidderAddress(0)) - .to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') + await expect(auctionRaffle.getBidderAddress(0)).to.be.revertedWith( + 'AuctionRaffle: bidder with given ID does not exist' + ) }) it('reverts for invalid bidder ID', async function () { await bid(1) - await expect(auctionRaffle.getBidderAddress(2)) - .to.be.revertedWith('AuctionRaffle: bidder with given ID does not exist') + await expect(auctionRaffle.getBidderAddress(2)).to.be.revertedWith( + 'AuctionRaffle: bidder with given ID does not exist' + ) }) it('returns bidder address', async function () { @@ -1070,23 +1049,36 @@ describe('AuctionRaffle', function () { return wallets[1] } - function validateBidsWithAddresses(bids) { - bids.forEach(({ bidder, bid: bid_ }, index) => { - expect(bidder).to.eq(wallets[index].address) - expect(bid_.bidderID).to.eq(index + 1) + async function validateBidsWithAddresses(bids: { bidder: string; bid: Bid }[]) { + for (let i = 0; i < bids.length; i++) { + const { bidder, bid: bid_ } = bids[i] + expect(bidder).to.eq(wallets[i].address) + expect(bid_.bidderID).to.eq(i + 1) expect(bid_.amount).to.eq(reservePrice) - expect(bid_.winType).to.eq(0) + expect(await auctionRaffle.getBidWinType(bid_.bidderID)).to.eq(0) expect(bid_.claimed).to.be.false - }) + } } - async function bidAndSettleRaffle(bidCount: number, randomNumbers?: BigNumberish[]): Promise { + async function bidAndSettleRaffle(bidCount: number, randomNumber?: BigNumberish): Promise { await bid(bidCount) await endBidding(auctionRaffleAsOwner) await settleAuction() - const numbers = randomNumbers || randomBigNumbers(1) - return auctionRaffleAsOwner.settleRaffle(numbers) + const numbers = randomNumber || randomBN() + return settleAndFulfillRaffle(numbers) + } + + async function settleAndFulfillRaffle(randomNumber: BigNumberish) { + const settleTx = await auctionRaffleAsOwner.settleRaffle() + expect(settleTx).to.emit(auctionRaffleAsOwner, 'RandomnessRequested') + const requestId = await auctionRaffleAsOwner.requestId() + expect(requestId).to.not.eq(0) + const tx = await vrfCoordinator.fulfillRandomWords(requestId, auctionRaffleAsOwner.address, [randomNumber], { + gasLimit: 2_500_000, + }) + expect(await auctionRaffleAsOwner.getState()).to.eq(State.raffleSettled) + return tx } async function endBidding(auctionRaffle: AuctionRaffleMock) { @@ -1101,12 +1093,46 @@ describe('AuctionRaffle', function () { async function bid(walletCount: number) { for (let i = 0; i < walletCount; i++) { - await bidAsWallet(wallets[i], reservePrice) + await bidOrBumpWithAttestation(reservePrice, wallets[i]) + } + } + + /** + * Bid or bump as an eligible wallet + * @param value Amount to bid or bump + * @param wallet Optional wallet to bid with + */ + async function bidOrBumpWithAttestation(value: BigNumberish, wallet?: Wallet) { + // Create attestation that this wallet is eligible + expect(await scoreAttestationVerifier.attestor()).to.eq(attestor.address, 'Unexpected attestor') + const score = 21 * 10 ** 8 // 21.0 + const contract = wallet ? auctionRaffle.connect(wallet) : auctionRaffle + const subject = await contract.signer.getAddress() + const { signature } = await attestScore(subject, score, attestor, scoreAttestationVerifier.address) + try { + const { amount } = await contract.getBid(subject) + expect(amount.gt(0)).to.eq(true) // sanity + // existing bid -> bump + return contract.bump({ value }) + } catch (err) { + // bid + return contract.bid(score, signature, { value }) } } - async function bidAsWallet(wallet: Wallet, value: BigNumberish) { - await auctionRaffle.connect(wallet).bid({ value }) + /** + * Bid as eligible wallet (will not try to bump) + * @param value Amount to bid or bump + * @param wallet Optional wallet to bid with + */ + async function bidWithAttestation(value: BigNumberish, wallet?: Wallet) { + // Create attestation that this wallet is eligible + expect(await scoreAttestationVerifier.attestor()).to.eq(attestor.address, 'Unexpected attestor') + const score = 21 * 10 ** 8 // 21.0 + const contract = wallet ? auctionRaffle.connect(wallet) : auctionRaffle + const subject = await contract.signer.getAddress() + const { signature } = await attestScore(subject, score, attestor, scoreAttestationVerifier.address) + return contract.bid(score, signature, { value }) } async function getBidByID(bidID: number): Promise { @@ -1117,7 +1143,8 @@ describe('AuctionRaffle', function () { async function getBidByWinType(bidCount: number, winType: WinType): Promise { for (let i = 1; i <= bidCount; i++) { const bid = await getBidByID(i) - if (bid.winType === winType) { + const bidWinType = await auctionRaffle.getBidWinType(bid.bidderID) + if (bidWinType === winType) { return bid } } @@ -1127,7 +1154,8 @@ describe('AuctionRaffle', function () { const bids = [] for (let i = 1; i <= bidCount; i++) { const bid = await getBidByID(i) - if (bid.winType === winType) { + const bidWinType = await auctionRaffle.getBidWinType(bid.bidderID) + if (bidWinType === winType) { bids.push(bid) } } diff --git a/packages/contracts/test/contracts/FeistelShuffle.test.ts b/packages/contracts/test/contracts/FeistelShuffle.test.ts new file mode 100644 index 000000000..b2e620232 --- /dev/null +++ b/packages/contracts/test/contracts/FeistelShuffle.test.ts @@ -0,0 +1,175 @@ +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { FeistelShuffleConsumer, FeistelShuffleConsumer__factory } from '../../build' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber, BigNumberish } from 'ethers' +import { randomBytes } from 'crypto' +import * as tsFeistel from '@kevincharm/gfc-fpe' +import { solidityKeccak256 } from 'ethers/lib/utils' + +const f = (R: bigint, i: bigint, seed: bigint, domain: bigint) => + BigNumber.from(solidityKeccak256(['uint256', 'uint256', 'uint256', 'uint256'], [R, i, seed, domain])).toBigInt() + +describe('FeistelShuffle', () => { + let deployer: SignerWithAddress + let feistelShuffle: FeistelShuffleConsumer + let indices: number[] + let seed: string + before(async () => { + const signers = await ethers.getSigners() + deployer = signers[0] + feistelShuffle = await new FeistelShuffleConsumer__factory(deployer).deploy() + indices = Array(100) + .fill(0) + .map((_, i) => i) + seed = ethers.utils.defaultAbiCoder.encode(['bytes32'], ['0x' + randomBytes(32).toString('hex')]) + }) + + function assertSetEquality(left: number[], right: number[]) { + const set = new Set() + for (const l of left) { + set.add(l) + } + expect(set.size).to.equal(left.length) + for (const r of right) { + expect(set.delete(r)).to.equal(true, `${r} exists in left`) + } + expect(set.size).to.equal(0) + } + + /** + * Same as calling `feistelShuffle.shuffle(...)`, but additionally + * checks the return value against the reference implementation and asserts + * they're equal. + * + * @param x + * @param domain + * @param seed + * @param rounds + * @returns + */ + async function checkedShuffle(x: BigNumberish, domain: BigNumberish, seed: BigNumberish, rounds: number) { + const contractRefAnswer = await feistelShuffle.shuffle(x, domain, seed, rounds) + const refAnswer = await tsFeistel.encrypt( + BigNumber.from(x).toBigInt(), + BigNumber.from(domain).toBigInt(), + BigNumber.from(seed).toBigInt(), + BigNumber.from(rounds).toBigInt(), + f + ) + expect(contractRefAnswer).to.equal(refAnswer) + // Compute x from x' using the inverse function + expect(await feistelShuffle.deshuffle(contractRefAnswer, domain, seed, rounds)).to.eq(x) + expect(await feistelShuffle.deshuffle__OPT(contractRefAnswer, domain, seed, rounds)).to.eq(x) + return contractRefAnswer + } + + it('should create permutation with FeistelShuffle', async () => { + const rounds = 4 + const shuffled: BigNumber[] = [] + for (let i = 0; i < indices.length; i++) { + const s = await feistelShuffle.shuffle__OPT(i, indices.length, seed, rounds) + shuffled.push(s) + } + assertSetEquality( + indices, + shuffled.map((s) => s.toNumber()) + ) + }) + + it('should match reference implementation', async () => { + const rounds = 4 + const shuffled: number[] = [] + for (const i of indices) { + // Test both unoptimised & optimised versions + const s = await checkedShuffle(i, indices.length, seed, rounds) + // Test that optimised Yul version spits out the same output + const sOpt = await feistelShuffle.shuffle__OPT(i, indices.length, seed, rounds) + expect(s).to.equal(sOpt) + shuffled.push(sOpt.toNumber()) + } + + const specOutput: number[] = [] + for (const index of indices) { + const xPrime = await tsFeistel.encrypt( + BigInt(index), + BigInt(indices.length), + BigNumber.from(seed).toBigInt(), + BigNumber.from(rounds).toBigInt(), + f + ) + specOutput.push(Number(xPrime)) + } + + expect(shuffled).to.deep.equal(specOutput) + }) + + it('should revert if x >= modulus', async () => { + const rounds = 4 + // on boundary + await expect(feistelShuffle.shuffle(100, 100, seed, rounds)).to.be.revertedWith('x too large') + await expect(feistelShuffle.shuffle__OPT(100, 100, seed, rounds)).to.be.reverted + // past boundary + await expect(feistelShuffle.shuffle(101, 100, seed, rounds)).to.be.revertedWith('x too large') + await expect(feistelShuffle.shuffle__OPT(101, 100, seed, rounds)).to.be.reverted + }) + + it('should revert if modulus == 0', async () => { + const rounds = 4 + await expect(feistelShuffle.shuffle(0, 0, seed, rounds)).to.be.revertedWith('modulus must be > 0') + await expect(feistelShuffle.shuffle__OPT(0, 0, seed, rounds)).to.be.reverted + }) + + it('should handle small modulus', async () => { + // This is mainly to ensure the sqrt / nextPerfectSquare functions are correct + const rounds = 4 + + // list size of 1 + let modulus = 1 + const permutedOneRef = await checkedShuffle(0, modulus, seed, rounds) + expect(permutedOneRef).to.equal(0) + expect(permutedOneRef).to.equal(await feistelShuffle.shuffle__OPT(0, modulus, seed, rounds)) + + // list size of 2 + modulus = 2 + const shuffledTwo = new Set() + for (let i = 0; i < modulus; i++) { + shuffledTwo.add((await checkedShuffle(i, modulus, seed, rounds)).toNumber()) + } + // |shuffledSet| = modulus + expect(shuffledTwo.size).to.equal(modulus) + // set equality with optimised version + for (let i = 0; i < modulus; i++) { + shuffledTwo.delete((await feistelShuffle.shuffle__OPT(i, modulus, seed, rounds)).toNumber()) + } + expect(shuffledTwo.size).to.equal(0) + + // list size of 3 + modulus = 3 + const shuffledThree = new Set() + for (let i = 0; i < modulus; i++) { + shuffledThree.add((await checkedShuffle(i, modulus, seed, rounds)).toNumber()) + } + // |shuffledSet| = modulus + expect(shuffledThree.size).to.equal(modulus) + // set equality with optimised version + for (let i = 0; i < modulus; i++) { + shuffledThree.delete((await feistelShuffle.shuffle__OPT(i, modulus, seed, rounds)).toNumber()) + } + expect(shuffledThree.size).to.equal(0) + + // list size of 4 (past boundary) + modulus = 4 + const shuffledFour = new Set() + for (let i = 0; i < modulus; i++) { + shuffledFour.add((await checkedShuffle(i, modulus, seed, rounds)).toNumber()) + } + // |shuffledSet| = modulus + expect(shuffledFour.size).to.equal(modulus) + // set equality with optimised version + for (let i = 0; i < modulus; i++) { + shuffledFour.delete((await feistelShuffle.shuffle__OPT(i, modulus, seed, rounds)).toNumber()) + } + expect(shuffledFour.size).to.equal(0) + }) +}) diff --git a/packages/contracts/test/contracts/ScoreAttestationVerifier.test.ts b/packages/contracts/test/contracts/ScoreAttestationVerifier.test.ts new file mode 100644 index 000000000..b0150846d --- /dev/null +++ b/packages/contracts/test/contracts/ScoreAttestationVerifier.test.ts @@ -0,0 +1,85 @@ +import { setupFixtureLoader } from '../setup' +import { expect } from 'chai' +import { ScoreAttestationVerifier, ScoreAttestationVerifier__factory } from 'contracts' +import { attestScore } from 'utils/attestScore' +import { BigNumberish, Signer, Wallet, ethers } from 'ethers' +import { MockProvider } from 'ethereum-waffle' +import { _TypedDataEncoder, defaultAbiCoder as abi } from 'ethers/lib/utils' + +describe('ScoreAttestationVerifier', function () { + const loadFixture = setupFixtureLoader() + + let attestor: Wallet + let scoreAttestationVerifier: ScoreAttestationVerifier + + function createScoreAttestationVerifierFixture(opts: ScoreAttestationVerifierFixtureOptions = {}) { + return async (wallets: Wallet[], _provider: MockProvider) => { + const deployer = opts.deployer || wallets[0] + const version = opts.version || '1' + const requiredScore = opts.initialRequiredScore || 20 * 10 ** 8 + const initialAttestor = opts.attestor || wallets[1] + const scoreAttestationVerifier = await new ScoreAttestationVerifier__factory(deployer).deploy( + version, + initialAttestor.address, + requiredScore + ) + return { + scoreAttestationVerifier, + deployer, + attestor: initialAttestor, + version, + requiredScore, + } + } + } + + it('verifies a valid attestation', async () => { + ;({ scoreAttestationVerifier, attestor } = await loadFixture(createScoreAttestationVerifierFixture())) + + const subject = Wallet.createRandom().address + const score = '2071087800' // 20.71 + const { digest, signature } = await attestScore(subject, score, attestor, scoreAttestationVerifier.address, { + chainId: '31337', + version: '1', + }) + expect(ethers.utils.recoverAddress(digest, signature)).to.eq(await scoreAttestationVerifier.attestor()) // Sanity + const payload = abi.encode(['address', 'uint256'], [subject, score]) + await expect(scoreAttestationVerifier.verify(payload, signature)).to.not.be.reverted + }) + + it('rejects an attestation from unauthorised attestor', async () => { + ;({ scoreAttestationVerifier, attestor } = await loadFixture(createScoreAttestationVerifierFixture())) + + const subject = Wallet.createRandom().address + const score = '2000000000' // 20 + const wrongAttestor = Wallet.createRandom() + const { digest, signature } = await attestScore(subject, score, wrongAttestor, scoreAttestationVerifier.address, { + chainId: '31337', + version: '1', + }) + expect(ethers.utils.recoverAddress(digest, signature)).to.not.eq(await scoreAttestationVerifier.attestor()) // Sanity + const payload = abi.encode(['address', 'uint256'], [subject, score]) + await expect(scoreAttestationVerifier.verify(payload, signature)).to.be.revertedWith('Unauthorised attestor') + }) + + it('rejects if minimum score not met', async () => { + ;({ scoreAttestationVerifier, attestor } = await loadFixture(createScoreAttestationVerifierFixture())) + + const subject = Wallet.createRandom().address + const score = '1999999999' // 19.99999999 + const { digest, signature } = await attestScore(subject, score, attestor, scoreAttestationVerifier.address, { + chainId: '31337', + version: '1', + }) + expect(ethers.utils.recoverAddress(digest, signature)).to.eq(await scoreAttestationVerifier.attestor()) // Sanity + const payload = abi.encode(['address', 'uint256'], [subject, score]) + await expect(scoreAttestationVerifier.verify(payload, signature)).to.be.revertedWith('Score too low') + }) +}) + +interface ScoreAttestationVerifierFixtureOptions { + deployer?: Signer + attestor?: Wallet + version?: string + initialRequiredScore?: BigNumberish +} diff --git a/packages/contracts/test/contracts/bid.ts b/packages/contracts/test/contracts/bid.ts index c68d06006..d799e6b63 100644 --- a/packages/contracts/test/contracts/bid.ts +++ b/packages/contracts/test/contracts/bid.ts @@ -3,6 +3,7 @@ import { BigNumber } from 'ethers' export interface Bid { bidderID: BigNumber, amount: BigNumber, - winType: number, + isAuctionWinner: boolean, claimed: boolean, + raffleParticipantIndex: BigNumber, } diff --git a/packages/contracts/test/fixtures/auctionRaffleFixture.ts b/packages/contracts/test/fixtures/auctionRaffleFixture.ts index 1c18fe0c3..0c5f4b500 100644 --- a/packages/contracts/test/fixtures/auctionRaffleFixture.ts +++ b/packages/contracts/test/fixtures/auctionRaffleFixture.ts @@ -1,8 +1,15 @@ -import { AuctionRaffleMock__factory, ExampleToken__factory } from 'contracts' +import { + AuctionRaffleMock__factory, + ExampleToken__factory, + MockLinkToken__factory, + ScoreAttestationVerifier__factory, + VrfCoordinatorV2MockWithErc677__factory, +} from 'contracts' import { BigNumberish, utils, Wallet } from 'ethers' import { MockProvider } from 'ethereum-waffle' import { getLatestBlockTimestamp } from 'utils/getLatestBlockTimestamp' import { WEEK } from 'scripts/utils/consts' +import { parseEther, parseUnits, solidityKeccak256 } from 'ethers/lib/utils' export const auctionWinnersCount = 1 export const raffleWinnersCount = 8 @@ -10,14 +17,14 @@ export const reservePrice = utils.parseEther('0.5') export const minBidIncrement = utils.parseEther('0.005') export type auctionRaffleParams = { - initialOwner?: string, - biddingStartTime?: number, - biddingEndTime?: number, - claimingEndTime?: number, - auctionWinnersCount?: number, - raffleWinnersCount?: number, - reservePrice?: BigNumberish, - minBidIncrement?: BigNumberish, + initialOwner?: string + biddingStartTime?: number + biddingEndTime?: number + claimingEndTime?: number + auctionWinnersCount?: number + raffleWinnersCount?: number + reservePrice?: BigNumberish + minBidIncrement?: BigNumberish } export function auctionRaffleFixture(wallets: Wallet[], provider: MockProvider) { @@ -39,27 +46,85 @@ export async function auctionRaffleE2EFixture(wallets: Wallet[], provider: MockP })(wallets, provider) } -export function configuredAuctionRaffleFixture(params: auctionRaffleParams) { +export function configuredAuctionRaffleFixture(configParams: auctionRaffleParams) { return async ([deployer, owner]: Wallet[], provider: MockProvider) => { - const currentBlockTimestamp = await getLatestBlockTimestamp(provider) - params = setAuctionRaffleParamsDefaults(owner, currentBlockTimestamp, params) + const currentBlockTimestamp = await getLatestBlockTimestamp(provider as any) + configParams = setAuctionRaffleParamsDefaults(owner, currentBlockTimestamp, configParams) + + // Mock mintable LINK token + const linkToken = await new MockLinkToken__factory(deployer).deploy() + await linkToken.grantMintAndBurnRoles(deployer.address) + await linkToken.mint(deployer.address, parseEther('1000')) + + const vrfCoordinator = await new VrfCoordinatorV2MockWithErc677__factory(deployer).deploy( + parseEther('0.005'), + parseUnits('1', 'gwei'), + linkToken.address + ) + // Create sub + const subId = await vrfCoordinator.callStatic.createSubscription() + await vrfCoordinator.createSubscription() + + // Fund sub + await linkToken.transferAndCall( + vrfCoordinator.address, + parseEther('100'), + utils.defaultAbiCoder.encode(['uint64'], [subId]) + ) + + const vrfRequesterParams = { + vrfCoordinator: vrfCoordinator.address, + linkToken: linkToken.address, + linkPremium: parseEther('0.005'), + gasLaneKeyHash: '0x72d2b016bb5b62912afea355ebf33b91319f828738b111b723b78696b9847b63', // 30 gwei + callbackGasLimit: 10_000_000, // maximum + minConfirmations: 1, // minimum + subId, + } + + // Deploy an attestation verifier + const attestor = Wallet.createRandom() + const scoreAttestationVerifier = await new ScoreAttestationVerifier__factory(deployer).deploy( + '1', + attestor.address, + 20 * 10 ** 8 // 20.0 + ) const auctionRaffle = await new AuctionRaffleMock__factory(deployer).deploy( - params.initialOwner, - params.biddingStartTime, - params.biddingEndTime, - params.claimingEndTime, - params.auctionWinnersCount, - params.raffleWinnersCount, - params.reservePrice, - params.minBidIncrement, + configParams.initialOwner, + { + biddingStartTime: configParams.biddingStartTime, + biddingEndTime: configParams.biddingEndTime, + claimingEndTime: configParams.claimingEndTime, + auctionWinnersCount: configParams.auctionWinnersCount, + raffleWinnersCount: configParams.raffleWinnersCount, + reservePrice: configParams.reservePrice, + minBidIncrement: configParams.minBidIncrement, + bidVerifier: scoreAttestationVerifier.address, + }, + vrfRequesterParams ) - return { provider, auctionRaffle } + // Whitelist auctionRaffle as VRF consumer on this subscription + await vrfCoordinator.addConsumer(subId, auctionRaffle.address) + + return { + provider, + auctionRaffle, + vrfCoordinator, + subId, + linkToken, + attestor, + scoreAttestationVerifier, + } } } -export function setAuctionRaffleParamsDefaults(owner: Wallet, blockTimestamp: number, params: auctionRaffleParams): auctionRaffleParams { +export function setAuctionRaffleParamsDefaults( + owner: Wallet, + blockTimestamp: number, + params: auctionRaffleParams +): auctionRaffleParams { return { ...defaultAuctionRaffleParams(owner, blockTimestamp), ...params } } diff --git a/packages/contracts/test/utils/attestScore.ts b/packages/contracts/test/utils/attestScore.ts new file mode 100644 index 000000000..56b9a354e --- /dev/null +++ b/packages/contracts/test/utils/attestScore.ts @@ -0,0 +1,48 @@ +import { BigNumberish, Wallet } from 'ethers' +import { _TypedDataEncoder, solidityPack, splitSignature } from 'ethers/lib/utils' + +export interface AttestScoreOptions { + version?: string + chainId?: string +} + +export async function attestScore( + subject: string, + score: BigNumberish, + attestor: Wallet, + scoreAttestationVerifierAddress: string, + opts: AttestScoreOptions = {} +) { + const version = opts.version || '1' + const chainId = opts.chainId || '31337' + const domain = { + name: 'ScoreAttestationVerifier', + version, + chainId, + verifyingContract: scoreAttestationVerifierAddress, + } + const types = { + // Score(address subject,uint256 score) + Score: [ + { + name: 'subject', + type: 'address', + }, + { + name: 'score', + type: 'uint256', + }, + ], + } + const values = { + subject, + score, + } + + const digest = _TypedDataEncoder.hash(domain, types, values) + const signature = await attestor._signTypedData(domain, types, values) + return { + digest, + signature, + } +} diff --git a/packages/frontend/.env b/packages/frontend/.env new file mode 100644 index 000000000..bb06196b8 --- /dev/null +++ b/packages/frontend/.env @@ -0,0 +1,4 @@ +NEXT_PUBLIC_INFURA_KEY=YOUR_INFURA_KEY +NEXT_PUBLIC_VOUCHER_REDEEM_DEADLINE=1714694400000 +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=YOUR_WALLETCONNECT_PROJECT_ID +NEXT_PUBLIC_GITCOIN_REQUIRED_SCORE=20 diff --git a/packages/frontend/.env.example b/packages/frontend/.env.example new file mode 100644 index 000000000..d9a242c29 --- /dev/null +++ b/packages/frontend/.env.example @@ -0,0 +1,13 @@ +NEXT_PUBLIC_INFURA_KEY= +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= +NEXT_PUBLIC_VOUCHER_REDEEM_DEADLINE="2024-04-25T17:44:44.773Z" +RATE_LIMIT_GLOBAL=100 +RATE_LIMIT_NONCES=5 +NONCE_EXPIRY=300000 +WHITELISTED_ORIGINS="example.com,https://example.io" +AUTH_SECRET="deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" +GTC_SCORER_API_KEY="gitcoin scorer api key" +GTC_SCORER_ID="gitcoin scorer id" +SCORE_ATTESTATION_VERIFIER_ADDRESS="0x0000000000000000000000000000000000000000" +SCORE_ATTESTATION_VERIFIER_VERSION="1" +SCORE_ATTESTOR_PRIVATE_KEY="0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" diff --git a/packages/frontend/.env.test b/packages/frontend/.env.test new file mode 100644 index 000000000..972b41d54 --- /dev/null +++ b/packages/frontend/.env.test @@ -0,0 +1,4 @@ +RATE_LIMIT_GLOBAL=100 +RATE_LIMIT_NONCES=5 +NONCE_EXPIRY=1000 +AUTH_SECRET="deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" diff --git a/packages/frontend/.eslintrc.js b/packages/frontend/.eslintrc.js new file mode 100644 index 000000000..42ac52de5 --- /dev/null +++ b/packages/frontend/.eslintrc.js @@ -0,0 +1,23 @@ +module.exports = { + extends: [ + 'next', + 'turbo', + 'prettier', + 'plugin:@typescript-eslint/recommended', + ], + rules: { + 'no-console': 'off', // error -> off + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'none', + ignoreRestSiblings: true, + vars: 'all', + }, + ], + '@next/next/no-html-link-for-pages': 'off', + 'react/jsx-uses-react': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never' }], + }, +} diff --git a/packages/frontend/.eslintrc.json b/packages/frontend/.eslintrc.json deleted file mode 100644 index 6299d827a..000000000 --- a/packages/frontend/.eslintrc.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "env": { - "es6": true, - "node": true, - "mocha": true, - "browser": true - }, - "extends": [ - "plugin:@typescript-eslint/recommended", - "eslint:recommended", - "plugin:jest/recommended", - "plugin:testing-library/react", - "plugin:react-hooks/recommended" - ], - "plugins": [ - "import" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "sourceType": "module" - }, - "rules": { - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "no-redeclare": "off", - "no-unused-vars": "off", - "prefer-const": [ - "error", - { - "destructuring": "all" - } - ], - "semi": [ - "error", - "never" - ], - "no-extra-semi": "off", - "@typescript-eslint/no-extra-semi": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-non-null-asserted-optional-chain": "off", - "no-dupe-class-members": "off", - "@typescript-eslint/no-unused-vars": "error", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "import/no-unresolved": "off", - "import/order": [ - "error", - { - "groups": [ - "builtin", - "external", - "parent", - [ - "index", - "sibling" - ] - ], - "pathGroupsExcludedImportTypes": [ - "builtin" - ], - "newlines-between": "always", - "alphabetize": { - "order": "asc", - "caseInsensitive": true - } - } - ] - }, - "globals": { - "JSX": true - } -} diff --git a/packages/frontend/.gitignore b/packages/frontend/.gitignore index a547bf36d..f93dc29dd 100644 --- a/packages/frontend/.gitignore +++ b/packages/frontend/.gitignore @@ -1,24 +1,39 @@ -# Logs -logs -*.log +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? + +# local env files +.env*.local +!.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +dist/ diff --git a/packages/frontend/.prettierrc.json b/packages/frontend/.prettierrc.json index 29c575606..b08f502f5 100644 --- a/packages/frontend/.prettierrc.json +++ b/packages/frontend/.prettierrc.json @@ -1,6 +1,8 @@ { + "$schema": "https://json.schemastore.org/prettierrc", "semi": false, "singleQuote": true, "printWidth": 120, - "bracketSpacing": true + "bracketSpacing": true, + "trailingComma": "all" } diff --git a/packages/frontend/README.md b/packages/frontend/README.md index 79a51c3ae..53b9357f0 100644 --- a/packages/frontend/README.md +++ b/packages/frontend/README.md @@ -1,6 +1,26 @@ -# Frontend +# Devcon Raffle Frontend -Config requires these values to be provided by environmental variables: -- `VITE_BACKEND_URL` - url of the backend API -- `VITE_PORTIS_DAPP_ID` - secret dapp ID generated when creating a Portis project -- `VITE_VOUCHER_REDEEM_DEADLINE` - date/time of cutoff for redeeming voucher codes in the [ECMAScript date format](https://tc39.es/ecma262/#sec-date-time-string-format) +## Getting Started + +```bash +pnpm dev +``` + +## Voucher Codes + +Auction & raffle winners are able to claim voucher codes when contract is settled. You must provide an encrypted list of voucher codes in the file `src/voucherCodes.{NODE_ENV}` (one for each respective environment). To encrypt a list of voucher codes, fire up the frontend with `pnpm dev` and navigate to the `http://localhost:3000/encrypt` page. + +## Tests + +First, ensure that you have the following environment variables defined (get these from https://www.scorer.gitcoin.co/): + +``` +GTC_SCORER_API_KEY +GTC_SCORER_ID +``` + +The rest of the environment variables needed for tests are already included in `.env.test`. Then, to run the tests: + +```bash +pnpm test +``` diff --git a/packages/frontend/babel.config.js b/packages/frontend/babel.config.js deleted file mode 100644 index 47607d67c..000000000 --- a/packages/frontend/babel.config.js +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = { - presets: [ - [ - '@babel/preset-env', - { - targets: { - node: 'current', - }, - }, - ], - [ - "@babel/preset-react", - { - runtime: "automatic" - }, - ], - '@babel/preset-typescript', - ], - plugins: [ - function () { - return { - visitor: { - MetaProperty(path) { - path.replaceWithSourceString('process') - }, - }, - } - }, - ], -} diff --git a/packages/frontend/index.html b/packages/frontend/index.html deleted file mode 100644 index 783a2344a..000000000 --- a/packages/frontend/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - Devcon 6 Auction & Raffle Ticket Sale - - -
- - - diff --git a/packages/frontend/jest.config.js b/packages/frontend/jest.config.js deleted file mode 100644 index e3900e88d..000000000 --- a/packages/frontend/jest.config.js +++ /dev/null @@ -1,14 +0,0 @@ -const config = { - testEnvironment: 'jsdom', - testMatch: ['/test/**/*.test.{ts,tsx}'], - setupFilesAfterEnv: [ - '/test/setupTests.ts' - ], - "moduleDirectories": ["node_modules", "bower_components", ""], - "moduleNameMapper": { - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/stubs/filesMock.js", - "\\.(css|less)$": "/__mocks__/stubs/stylesMock.js" - } -} - -module.exports = config diff --git a/packages/frontend/next.config.mjs b/packages/frontend/next.config.mjs new file mode 100644 index 000000000..e16f97180 --- /dev/null +++ b/packages/frontend/next.config.mjs @@ -0,0 +1,30 @@ +/** @type {import('next/dist/lib/load-custom-routes').Header[]} */ +const corsHeaders = process.env.WHITELISTED_ORIGINS + ? [ + { + source: '/api/:path*', + headers: [ + { key: 'Access-Control-Allow-Credentials', value: 'true' }, + { key: 'Access-Control-Allow-Origin', value: process.env.WHITELISTED_ORIGINS }, + { key: 'Access-Control-Allow-Methods', value: 'GET,PUT,POST,DELETE,OPTIONS' }, + { + key: 'Access-Control-Allow-Headers', + value: 'Content-Type, Authorization, Cookie', + }, + ], + }, + ] + : [] + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + compiler: { + styledComponents: true, + }, + async headers() { + return [...corsHeaders] + }, +} + +export default nextConfig diff --git a/packages/frontend/package.json b/packages/frontend/package.json index ff8b5e790..40fee2fc5 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -1,73 +1,66 @@ { - "name": "@devcon-raffle/frontend", - "version": "0.0.1", + "name": "frontend", + "version": "0.1.0", "private": true, "scripts": { - "start": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "lint": "yarn lint:prettier --check && yarn lint:eslint", - "lint:fix": "yarn lint:prettier --write --loglevel warn && yarn lint:eslint --fix", - "lint:eslint": "eslint './{src,test}/**/*.{ts,tsx}' --cache", - "lint:prettier": "prettier './{src,test}/**/*.{ts,tsx}'", - "test": "jest" + "preinstall": "npx only-allow pnpm", + "frames": "frames", + "dev": "pnpm next dev", + "build": "pnpm next build", + "start": "pnpm next start", + "lint": "pnpm run lint:ts && pnpm run lint:prettier --check && next lint", + "lint:fix": "pnpm run lint:prettier --write && pnpm run lint:eslint --fix && pnpm run lint:ts", + "lint:ts": "tsc --noEmit", + "lint:prettier": "prettier './{src,__tests__}/**/*.{ts,tsx,graphql}' --cache", + "lint:eslint": "eslint './{src,__tests__}/**/*.{ts,tsx}' --cache", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run" }, "dependencies": { - "@coinbase/wallet-sdk": "^3.0.11", - "@devcon-raffle/contracts": "0.0.1", - "@ethersproject/abi": "^5.6.0", - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/constants": "^5.6.0", - "@ethersproject/hash": "^5.6.0", - "@ethersproject/providers": "~5.6.4", - "@ethersproject/units": "^5.6.0", - "@metamask/jazzicon": "^2.0.0", - "@portis/web3": "^4.0.7", - "@radix-ui/react-accordion": "^0.1.6", - "@radix-ui/react-dialog": "^0.1.7", - "@radix-ui/react-separator": "^0.1.4", - "@radix-ui/react-toast": "^0.1.1", - "@radix-ui/react-tooltip": "^0.1.7", - "@types/styled-components": "^5.1.24", - "@usedapp/core": "^1.0.2", - "@walletconnect/web3-provider": "^1.7.8", - "copy-to-clipboard": "^3.3.1", - "immutable": "^4.0.0", + "@frames.js/debugger": "^0.2.12", + "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-tooltip": "^1.0.7", + "@tanstack/react-query": "^5.28.9", + "@vercel/og": "^0.6.2", + "@web3modal/wagmi": "^4.1.9", + "copy-to-clipboard": "^3.3.3", + "expiry-set": "^1.0.0", + "frames.js": "^0.16.6", + "jose": "^5.2.4", + "lru-cache": "^10.2.1", "moment": "^2.29.1", "moment-timezone": "^0.5.34", - "react": "^17.0.2", - "react-async-hook": "^4.0.0", - "react-dom": "^17.0.2", - "react-router-dom": "^6.2.2", - "set-interval-async": "^2.0.3", - "styled-components": "^5.3.3", - "use-async-effect": "^2.2.5", - "web3modal": "^1.9.7" + "next": "14.1.4", + "pino": "^8.20.0", + "react": "^18", + "react-dom": "^18", + "sonner": "^1.4.41", + "styled-components": "^6.1.8", + "viem": "^2.9.3", + "wagmi": "^2.10.2", + "zod": "^3.22.4" }, "devDependencies": { - "@babel/preset-env": "^7.16.11", - "@babel/preset-react": "^7.16.7", - "@babel/preset-typescript": "^7.16.7", - "@testing-library/jest-dom": "^5.16.2", - "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", - "@types/react": "^17.0.33", - "@types/react-dom": "^17.0.10", - "@types/set-interval-async": "^1.0.0", - "@typescript-eslint/eslint-plugin": "^5.13.0", - "@typescript-eslint/parser": "^5.13.0", - "@vitejs/plugin-react": "^1.0.7", - "eslint": "^8.10.0", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jest": "^26.1.1", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-testing-library": "^5.0.6", - "jest": "^27.5.1", - "prettier": "^2.5.1", - "rollup-plugin-node-globals": "^1.4.0", - "rollup-plugin-polyfill-node": "^0.9.0", - "typescript": "~4.5.4", - "util": "^0.12.4", - "vite": "^2.8.0" + "@types/cookie": "^0.6.0", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/styled-components": "^5.1.34", + "@typescript-eslint/eslint-plugin": "6.7.2", + "@typescript-eslint/parser": "^6.7.2", + "cookie": "^0.6.0", + "dotenv": "^16.0.0", + "eslint": "^8.47.0", + "eslint-config-next": "14.1.4", + "eslint-config-prettier": "^9.1.0", + "eslint-config-turbo": "^1.13.2", + "eslint-plugin-react": "^7.34.1", + "next-test-api-route-handler": "^4.0.7", + "prettier": "^2.8.8", + "typescript": "^5", + "vite-tsconfig-paths": "^4.3.2", + "vitest": "^1.5.2" } } diff --git a/packages/frontend/public/favicon.ico b/packages/frontend/public/favicon.ico new file mode 100644 index 000000000..6d810597c Binary files /dev/null and b/packages/frontend/public/favicon.ico differ diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx deleted file mode 100644 index d138400a9..000000000 --- a/packages/frontend/src/App.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Root } from 'src/Root' - -import { Providers } from './providers/Providers' -import { GlobalStyles } from './styles/GlobalStyles' - -function App() { - return ( - - - - - ) -} - -export default App diff --git a/packages/frontend/src/Root.tsx b/packages/frontend/src/Root.tsx deleted file mode 100644 index 48656a363..000000000 --- a/packages/frontend/src/Root.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { HashRouter, Route, Routes } from 'react-router-dom' -import styled from 'styled-components' - -import { Footer } from './components/Footer/Footer' -import { TopBar } from './components/TopBar/TopBar' -import { Bids, Home } from './pages' - -export function Root() { - return ( - - - - - } /> - } /> - - -