Skip to content

Commit

Permalink
Adds more tests for blind auciton
Browse files Browse the repository at this point in the history
Signed-off-by: Konstantina Blazhukova <[email protected]>
  • Loading branch information
konstantinabl committed Nov 3, 2023
1 parent f0013da commit 8f2dede
Show file tree
Hide file tree
Showing 3 changed files with 384 additions and 0 deletions.
171 changes: 171 additions & 0 deletions contracts/solidity/blind-auction/BlindAuction.sol
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];
}
}
4 changes: 4 additions & 0 deletions test/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const Contract = {
Transaction: 'Transaction',
MessageFrameAddresses: 'MessageFrameAddresses',
New: 'New',
<<<<<<< HEAD
AssemblyAddress: 'AssemblyAddress',
AddressContract: 'AddressContract',
Recipient: 'Recipient',
Expand All @@ -117,6 +118,9 @@ const Contract = {
Defaults: "Defaults",
NonExisting: "NonExisting",
NonExtDup: "NonExtDup",
=======
BlindAuction: 'BlindAuction'
>>>>>>> 83b21c4 (Adds more tests for blind auciton)
}

const CALL_EXCEPTION = 'CALL_EXCEPTION'
Expand Down
209 changes: 209 additions & 0 deletions test/solidity/blind-auction/blindAuction.js
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')
})
})

0 comments on commit 8f2dede

Please sign in to comment.