-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Konstantina Blazhukova <[email protected]>
- Loading branch information
1 parent
f0013da
commit 8f2dede
Showing
3 changed files
with
384 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.20; | ||
|
||
contract BlindAuction { | ||
struct Bid { | ||
bytes32 blindedBid; | ||
uint deposit; | ||
} | ||
|
||
address payable public beneficiary; | ||
uint public biddingEnd; | ||
uint public revealEnd; | ||
bool public ended; | ||
|
||
mapping(address => Bid[]) public bids; | ||
|
||
address public highestBidder; | ||
uint public highestBid; | ||
|
||
// Allowed withdrawals of previous bids | ||
mapping(address => uint) pendingReturns; | ||
|
||
event AuctionEnded(address winner, uint highestBid); | ||
|
||
// Errors that describe failures. | ||
|
||
/// The function has been called too early. | ||
/// Try again at `time`. | ||
error TooEarly(uint time); | ||
/// The function has been called too late. | ||
/// It cannot be called after `time`. | ||
error TooLate(uint time); | ||
/// The function auctionEnd has already been called. | ||
error AuctionEndAlreadyCalled(); | ||
|
||
// Modifiers are a convenient way to validate inputs to | ||
// functions. `onlyBefore` is applied to `bid` below: | ||
// The new function body is the modifier's body where | ||
// `_` is replaced by the old function body. | ||
modifier onlyBefore(uint time) { | ||
if (block.timestamp >= time) revert TooLate(time); | ||
_; | ||
} | ||
modifier onlyAfter(uint time) { | ||
if (block.timestamp <= time) revert TooEarly(time); | ||
_; | ||
} | ||
|
||
constructor( | ||
uint biddingTime, | ||
uint revealTime, | ||
address payable beneficiaryAddress | ||
) { | ||
beneficiary = beneficiaryAddress; | ||
biddingEnd = block.timestamp + biddingTime; | ||
revealEnd = biddingEnd + revealTime; | ||
} | ||
|
||
/// Place a blinded bid with `blindedBid` = | ||
/// keccak256(abi.encodePacked(value, fake, secret)). | ||
/// The sent ether is only refunded if the bid is correctly | ||
/// revealed in the revealing phase. The bid is valid if the | ||
/// ether sent together with the bid is at least "value" and | ||
/// "fake" is not true. Setting "fake" to true and sending | ||
/// not the exact amount are ways to hide the real bid but | ||
/// still make the required deposit. The same address can | ||
/// place multiple bids. | ||
function bid(bytes32 blindedBid) | ||
external | ||
payable | ||
onlyBefore(biddingEnd) | ||
{ | ||
bids[msg.sender].push(Bid({ | ||
blindedBid: blindedBid, | ||
deposit: msg.value | ||
})); | ||
} | ||
|
||
/// Reveal your blinded bids. You will get a refund for all | ||
/// correctly blinded invalid bids and for all bids except for | ||
/// the totally highest. | ||
function reveal( | ||
uint[] calldata values, | ||
bool[] calldata fakes, | ||
bytes32[] calldata secrets | ||
) | ||
external | ||
onlyAfter(biddingEnd) | ||
onlyBefore(revealEnd) | ||
{ | ||
uint length = bids[msg.sender].length; | ||
require(values.length == length); | ||
require(fakes.length == length); | ||
require(secrets.length == length); | ||
|
||
uint refund; | ||
for (uint i = 0; i < length; i++) { | ||
Bid storage bidToCheck = bids[msg.sender][i]; | ||
(uint value, bool fake, bytes32 secret) = | ||
(values[i], fakes[i], secrets[i]); | ||
//console.log(bidToCheck.blindedBid); | ||
//console.log(keccak256(abi.encodePacked(value, fake, secret))); | ||
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) { | ||
// Bid was not actually revealed. | ||
// Do not refund deposit. | ||
continue; | ||
} | ||
refund += bidToCheck.deposit; | ||
if (!fake && bidToCheck.deposit >= value) { | ||
if (placeBid(msg.sender, value)) | ||
refund -= value; | ||
} | ||
// Make it impossible for the sender to re-claim | ||
// the same deposit. | ||
bidToCheck.blindedBid = bytes32(0); | ||
} | ||
payable(msg.sender).transfer(refund); | ||
} | ||
|
||
/// Withdraw a bid that was overbid. | ||
function withdraw() external { | ||
uint amount = pendingReturns[msg.sender]; | ||
if (amount > 0) { | ||
// It is important to set this to zero because the recipient | ||
// can call this function again as part of the receiving call | ||
// before `transfer` returns (see the remark above about | ||
// conditions -> effects -> interaction). | ||
pendingReturns[msg.sender] = 0; | ||
|
||
payable(msg.sender).transfer(amount); | ||
} | ||
} | ||
|
||
/// End the auction and send the highest bid | ||
/// to the beneficiary. | ||
function auctionEnd() | ||
external | ||
onlyAfter(revealEnd) | ||
{ | ||
if (ended) revert AuctionEndAlreadyCalled(); | ||
emit AuctionEnded(highestBidder, highestBid); | ||
ended = true; | ||
beneficiary.transfer(highestBid); | ||
} | ||
|
||
// This is an "internal" function which means that it | ||
// can only be called from the contract itself (or from | ||
// derived contracts). | ||
function placeBid(address bidder, uint value) internal | ||
returns (bool success) | ||
{ | ||
if (value <= highestBid) { | ||
return false; | ||
} | ||
if (highestBidder != address(0)) { | ||
// Refund the previously highest bidder. | ||
pendingReturns[highestBidder] += highestBid; | ||
} | ||
highestBid = value; | ||
highestBidder = bidder; | ||
return true; | ||
} | ||
|
||
function getBids(address bidderAddress) public view returns(Bid[] memory) { | ||
return bids[bidderAddress]; | ||
} | ||
|
||
function getPendingReturns(address bidderAddress) public view returns(uint) { | ||
return pendingReturns[bidderAddress]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
/*- | ||
* | ||
* Hedera Smart Contracts | ||
* | ||
* Copyright (C) 2023 Hedera Hashgraph, LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
const chai = require('chai') | ||
const { expect } = require('chai') | ||
const chaiAsPromised = require("chai-as-promised") | ||
const { ethers } = require('hardhat') | ||
const Constants = require('../../constants') | ||
chai.use(chaiAsPromised); | ||
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | ||
|
||
const deployBlindAuctionContract = async (biddingTime, revealTime, beneficiaryAddress) => { | ||
const factory = await ethers.getContractFactory(Constants.Contract.BlindAuction) | ||
const contract = await factory.deploy(biddingTime, revealTime, beneficiaryAddress) | ||
|
||
await contract.deployed() | ||
|
||
return contract; | ||
} | ||
|
||
describe('Solidity Errors', function () { | ||
let beneficiary, wallet1; | ||
const oneEther = ethers.utils.parseEther("100.0"); | ||
const twoEther = ethers.utils.parseEther("200.0"); | ||
const oneTenthEther = ethers.utils.parseEther("0.1"); | ||
const fiftyGwei = ethers.utils.parseUnits('50', 'gwei') | ||
|
||
before(async function () { | ||
[beneficiary, wallet1] = await ethers.getSigners(); | ||
}) | ||
|
||
it('should confirm beneficiary is set correctly', async function () { | ||
const contract = await deployBlindAuctionContract(3, 5, beneficiary.address); | ||
const beneficiaryAddress = await contract.beneficiary(); | ||
|
||
expect(beneficiaryAddress).to.eq(beneficiary.address); | ||
}) | ||
|
||
it('should confirm a user can bid', async function () { | ||
const contract = await deployBlindAuctionContract(3, 5, beneficiary.address); | ||
//pack encode the bid we want to put | ||
const bidData = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[oneEther, false, 2]); | ||
|
||
//bid from another account | ||
const result = await contract.connect(wallet1).bid(bidData, {value: oneEther}); | ||
await result.wait(); | ||
const firstBidder = await contract.getBids(wallet1.address); | ||
|
||
expect(firstBidder.length).to.eq(1); | ||
expect(firstBidder[0].blindedBid).to.eq(bidData); | ||
}) | ||
|
||
it('should confirm a user can reveal their bids', async function () { | ||
const contract = await deployBlindAuctionContract(6, 5, beneficiary.address); | ||
|
||
const firstBid = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[10000000000, false, ethers.utils.formatBytes32String('2')]); | ||
const secondBid = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[10000000000, true, ethers.utils.formatBytes32String('23')]); | ||
|
||
|
||
const bid = await contract.connect(wallet1).bid(firstBid, {value: oneEther}); | ||
await bid.wait(); | ||
|
||
await sleep(2000); | ||
|
||
const bid2 = await contract.connect(wallet1).bid(secondBid, {value: fiftyGwei}); | ||
await bid2.wait(); | ||
|
||
await sleep(3000); | ||
|
||
const result = await contract.connect(wallet1).reveal([10000000000, 10000000000], [false, true], [ethers.utils.formatBytes32String('2'), ethers.utils.formatBytes32String('23')], {gasLimit: 5000000}); | ||
await result.wait(); | ||
await sleep(3000); | ||
|
||
const highestBidder = await contract.highestBidder(); | ||
const highestBid = await contract.highestBid() | ||
|
||
//add expect statements here | ||
expect(highestBid).to.equal(BigInt(10000000000)); | ||
expect(highestBidder).to.equal(wallet1.address); | ||
}) | ||
|
||
it('should confirm a user can withdraw', async function () { | ||
const contract = await deployBlindAuctionContract(10, 5, beneficiary.address); | ||
|
||
const firstBid = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[10000000000, false, ethers.utils.formatBytes32String('2')]); | ||
const secondBid = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[20000000000, true, ethers.utils.formatBytes32String('23')]); | ||
const thirdBid = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[20000000000, false, ethers.utils.formatBytes32String('5')]); | ||
|
||
const bid = await contract.connect(wallet1).bid(firstBid, {value: oneEther}); | ||
await bid.wait(); | ||
|
||
await sleep(2000); | ||
|
||
const bid2 = await contract.connect(wallet1).bid(secondBid, {value: fiftyGwei}); | ||
await bid2.wait(); | ||
|
||
const bid3 = await contract.connect(wallet1).bid(thirdBid, {value: twoEther}); | ||
await bid3.wait(); | ||
|
||
await sleep(3000); | ||
|
||
const result = await contract.connect(wallet1).reveal([10000000000, 20000000000, 20000000000], [false, true, false], [ethers.utils.formatBytes32String('2'), ethers.utils.formatBytes32String('23'), ethers.utils.formatBytes32String('5')], {gasLimit: 5000000}); | ||
await result.wait(); | ||
|
||
await sleep(2000); | ||
|
||
const highestBidder = await contract.highestBidder(); | ||
const highestBid = await contract.highestBid(); | ||
|
||
const balanceBeforeWithdraw = await ethers.provider.getBalance(wallet1.address); | ||
|
||
const withdraw = await contract.connect(wallet1).withdraw(); | ||
await withdraw.wait(); | ||
|
||
await sleep(1000); | ||
const balanceAfterWithdraw = await ethers.provider.getBalance(wallet1.address); | ||
|
||
expect(balanceBeforeWithdraw).to.be.lessThan(balanceAfterWithdraw); | ||
expect(highestBid).to.equal(BigInt(20000000000)); | ||
expect(highestBidder).to.equal(wallet1.address); | ||
}) | ||
|
||
it('should confirm a user can end an auction', async function () { | ||
const contract = await deployBlindAuctionContract(5, 5, beneficiary.address); | ||
|
||
const firstBid = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[10000000000, false, ethers.utils.formatBytes32String('2')]); | ||
const secondBid = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[10000000000, true, ethers.utils.formatBytes32String('23')]); | ||
const thirdBid = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[20000000000, false, ethers.utils.formatBytes32String('5')]); | ||
|
||
const bid = await contract.connect(wallet1).bid(firstBid, {value: oneEther}); | ||
await bid.wait(); | ||
|
||
const bid2 = await contract.connect(wallet1).bid(secondBid, {value: fiftyGwei}); | ||
await bid2.wait(); | ||
|
||
const bid3 = await contract.connect(wallet1).bid(thirdBid, {value: twoEther}); | ||
await bid3.wait(); | ||
|
||
await sleep(3000); | ||
|
||
const reveal = await contract.connect(wallet1).reveal([10000000000, 20000000000, 20000000000], [false, true, false], [ethers.utils.formatBytes32String('2'), ethers.utils.formatBytes32String('23'), ethers.utils.formatBytes32String('5')], {gasLimit: 5000000}); | ||
await reveal.wait(); | ||
|
||
const balanceBeforeAuctionEnd = await ethers.provider.getBalance(beneficiary.address); | ||
|
||
await sleep(2000); | ||
const highestBidder = await contract.highestBidder(); | ||
const highestBid = await contract.highestBid() | ||
|
||
const result = await contract.connect(wallet1).auctionEnd(); | ||
await result.wait(); | ||
|
||
await sleep(2000); | ||
|
||
const balanceAfterAuctionEnd = await ethers.provider.getBalance(beneficiary.address); | ||
|
||
expect(highestBid).to.equal(BigInt(20000000000)); | ||
expect(highestBidder).to.equal(wallet1.address); | ||
expect(balanceBeforeAuctionEnd).to.be.lessThan(balanceAfterAuctionEnd); | ||
}) | ||
|
||
it('should confirm a user cannot bid after end', async function () { | ||
const contract = await deployBlindAuctionContract(4, 2, beneficiary.address); | ||
const bidData = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[oneEther, false, 2]); | ||
|
||
//wait for next block | ||
await sleep(5000); | ||
|
||
|
||
const result = await contract.connect(wallet1).bid(bidData, {value: oneEther}); | ||
await expect(result.wait()).to.eventually.be.rejected.and.have.property('code', 'CALL_EXCEPTION') | ||
}) | ||
|
||
it('should confirm a user cannot reveal after reveal end', async function () { | ||
const contract = await deployBlindAuctionContract(5, 2, beneficiary.address); | ||
|
||
const bidData = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[oneEther, false, 2]); | ||
const anotherBidData = ethers.utils.solidityKeccak256(["uint256", "bool", "uint256"] ,[twoEther, true, 23]); | ||
|
||
const bid = await contract.connect(wallet1).bid(bidData, {value: oneEther}); | ||
await bid.wait(); | ||
|
||
const bidAgain = await contract.connect(wallet1).bid(anotherBidData, {value: oneTenthEther}); | ||
await bidAgain.wait(); | ||
|
||
await sleep(6000); | ||
|
||
const result = await contract.connect(wallet1).reveal([oneEther, oneTenthEther], [false, true], [ethers.utils.formatBytes32String(2), ethers.utils.formatBytes32String(23)]); | ||
await expect(result.wait()).to.eventually.be.rejected.and.have.property('code', 'CALL_EXCEPTION') | ||
}) | ||
}) |