diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..0011640 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,61 @@ +name: "CI" + +env: + DOTENV_CONFIG_PATH: "./.env.example" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + ci: + services: + # Label used to access the service container + localfhenix: + options: --name localfhenix + image: ghcr.io/fhenixprotocol/fhenix-devnet:0.1.6 + ports: + # Opens tcp port + - 5000:5000 + - 6000:6000 + - 8545:8545 + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v3" + + - name: "Install Pnpm" + uses: "pnpm/action-setup@v2" + with: + version: "8" + + - name: "Install Node.js" + uses: "actions/setup-node@v3" + with: + cache: "pnpm" + node-version: "lts/*" + + - name: "Install the dependencies" + run: "pnpm install" + + - name: "Lint the code" + run: "pnpm lint" + + - name: "Add lint summary" + run: | + echo "## Lint results" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + - name: "Compile the contracts and generate the TypeChain bindings" + run: "pnpm typechain" + +# - name: "Test the contracts" +# run: "pnpm test" + + - name: "Add test summary" + run: | + echo "## Test results" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/contracts/Auction.sol b/contracts/Auction.sol new file mode 100644 index 0000000..35a8ade --- /dev/null +++ b/contracts/Auction.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity >=0.8.13 <0.9.0; + +import { inEuint32, euint32, FHE } from "@fhenixprotocol/contracts/FHE.sol"; +import { FHERC20 } from "./fherc20.sol"; +import "./ConfAddress.sol"; + +struct HistoryEntry { + euint32 amount; + bool refunded; +} + +contract Auction { + address payable public auctioneer; + mapping(address => HistoryEntry) internal auctionHistory; + euint32 internal CONST_0_ENCRYPTED; + euint32 internal highestBid; + Eaddress internal defaultAddress; + Eaddress internal highestBidder; + euint32 internal eMaxEuint32; + uint256 public auctionEndTime; + FHERC20 internal _wfhenix; + + // When auction is ended this will contain the PLAINTEXT winner address + address public winnerAddress; + + event AuctionEnded(address winner, uint32 bid); + + constructor(address wfhenix, uint256 biddingTime) payable { + _wfhenix = FHERC20(wfhenix); + auctioneer = payable(msg.sender); + auctionEndTime = block.timestamp + biddingTime; + CONST_0_ENCRYPTED = FHE.asEuint32(0); + highestBid = CONST_0_ENCRYPTED; + for (uint i = 0; i < 5; i++) { + defaultAddress.values[i] = CONST_0_ENCRYPTED; + highestBidder.values[i] = CONST_0_ENCRYPTED; + } + + eMaxEuint32 = FHE.asEuint32(0xFFFFFFFF); + } + + // Modifiers + modifier onlyAuctioneer() { + require(msg.sender == auctioneer, "Only auctioneer can perform this action"); + _; + } + + modifier afterAuctionEnds() { + require(block.timestamp >= auctionEndTime, "Auction ongoing"); + _; + } + + modifier auctionNotEnded() { + require(winnerAddress == address(0), "Auction already ended"); + _; + } + + modifier auctionEnded() { + require(winnerAddress != address(0), "Auction already ended"); + _; + } + + modifier notWinner() { + require(msg.sender != winnerAddress, "Winner cannot perform this action"); + _; + } + + function updateHistory(address addr, euint32 currentBid) internal returns (euint32) { + // Check for overflow, if such, just don't change the actualBid + // NOTE: overflow is most likely an abnormal action so the funds WON'T be refunded! + if (!FHE.isInitialized(auctionHistory[addr].amount)) { + HistoryEntry memory entry; + entry.amount = currentBid; + entry.refunded = false; + auctionHistory[addr] = entry; + return auctionHistory[addr].amount; + } + + // Checking overflow here is optional as in real-life precision would be accounted for. + ebool hadOverflow = (eMaxEuint32 - currentBid).lt(auctionHistory[addr].amount); + euint32 actualBid = FHE.select(hadOverflow, CONST_0_ENCRYPTED, currentBid); + + // Add the actual bid to the previous bid + // If there was no bid it will work because the default value of uint32 is encrypted 02 + auctionHistory[addr].amount = auctionHistory[addr].amount + actualBid; + return auctionHistory[addr].amount; + } + + function bid(inEuint32 calldata amount) + external + auctionNotEnded + { + + euint32 spent = _wfhenix.transferFromEncrypted(msg.sender, address(this), amount); + + euint32 newBid = updateHistory(msg.sender, spent); + // Can't update here highestBid directly because we need and indication whether the highestBid was changed + // if we will change here the highestBid + // we will have an edge case when the current bid will be equal to the highestBid + euint32 newHeighestBid = FHE.max(newBid, highestBid); + + Eaddress memory eaddr = ConfAddress.toEaddress(payable(msg.sender)); + ebool wasBidChanged = newHeighestBid.gt(highestBid); + + highestBidder = ConfAddress.conditionalUpdate(wasBidChanged, highestBidder, eaddr); + highestBid = newHeighestBid; + } + + function getWinner() + external + view + auctionEnded + returns (address) { + return winnerAddress; + } + + function getWinningBid() + external + view + auctionEnded + returns (uint256) { + return FHE.decrypt(highestBid); + } + + function endAuction() + external + onlyAuctioneer + afterAuctionEnds + auctionNotEnded + { + winnerAddress = ConfAddress.unsafeToAddress(highestBidder); + // The cards can be revealed now, we can safely reveal the bidder + emit AuctionEnded(winnerAddress, FHE.decrypt(highestBid)); + } + + // just for debugging purposes + function debugEndAuction() + public + onlyAuctioneer + auctionNotEnded + { + winnerAddress = ConfAddress.unsafeToAddress(highestBidder); + // The cards can be revealed now, we can safely reveal the bidder + emit AuctionEnded(winnerAddress, FHE.decrypt(highestBid)); + } + + function redeemFunds() + external + notWinner + auctionEnded + { + require(!auctionHistory[msg.sender].refunded, "Already refunded"); + + euint32 toBeRedeemed = auctionHistory[msg.sender].amount; + + auctionHistory[msg.sender].refunded = true; + + _wfhenix.transferEncrypted(msg.sender, toBeRedeemed); + } +} \ No newline at end of file diff --git a/contracts/BytesLib.sol b/contracts/BytesLib.sol new file mode 100644 index 0000000..8c7358c --- /dev/null +++ b/contracts/BytesLib.sol @@ -0,0 +1,576 @@ +// SPDX-License-Identifier: Unlicense +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. + */ +pragma solidity >=0.8.0 <0.9.0; + + +library BytesLib { + function concat( + bytes memory _preBytes, + bytes memory _postBytes + ) + internal + pure + returns (bytes memory) + { + bytes memory tempBytes; + + assembly { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // Store the length of the first bytes array at the beginning of + // the memory for tempBytes. + let length := mload(_preBytes) + mstore(tempBytes, length) + + // Maintain a memory counter for the current write location in the + // temp bytes array by adding the 32 bytes for the array length to + // the starting location. + let mc := add(tempBytes, 0x20) + // Stop copying when the memory counter reaches the length of the + // first bytes array. + let end := add(mc, length) + + for { + // Initialize a copy counter to the start of the _preBytes data, + // 32 bytes into its memory. + let cc := add(_preBytes, 0x20) + } lt(mc, end) { + // Increase both counters by 32 bytes each iteration. + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // Write the _preBytes data into the tempBytes memory 32 bytes + // at a time. + mstore(mc, mload(cc)) + } + + // Add the length of _postBytes to the current length of tempBytes + // and store it as the new length in the first 32 bytes of the + // tempBytes memory. + length := mload(_postBytes) + mstore(tempBytes, add(length, mload(tempBytes))) + + // Move the memory counter back from a multiple of 0x20 to the + // actual end of the _preBytes data. + mc := end + // Stop copying when the memory counter reaches the new combined + // length of the arrays. + end := add(mc, length) + + for { + let cc := add(_postBytes, 0x20) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + // Update the free-memory pointer by padding our last write location + // to 32 bytes: add 31 bytes to the end of tempBytes to move to the + // next 32 byte block, then round down to the nearest multiple of + // 32. If the sum of the length of the two arrays is zero then add + // one before rounding down to leave a blank 32 bytes (the length block with 0). + mstore(0x40, and( + add(add(end, iszero(add(length, mload(_preBytes)))), 31), + not(31) // Round down to the nearest 32 bytes. + )) + } + + return tempBytes; + } + + function concatStorage(bytes storage _preBytes, bytes memory _postBytes) internal { + assembly { + // Read the first 32 bytes of _preBytes storage, which is the length + // of the array. (We don't need to use the offset into the slot + // because arrays use the entire slot.) + let fslot := sload(_preBytes.slot) + // Arrays of 31 bytes or less have an even value in their slot, + // while longer arrays have an odd value. The actual length is + // the slot divided by two for odd values, and the lowest order + // byte divided by two for even values. + // If the slot is even, bitwise and the slot with 255 and divide by + // two to get the length. If the slot is odd, bitwise and the slot + // with -1 and divide by two. + let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) + let mlength := mload(_postBytes) + let newlength := add(slength, mlength) + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + switch add(lt(slength, 32), lt(newlength, 32)) + case 2 { + // Since the new array still fits in the slot, we just need to + // update the contents of the slot. + // uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length + sstore( + _preBytes.slot, + // all the modifications to the slot are inside this + // next block + add( + // we can just add to the slot contents because the + // bytes we want to change are the LSBs + fslot, + add( + mul( + div( + // load the bytes from memory + mload(add(_postBytes, 0x20)), + // zero all bytes to the right + exp(0x100, sub(32, mlength)) + ), + // and now shift left the number of bytes to + // leave space for the length in the slot + exp(0x100, sub(32, newlength)) + ), + // increase length by the double of the memory + // bytes length + mul(mlength, 2) + ) + ) + ) + } + case 1 { + // The stored value fits in the slot, but the combined value + // will exceed it. + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // The contents of the _postBytes array start 32 bytes into + // the structure. Our first read should obtain the `submod` + // bytes that can fit into the unused space in the last word + // of the stored array. To get this, we read 32 bytes starting + // from `submod`, so the data we read overlaps with the array + // contents by `submod` bytes. Masking the lowest-order + // `submod` bytes allows us to add that value directly to the + // stored value. + + let submod := sub(32, slength) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore( + sc, + add( + and( + fslot, + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00 + ), + and(mload(mc), mask) + ) + ) + + for { + mc := add(mc, 0x20) + sc := add(sc, 1) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + sstore(sc, mload(mc)) + } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + default { + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + // Start copying to the last used word of the stored array. + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // Copy over the first `submod` bytes of the new data as in + // case 1 above. + let slengthmod := mod(slength, 32) + let mlengthmod := mod(mlength, 32) + let submod := sub(32, slengthmod) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore(sc, add(sload(sc), and(mload(mc), mask))) + + for { + sc := add(sc, 1) + mc := add(mc, 0x20) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + sstore(sc, mload(mc)) + } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + } + } + + function slice( + bytes memory _bytes, + uint256 _start, + uint256 _length + ) + internal + pure + returns (bytes memory) + { + require(_length + 31 >= _length, "slice_overflow"); + require(_bytes.length >= _start + _length, "slice_outOfBounds"); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + + function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) { + require(_bytes.length >= _start + 1 , "toUint8_outOfBounds"); + uint8 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x1), _start)) + } + + return tempUint; + } + + function toUint16(bytes memory _bytes, uint256 _start) internal pure returns (uint16) { + require(_bytes.length >= _start + 2, "toUint16_outOfBounds"); + uint16 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x2), _start)) + } + + return tempUint; + } + + function toUint32(bytes memory _bytes, uint256 _start) internal pure returns (uint32) { + require(_bytes.length >= _start + 4, "toUint32_outOfBounds"); + uint32 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x4), _start)) + } + + return tempUint; + } + + function toUint64(bytes memory _bytes, uint256 _start) internal pure returns (uint64) { + require(_bytes.length >= _start + 8, "toUint64_outOfBounds"); + uint64 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x8), _start)) + } + + return tempUint; + } + + function toUint96(bytes memory _bytes, uint256 _start) internal pure returns (uint96) { + require(_bytes.length >= _start + 12, "toUint96_outOfBounds"); + uint96 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0xc), _start)) + } + + return tempUint; + } + + function toUint128(bytes memory _bytes, uint256 _start) internal pure returns (uint128) { + require(_bytes.length >= _start + 16, "toUint128_outOfBounds"); + uint128 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x10), _start)) + } + + return tempUint; + } + + function toUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint256) { + require(_bytes.length >= _start + 32, "toUint256_outOfBounds"); + uint256 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x20), _start)) + } + + return tempUint; + } + + function toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) { + require(_bytes.length >= _start + 32, "toBytes32_outOfBounds"); + bytes32 tempBytes32; + + assembly { + tempBytes32 := mload(add(add(_bytes, 0x20), _start)) + } + + return tempBytes32; + } + + function equal(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) { + bool success = true; + + assembly { + let length := mload(_preBytes) + + // if lengths don't match the arrays are not equal + switch eq(length, mload(_postBytes)) + case 1 { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + let mc := add(_preBytes, 0x20) + let end := add(mc, length) + + for { + let cc := add(_postBytes, 0x20) + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + } eq(add(lt(mc, end), cb), 2) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // if any of these checks fails then arrays are not equal + if iszero(eq(mload(mc), mload(cc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } + + function equal_nonAligned(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) { + bool success = true; + + assembly { + let length := mload(_preBytes) + + // if lengths don't match the arrays are not equal + switch eq(length, mload(_postBytes)) + case 1 { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + let endMinusWord := add(_preBytes, length) + let mc := add(_preBytes, 0x20) + let cc := add(_postBytes, 0x20) + + for { + // the next line is the loop condition: + // while(uint256(mc < endWord) + cb == 2) + } eq(add(lt(mc, endMinusWord), cb), 2) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // if any of these checks fails then arrays are not equal + if iszero(eq(mload(mc), mload(cc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + + // Only if still successful + // For <1 word tail bytes + if gt(success, 0) { + // Get the remainder of length/32 + // length % 32 = AND(length, 32 - 1) + let numTailBytes := and(length, 0x1f) + let mcRem := mload(mc) + let ccRem := mload(cc) + for { + let i := 0 + // the next line is the loop condition: + // while(uint256(i < numTailBytes) + cb == 2) + } eq(add(lt(i, numTailBytes), cb), 2) { + i := add(i, 1) + } { + if iszero(eq(byte(i, mcRem), byte(i, ccRem))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } + + function equalStorage( + bytes storage _preBytes, + bytes memory _postBytes + ) + internal + view + returns (bool) + { + bool success = true; + + assembly { + // we know _preBytes_offset is 0 + let fslot := sload(_preBytes.slot) + // Decode the length of the stored array like in concatStorage(). + let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) + let mlength := mload(_postBytes) + + // if lengths don't match the arrays are not equal + switch eq(slength, mlength) + case 1 { + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + if iszero(iszero(slength)) { + switch lt(slength, 32) + case 1 { + // blank the last byte which is the length + fslot := mul(div(fslot, 0x100), 0x100) + + if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) { + // unsuccess: + success := 0 + } + } + default { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := keccak256(0x0, 0x20) + + let mc := add(_postBytes, 0x20) + let end := add(mc, mlength) + + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + for {} eq(add(lt(mc, end), cb), 2) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + if iszero(eq(sload(sc), mload(mc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } +} \ No newline at end of file diff --git a/contracts/ConfAddress.sol b/contracts/ConfAddress.sol new file mode 100644 index 0000000..8a6f6c3 --- /dev/null +++ b/contracts/ConfAddress.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity >=0.8.13 <0.9.0; + +import { euint32, ebool, FHE } from "@fhenixprotocol/contracts/FHE.sol"; +import "./BytesLib.sol"; + +/// @title Encrypted Address Library +/// @notice Provides methods for creating and managing addresses encrypted with FHE (Fully Homomorphic Encryption) +/// @dev Assumes the existence of an FHE library that implements fully homomorphic encryption functions + +/// @dev A representation of an encrypted address using Fully Homomorphic Encryption. +/// It consists of 5 encrypted 32-bit unsigned integers (`euint32`). + struct Eaddress { + euint32[5] values; + } + +library ConfAddress { + /// @notice Encrypts a plaintext Ethereum address into its encrypted representation (`eaddress`). + /// @dev Iterates over 5 chunks of the address, applying a bitmask to each, then encrypting with `FHE`. + /// @param addr The plain Ethereum address to encrypt + /// @return eaddr The encrypted representation of the address + function toEaddress(address payable addr) internal pure returns (Eaddress memory) { + uint160 addrValue = uint160(address(addr)); + /// @dev A bitmask constant for selecting specific 32-bit chunks from a 160-bit Ethereum address. + /// It has the first 32 bits set to 1, and the remaining bits set to 0. + uint160 MASK = + uint160(uint256(0x000000000000000000000000FFFFFFFF00000000000000000000000000000000)); + + Eaddress memory eaddr; + + for (uint i = 0; i < 5; i++) { + uint160 currentChunkOffset = uint160(i * 32); + uint160 mask = MASK >> currentChunkOffset; // Mask the correct chunk based on i + uint32 chunk = uint32((addrValue & mask) >> (128 - currentChunkOffset)); + eaddr.values[i] = FHE.asEuint32(chunk); + } + + return eaddr; + } + + /// @notice Decrypts an `eaddress` to retrieve the original plaintext Ethereum address. + /// @dev This operation should be used with caution as it exposes the encrypted address. + /// @param eaddr The encrypted address to decrypt + /// @return The decrypted plaintext Ethereum address + function unsafeToAddress(Eaddress memory eaddr) internal pure returns (address) { + uint160 addrValue; + for (uint i = 0; i < 5; i++) { + uint32 currentChunkOffset = uint32((4 - i) * 32); + uint32 val = FHE.decrypt(eaddr.values[i]); + uint160 currentValue = uint160(val) << currentChunkOffset; + addrValue += currentValue; + } + + bytes memory addrBz = new bytes(32); + assembly { + mstore(add(addrBz,32), addrValue) + } + + return BytesLib.toAddress(addrBz, 12); + } + + /// @notice Re-encrypts the encrypted values within an `eaddress`. + /// @dev The re-encryption is done to change the encrypted representation without + /// altering the underlying plaintext address, which can be useful for obfuscation purposes in storage. + /// @param eaddr The encrypted address to re-encrypt + /// @param ezero An encrypted zero value that triggers the re-encryption + function resestEaddress(Eaddress memory eaddr, euint32 ezero) internal pure { + for (uint i = 0; i < 5; i++) { + // Adding zero will practiaclly reencrypt the value without it being changed + eaddr.values[i] = eaddr.values[i] + ezero; + } + } + + /// @notice Determines if an encrypted address is equal to a given plaintext Ethereum address. + /// @dev This operation encrypts the plaintext address and compares the encrypted representations. + /// @param lhs The encrypted address to compare + /// @param addr The plaintext Ethereum address to compare against + /// @return res A boolean indicating if the encrypted and plaintext addresses are equal + function equals(Eaddress storage lhs, address payable addr) internal view returns (ebool) { + Eaddress memory rhs = toEaddress(addr); + ebool res = FHE.eq(lhs.values[0], rhs.values[0]); + for (uint i = 1; i < 5; i++) { + res = res & FHE.eq(lhs.values[i], rhs.values[i]); + } + + return res; + } + + function conditionalUpdate( + ebool condition, + Eaddress memory eaddr, + Eaddress memory newEaddr + ) internal pure returns (Eaddress memory) { + for (uint i = 0; i < 5; i++) { + // Even if condition is false the ENCRYPTED value of eaddr.values[i] will be changed + // because the encryption is not deterministic + // so no one will know whether the highest bidder was changed or not + eaddr.values[i] = FHE.select(condition, newEaddr.values[i], eaddr.values[i]); + } + + return eaddr; + } +} \ No newline at end of file diff --git a/contracts/IFHERC20.sol b/contracts/IFHERC20.sol new file mode 100644 index 0000000..fbf10bb --- /dev/null +++ b/contracts/IFHERC20.sol @@ -0,0 +1,85 @@ +pragma solidity ^0.8.20; + +// SPDX-License-Identifier: MIT +// Fhenix Protocol (last updated v0.1.0) (token/FHERC20/IFHERC20.sol) +// Inspired by OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts) (token/ERC20/IERC20.sol) + +import { Permission, Permissioned } from "@fhenixprotocol/contracts/access/Permissioned.sol"; +import { euint32, inEuint32 } from "@fhenixprotocol/contracts/FHE.sol"; + +/** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ +interface IFHERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event TransferEncrypted(address indexed from, address indexed to); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approveEncrypted}. `value` is the new allowance. + */ + event ApprovalEncrypted(address indexed owner, address indexed spender); + + // /** + // * @dev Returns the value of tokens in existence. + // */ + // function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`, sealed and encrypted for the caller. + */ + function balanceOfEncrypted(address account, Permission memory auth) external view returns (bytes memory); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {TransferEncrypted} event. + */ + function transferEncrypted(address to, inEuint32 calldata value) external returns (euint32); + function transferEncrypted(address to, euint32 value) external returns (euint32); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowanceEncrypted(address spender, Permission memory permission) external view returns (bytes memory); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {ApprovalEncrypted} event. + */ + function approveEncrypted(address spender, inEuint32 calldata value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {TransferEncrypted} event. + */ + function transferFromEncrypted(address from, address to, inEuint32 calldata value) external returns (euint32); + function transferFromEncrypted(address from, address to, euint32 value) external returns (euint32); +} \ No newline at end of file diff --git a/contracts/Voting.sol b/contracts/Voting.sol new file mode 100644 index 0000000..06baa47 --- /dev/null +++ b/contracts/Voting.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity >=0.8.19 <0.9.0; + +import "@fhenixprotocol/contracts/FHE.sol"; +import "@fhenixprotocol/contracts/access/Permissioned.sol"; + +contract Voting is Permissioned { + uint8 internal constant MAX_OPTIONS = 4; + + // Pre-compute these to prevent unnecessary gas usage for the users + // euint16 internal _zero = FHE.asEuint16(0); + // euint16 internal _one = FHE.asEuint16(1); + euint32 internal _u32Sixteen = FHE.asEuint32(16); + euint8[MAX_OPTIONS] internal _encOptions = [FHE.asEuint8(0), FHE.asEuint8(1), FHE.asEuint8(2), FHE.asEuint8(3)]; + + string public proposal; + string[] public options; + uint public voteEndTime; + euint16[MAX_OPTIONS] internal _tally; // Since every vote is worth 1, I assume we can use a 16-bit integer + + euint8 internal _winningOption; + euint16 internal _winningTally; + + mapping(address => euint8) internal _votes; + + constructor(string memory _proposal, string[] memory _options, uint votingPeriod) { + require(options.length <= MAX_OPTIONS, "too many options!"); + + proposal = _proposal; + options = _options; + voteEndTime = block.timestamp + votingPeriod; + } + + function vote(inEuint8 memory voteBytes) public { + require(block.timestamp < voteEndTime, "voting is over!"); + require(!FHE.isInitialized(_votes[msg.sender]), "already voted!"); + euint8 encryptedVote = FHE.asEuint8(voteBytes); // Cast bytes into an encrypted type + _requireValid(encryptedVote); + + _votes[msg.sender] = encryptedVote; + _addToTally(encryptedVote /* , _one */); + } + + function finalize() public { + require(voteEndTime < block.timestamp, "voting is still in progress!"); + + _winningOption = _encOptions[0]; + _winningTally = _tally[0]; + for (uint8 i = 1; i < options.length; i++) { + euint16 newWinningTally = FHE.max(_winningTally, _tally[i]); + _winningOption = FHE.select(newWinningTally.gt(_winningTally), _encOptions[i], _winningOption); + _winningTally = newWinningTally; + } + } + + function winning() public view returns (uint8, uint16) { + require(voteEndTime < block.timestamp, "voting is still in progress!"); + return (FHE.decrypt(_winningOption), FHE.decrypt(_winningTally)); + } + + function getUserVote( + Permission memory signature + ) public view onlyPermitted(signature, msg.sender) returns (bytes memory) { + require(FHE.isInitialized(_votes[msg.sender]), "no vote found!"); + return FHE.sealoutput(_votes[msg.sender], signature.publicKey); + } + + function _requireValid(euint8 encryptedVote) internal view { + // Make sure that: (0 <= vote <= options.length) + ebool isValid = encryptedVote.gte(_encOptions[0]).and(encryptedVote.lte(_encOptions[options.length - 1])); + FHE.req(isValid); + } + + function _addToTally(euint8 option /* , euint16 amount */) internal { + // We don't want to leak the user's vote, so we have to change the tally of every option. + // So for example, if the user voted for option 1: + // tally[0] = tally[0] + enc(0) + // tally[1] = tally[1] + enc(1) + // etc .. + for (uint8 i = 0; i < options.length; i++) { + // euint16 amountOrZero = FHE.select(option.eq(_encOptions[i]), _one, _zero); + ebool amountOrZero = option.eq(_encOptions[i]); // `eq()` result is known to be enc(0) or enc(1) + _tally[i] = _tally[i] + amountOrZero.toU16(); // `eq()` result is known to be enc(0) or enc(1) + } + } +} \ No newline at end of file diff --git a/contracts/counter.sol b/contracts/counter.sol deleted file mode 100644 index 31e10e0..0000000 --- a/contracts/counter.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.20 <0.9.0; - -import "@fhenixprotocol/contracts/FHE.sol"; -import {Permissioned, Permission} from "@fhenixprotocol/contracts/access/Permissioned.sol"; - -contract Counter is Permissioned { - euint32 private counter; - address public owner; - - constructor() { - owner = msg.sender; - } - - function add(bytes calldata encryptedValue) public { - euint32 value = FHE.asEuint32(encryptedValue); - counter = counter + value; - } - - function getCounter() public view returns (uint256) { - return FHE.decrypt(counter); - } - - function getCounterPermit( - Permission memory permission - ) public view onlySender(permission) returns (uint256) { - return FHE.decrypt(counter); - } - - function getCounterPermitSealed( - Permission memory permission - ) public view onlySender(permission) returns (bytes memory) { - return FHE.sealoutput(counter, permission.publicKey); - } -} diff --git a/contracts/fherc20.sol b/contracts/fherc20.sol new file mode 100644 index 0000000..366f649 --- /dev/null +++ b/contracts/fherc20.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { FHE, euint32, inEuint32 } from "@fhenixprotocol/contracts/FHE.sol"; +import { Permissioned, Permission } from "@fhenixprotocol/contracts/access/Permissioned.sol"; + +import { IFHERC20 } from "./IFHERC20.sol"; + +error ErrorInsufficientFunds(); +error ERC20InvalidApprover(address); +error ERC20InvalidSpender(address); + + +contract FHERC20 is IFHERC20, ERC20, Permissioned { + + // A mapping from address to an encrypted balance. + mapping(address => euint32) internal _encBalances; + // A mapping from address (owner) to a mapping of address (spender) to an encrypted amount. + mapping(address => mapping(address => euint32)) private _allowed; + euint32 private totalEncryptedSupply = FHE.asEuint32(0); + + constructor( + string memory name, + string memory symbol + ) ERC20(name, symbol) {} + + function _allowanceEncrypted(address owner, address spender) public view virtual returns (euint32) { + return _allowed[owner][spender]; + } + function allowanceEncrypted( + address spender, + Permission calldata permission + ) public view virtual onlySender(permission) returns (bytes memory) { + return FHE.sealoutput(_allowanceEncrypted(msg.sender, spender), permission.publicKey); + } + + function approveEncrypted(address spender, inEuint32 calldata value) public virtual returns (bool) { + _approve(msg.sender, spender, FHE.asEuint32(value)); + return true; + } + + function _approve(address owner, address spender, euint32 value) internal { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + _allowed[owner][spender] = value; + } + + function _spendAllowance(address owner, address spender, euint32 value) internal virtual returns (euint32) { + euint32 currentAllowance = _allowanceEncrypted(owner, spender); + euint32 spent = FHE.min(currentAllowance, value); + _approve(owner, spender, (currentAllowance - spent)); + + return spent; + } + + function transferFromEncrypted(address from, address to, euint32 value) public virtual returns (euint32) { + euint32 val = value; + euint32 spent = _spendAllowance(from, msg.sender, val); + _transferImpl(from, to, spent); + return spent; + } + + function transferFromEncrypted(address from, address to, inEuint32 calldata value) public virtual returns (euint32) { + euint32 val = FHE.asEuint32(value); + euint32 spent = _spendAllowance(from, msg.sender, val); + _transferImpl(from, to, spent); + return spent; + } + + function wrap(uint32 amount) public { + if (balanceOf(msg.sender) < amount) { + revert ErrorInsufficientFunds(); + } + + _burn(msg.sender, amount); + euint32 eAmount = FHE.asEuint32(amount); + _encBalances[msg.sender] = _encBalances[msg.sender] + eAmount; + totalEncryptedSupply = totalEncryptedSupply + eAmount; + } + + function unwrap(uint32 amount) public { + euint32 encAmount = FHE.asEuint32(amount); + + euint32 amountToUnwrap = FHE.select(_encBalances[msg.sender].gt(encAmount), FHE.asEuint32(0), encAmount); + + _encBalances[msg.sender] = _encBalances[msg.sender] - amountToUnwrap; + totalEncryptedSupply = totalEncryptedSupply - amountToUnwrap; + + _mint(msg.sender, FHE.decrypt(amountToUnwrap)); + } + + function mint(uint256 amount) public { + _mint(msg.sender, amount); + } + + function mintEncrypted(inEuint32 calldata encryptedAmount) public { + euint32 amount = FHE.asEuint32(encryptedAmount); + _encBalances[msg.sender] = _encBalances[msg.sender] + amount; + totalEncryptedSupply = totalEncryptedSupply + amount; + } + + function transferEncrypted(address to, inEuint32 calldata encryptedAmount) public returns (euint32) { + return transferEncrypted(to, FHE.asEuint32(encryptedAmount)); + } + + // Transfers an amount from the message sender address to the `to` address. + function transferEncrypted(address to, euint32 amount) public returns (euint32) { + return _transferImpl(msg.sender, to, amount); + } + + // Transfers an encrypted amount. + function _transferImpl(address from, address to, euint32 amount) internal returns (euint32) { + // Make sure the sender has enough tokens. + euint32 amountToSend = FHE.select(amount.lt(_encBalances[from]), amount, FHE.asEuint32(0)); + + // Add to the balance of `to` and subract from the balance of `from`. + _encBalances[to] = _encBalances[to] + amountToSend; + _encBalances[from] = _encBalances[from] - amountToSend; + + return amountToSend; + } + + function balanceOfEncrypted( + address account, Permission memory auth + ) virtual public view onlyPermitted(auth, account) returns (bytes memory) { + return _encBalances[msg.sender].seal(auth.publicKey); + } + + // // Returns the total supply of tokens, sealed and encrypted for the caller. + // // todo: add a permission check for total supply readers + // function getEncryptedTotalSupply( + // Permission calldata permission + // ) public view onlySender(permission) returns (bytes memory) { + // return totalEncryptedSupply.seal(permission.publicKey); + // } +} \ No newline at end of file diff --git a/deploy/deployAuction.ts b/deploy/deployAuction.ts new file mode 100644 index 0000000..5f4c240 --- /dev/null +++ b/deploy/deployAuction.ts @@ -0,0 +1,35 @@ +import { DeployFunction } from "hardhat-deploy/types"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { fhenixjs, ethers } = hre; + const { deploy } = hre.deployments; + const [signer] = await ethers.getSigners(); + + if (hre.network.name === "localfhenix") { + if (await signer.getBalance() < ethers.utils.parseEther("1.0")) { + await fhenixjs.getFunds(signer.address); + } + } + const token = await deploy("FHERC20", { + from: signer.address, + args: ["token", "FHE"], + log: true, + skipIfAlreadyDeployed: true, + }); + + console.log(`auction fherc20 contract: `, token.address); + + const voting = await deploy("Auction", { + from: signer.address, + args: [token.address, 3600], + log: true, + skipIfAlreadyDeployed: false, + }); + + console.log(`Auction contract: `, voting.address); +}; + +export default func; +func.id = "deploy_auction"; +func.tags = ["Auction", "FHERC20-Auction"]; \ No newline at end of file diff --git a/deploy/deploy.ts b/deploy/deployFherc20.ts similarity index 50% rename from deploy/deploy.ts rename to deploy/deployFherc20.ts index c0eb8ed..f628270 100644 --- a/deploy/deploy.ts +++ b/deploy/deployFherc20.ts @@ -6,18 +6,21 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deploy } = hre.deployments; const [signer] = await ethers.getSigners(); - await fhenixjs.getFunds(signer.address); - - const counter = await deploy("Counter", { + if (hre.network.name === "localfhenix") { + if (await signer.getBalance() < ethers.utils.parseEther("1.0")) { + await fhenixjs.getFunds(signer.address); + } + } + const counter = await deploy("FHERC20", { from: signer.address, - args: [], + args: ["token", "FHE"], log: true, - skipIfAlreadyDeployed: false, + skipIfAlreadyDeployed: true, }); - console.log(`Counter contjract: `, counter.address); + console.log(`fherc20 contract: `, counter.address); }; export default func; -func.id = "deploy_counter"; -func.tags = ["Counter"]; +func.id = "deploy_fherc20"; +func.tags = ["FHERC20"]; diff --git a/deploy/deployVoting.ts b/deploy/deployVoting.ts new file mode 100644 index 0000000..b179bf1 --- /dev/null +++ b/deploy/deployVoting.ts @@ -0,0 +1,27 @@ +import { DeployFunction } from "hardhat-deploy/types"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { fhenixjs, ethers } = hre; + const { deploy } = hre.deployments; + const [signer] = await ethers.getSigners(); + + if (hre.network.name === "localfhenix") { + if (await signer.getBalance() < ethers.utils.parseEther("1.0")) { + await fhenixjs.getFunds(signer.address); + } + } + + const voting = await deploy("Voting", { + from: signer.address, + args: ["question??", ["yes", "no"], 30], + log: true, + skipIfAlreadyDeployed: false, + }); + + console.log(`Voting contract: `, voting.address); +}; + +export default func; +func.id = "deploy_voting"; +func.tags = ["Voting"]; \ No newline at end of file diff --git a/package.json b/package.json index 61fda73..8d70a99 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,25 @@ { - "name": "fhenix-hardhat-example", + "name": "fhenix-contract-playground", "version": "1.0.0", - "description": "An example repository to get you started with Fhenix development", + "description": "A number of ready-made contracts for usage with Fhenix", "scripts": { "test": "hardhat test", "compile": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat compile", "task:deploy": "hardhat deploy", - "task:addCount": "hardhat task:addCount", + "task:mint": "hardhat task:mint", + "task:getBalance": "hardhat task:getBalance", + "task:transfer": "hardhat task:transfer", + + "task:bid": "hardhat task:bid", + "task:endAuction": "hardhat task:endAuction", + + "task:vote": "hardhat task:vote", + "task:fundAccounts": "hardhat task:fundAccounts", "task:getCount": "hardhat task:getCount", + "task:fin": "hardhat task:fin", + "task:getWin": "hardhat task:getWin", + "task:getVote": "hardhat task:getVote", + "localfhenix:start": "hardhat localfhenix:start", "localfhenix:stop": "hardhat localfhenix:stop" }, @@ -32,8 +44,8 @@ "@openzeppelin/contracts": "^5.0.1", "chai": "^4.4.1", "cross-env": "^7.0.3", - "fhenix-hardhat-docker": "0.1.0-beta.11", - "fhenix-hardhat-plugin": "0.1.0-beta.11", + "fhenix-hardhat-docker": "0.1.0-beta.15", + "fhenix-hardhat-plugin": "0.1.0-beta.15", "hardhat": "^2.19.5", "hardhat-deploy": "^0.11.45", "hardhat-gas-reporter": "^1.0.10", diff --git a/tasks/auction/bid.ts b/tasks/auction/bid.ts new file mode 100644 index 0000000..b120038 --- /dev/null +++ b/tasks/auction/bid.ts @@ -0,0 +1,51 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; +import { FHERC20 } from "../../typechain-types"; + +task("task:bid") + .addParam("amount", "Amount to transfer (plaintext number)", "1") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { fhenixjs, ethers, deployments } = hre; + const [signer] = await ethers.getSigners(); + let signerAddress = await signer.getAddress(); + const amountToBid = Number(taskArguments.amount); + + + const fherc20 = await deployments.get("FHERC20"); + const tokenContract = await ethers.getContractAt("FHERC20", fherc20.address); + + const auction = await deployments.get("Auction"); + const auctionContract = await ethers.getContractAt("Auction", auction.address); + + const encryptedAmount = await fhenixjs.encrypt_uint32(amountToBid); + + let contractWithSigner = tokenContract.connect(signer) as unknown as FHERC20; + + console.log(`setting allowance on token contract: ${fherc20.address} for auction contract at: ${fherc20.address}`); + + console.time("allowanceDuration"); + try { + await contractWithSigner.approveEncrypted(auction.address, encryptedAmount); + } catch (e) { + console.log(`failed to set allowance: ${e}`); + console.timeEnd("allowanceDuration"); + return; + } + console.timeEnd("allowanceDuration"); + + console.log(`minting ${amountToBid}`) + await hre.run("task:mint", {amount: String(amountToBid)}) + + console.log(`bidding @ auction contract at: ${fherc20.address}, amount: ${amountToBid}`); + console.time("bidDuration"); + try { + await auctionContract.bid(encryptedAmount); + } catch (e) { + console.log(`failed to bid: ${e}`); + console.timeEnd("bidDuration"); + return; + } + console.timeEnd("bidDuration"); + + + }); diff --git a/tasks/auction/endAuction.ts b/tasks/auction/endAuction.ts new file mode 100644 index 0000000..1dc1b68 --- /dev/null +++ b/tasks/auction/endAuction.ts @@ -0,0 +1,63 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; +import { Auction } from "../../typechain-types"; + +task("task:endAuction") + .addFlag("debug", "Debugging mode") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { ethers, deployments } = hre; + const [signer] = await ethers.getSigners(); + + const auction = await deployments.get("Auction"); + + console.log( + `Running endAuction, targeting contract at: ${auction.address}`, + ); + + const contract = await ethers.getContractAt("Auction", auction.address); + + let contractWithSigner = contract.connect(signer) as unknown as Auction; + console.time("endAuction"); + try { + if (taskArguments.debug === true) { + await contractWithSigner.debugEndAuction(); + } else { + await contractWithSigner.endAuction(); + } + } catch (e) { + console.log(`failed to end auction: ${e}`); + console.timeEnd("endAuction"); + return; + } + console.timeEnd("endAuction"); + }); + + +task("task:getWinner") + .addFlag("debug", "Debugging mode") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { ethers, deployments } = hre; + const [signer] = await ethers.getSigners(); + + const auction = await deployments.get("Auction"); + + console.log( + `Running endAuction, targeting contract at: ${auction.address}`, + ); + + const contract = await ethers.getContractAt("Auction", auction.address); + + let contractWithSigner = contract.connect(signer) as unknown as Auction; + console.time("endAuction"); + try { + let winner = await contractWithSigner.getWinner(); + console.log(`winner: ${winner}`); + let winning_bid = await contractWithSigner.getWinningBid(); + console.log(`winning bid: ${winning_bid.toString()}`); + } catch (e) { + console.log(`failed to end auction: ${e}`); + console.timeEnd("endAuction"); + return; + } + console.timeEnd("endAuction"); + }); \ No newline at end of file diff --git a/tasks/auction/index.ts b/tasks/auction/index.ts new file mode 100644 index 0000000..f218a1d --- /dev/null +++ b/tasks/auction/index.ts @@ -0,0 +1,2 @@ +export * from "./bid"; +export * from "./endAuction"; \ No newline at end of file diff --git a/tasks/fherc20/getBalance.ts b/tasks/fherc20/getBalance.ts new file mode 100644 index 0000000..005f898 --- /dev/null +++ b/tasks/fherc20/getBalance.ts @@ -0,0 +1,31 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; +import { FHERC20 } from "../../typechain-types"; + +task("task:getBalance").setAction(async function ( + _taskArguments: TaskArguments, + hre, +) { + const { fhenixjs, ethers, deployments } = hre; + const [signer] = await ethers.getSigners(); + + const erc20 = await deployments.get("FHERC20"); + const address = await signer.getAddress(); + console.log(`Running getCount, targeting contract at: ${erc20.address}`); + + const contract = (await ethers.getContractAt( + "FHERC20", + erc20.address, + )) as unknown as unknown as FHERC20; + + let permit = await fhenixjs.generatePermit( + erc20.address, + undefined, // use the internal provider + signer, + ); + + const sealedResult = await contract.balanceOfEncrypted(address, permit); + let unsealed = fhenixjs.unseal(erc20.address, sealedResult); + + console.log(`got balance result: ${unsealed.toString()}`); +}); diff --git a/tasks/fherc20/index.ts b/tasks/fherc20/index.ts new file mode 100644 index 0000000..9f952dc --- /dev/null +++ b/tasks/fherc20/index.ts @@ -0,0 +1,3 @@ +export * from "./transfer"; +export * from "./getBalance"; +export * from "./mint"; diff --git a/tasks/addCount.ts b/tasks/fherc20/mint.ts similarity index 53% rename from tasks/addCount.ts rename to tasks/fherc20/mint.ts index b363f33..7365073 100644 --- a/tasks/addCount.ts +++ b/tasks/fherc20/mint.ts @@ -1,32 +1,35 @@ import { task } from "hardhat/config"; import type { TaskArguments } from "hardhat/types"; -import { Counter } from "../typechain-types"; +import { FHERC20 } from "../../typechain-types"; -task("task:addCount") - .addParam("amount", "Amount to add to the counter (plaintext number)", "1") +task("task:mint") + .addParam("amount", "Amount to transfer (plaintext number)", "1") + .addOptionalParam("to", "Destination address") .setAction(async function (taskArguments: TaskArguments, hre) { const { fhenixjs, ethers, deployments } = hre; const [signer] = await ethers.getSigners(); + let signerAddress = await signer.getAddress(); const amountToAdd = Number(taskArguments.amount); - const Counter = await deployments.get("Counter"); + + let destinationAddress = taskArguments?.to || signerAddress; + + const Counter = await deployments.get("FHERC20"); console.log( `Running addCount(${amountToAdd}), targeting contract at: ${Counter.address}`, ); - const contract = await ethers.getContractAt("Counter", Counter.address); + const contract = await ethers.getContractAt("FHERC20", Counter.address); const encyrptedAmount = await fhenixjs.encrypt_uint32(amountToAdd); - let contractWithSigner = contract.connect(signer) as unknown as Counter; + let contractWithSigner = contract.connect(signer) as unknown as FHERC20; try { - // add() gets `bytes calldata encryptedValue` - // therefore we need to pass in the `data` property - await contractWithSigner.add(encyrptedAmount.data); + await contractWithSigner.mintEncrypted(encyrptedAmount); } catch (e) { - console.log(`Failed to send add transaction: ${e}`); + console.log(`transfer balance: ${e}`); return; } }); diff --git a/tasks/fherc20/transfer.ts b/tasks/fherc20/transfer.ts new file mode 100644 index 0000000..416fee8 --- /dev/null +++ b/tasks/fherc20/transfer.ts @@ -0,0 +1,41 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; +import { FHERC20 } from "../../typechain-types"; + +task("task:transfer") + .addParam("amount", "Amount to transfer (plaintext number)", "1") + .addOptionalParam("to", "Destination address") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { fhenixjs, ethers, deployments } = hre; + const [signer] = await ethers.getSigners(); + + let signerAddress = await signer.getAddress(); + const amountToAdd = Number(taskArguments.amount); + + let destinationAddress = taskArguments?.to || signerAddress; + + const fherc20 = await deployments.get("FHERC20"); + + console.log( + `Running addCount(${amountToAdd}), targeting contract at: ${fherc20.address}`, + ); + + const contract = await ethers.getContractAt("FHERC20", fherc20.address); + + const encryptedAmount = await fhenixjs.encrypt_uint32(amountToAdd); + + let contractWithSigner = contract.connect(signer) as unknown as FHERC20; + + console.time("transferEncryptedDuration"); + + try { + await contractWithSigner.transferEncrypted(destinationAddress, encryptedAmount); + } catch (e) { + console.log(`failed to transfer balance: ${e}`); + console.timeEnd("failed transferEncryptedDuration"); + return; + } + + console.timeEnd("transferEncryptedDuration"); + + }); diff --git a/tasks/getCount.ts b/tasks/getCount.ts deleted file mode 100644 index 703451d..0000000 --- a/tasks/getCount.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { task } from "hardhat/config"; -import type { TaskArguments } from "hardhat/types"; -import { Counter } from "../typechain-types"; - -task("task:getCount").setAction(async function ( - _taskArguments: TaskArguments, - hre, -) { - const { fhenixjs, ethers, deployments } = hre; - const [signer] = await ethers.getSigners(); - - const Counter = await deployments.get("Counter"); - - console.log(`Running getCount, targeting contract at: ${Counter.address}`); - - const contract = (await ethers.getContractAt( - "Counter", - Counter.address, - )) as unknown as unknown as Counter; - - let permit = await fhenixjs.generatePermit( - Counter.address, - undefined, // use the internal provider - signer, - ); - - const result = await contract.getCounterPermit(permit); - console.log(`got count: ${result.toString()}`); - - const sealedResult = await contract.getCounterPermitSealed(permit); - let unsealed = fhenixjs.unseal(Counter.address, sealedResult); - - console.log(`got unsealed result: ${unsealed.toString()}`); -}); diff --git a/tasks/index.ts b/tasks/index.ts index ae0299e..dce4151 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -1,2 +1,3 @@ -export * from "./addCount"; -export * from "./getCount"; +export * from "./fherc20"; +export * from "./voting"; +export * from "./auction"; \ No newline at end of file diff --git a/tasks/voting/add.ts b/tasks/voting/add.ts new file mode 100644 index 0000000..2973bd4 --- /dev/null +++ b/tasks/voting/add.ts @@ -0,0 +1,25 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; + +task("task:addCount") + .addParam("amount", "Amount to add to the counter (plaintext number)") + .addParam("account", "Specify which account [0, 9]") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { fhenixjs, ethers, deployments } = hre; + const [signer] = await ethers.getSigners(); + let signerAddress = await signer.getAddress(); + + const Counter = await deployments.get("Counter"); + + const signers = await ethers.getSigners(); + + const counter = await ethers.getContractAt("Counter", Counter.address); + + console.log(`contract at: ${Counter.address}, for signer: ${signers[taskArguments.account].address}`); + + const eAmount = await fhenixjs.encrypt_uint32(Number(taskArguments.amount)); + + await counter.connect(signers[Number(taskArguments.account)]).add(eAmount); + + console.log(`Added ${taskArguments.amount} to counter!`); + }); \ No newline at end of file diff --git a/tasks/voting/finalize.ts b/tasks/voting/finalize.ts new file mode 100644 index 0000000..2b2c430 --- /dev/null +++ b/tasks/voting/finalize.ts @@ -0,0 +1,20 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; + +task("task:fin") + .addParam("account", "Specify which account [0, 9]") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { ethers, deployments } = hre; + + const Voting = await deployments.get("Voting"); + + const signers = await ethers.getSigners(); + + const voting = await ethers.getContractAt("Voting", Voting.address); + + console.log(`contract at: ${Voting.address}, for signer: ${signers[taskArguments.account].address}`); + + await voting.connect(signers[Number(taskArguments.account)]).finalize(); + + console.log(`Finalized voting!`); + }); \ No newline at end of file diff --git a/tasks/voting/fundAccounts.ts b/tasks/voting/fundAccounts.ts new file mode 100644 index 0000000..83ab243 --- /dev/null +++ b/tasks/voting/fundAccounts.ts @@ -0,0 +1,16 @@ +import { task } from "hardhat/config"; + +task("task:fundAccounts", "Prints the list of accounts", async (_taskArgs, hre) => { + const { ethers } = hre; + const accounts = await hre.ethers.getSigners(); + + const fundingAcct = accounts[0]; + + let amountInWei = ethers.utils.parseEther("0.1"); + + for (let i = 1; i < accounts.length; i++) { + const account = accounts[i]; + await fundingAcct.sendTransaction({ to: account.address, value: amountInWei }); + console.log(account.address); + } +}); \ No newline at end of file diff --git a/tasks/voting/getCount.ts b/tasks/voting/getCount.ts new file mode 100644 index 0000000..5cef932 --- /dev/null +++ b/tasks/voting/getCount.ts @@ -0,0 +1,25 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; + +task("task:getCount") + .addParam("account", "Specify which account [0, 9]") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { fhenixjs, ethers, deployments } = hre; + const [signer] = await ethers.getSigners(); + + const Counter = await deployments.get("Counter"); + + const signers = await ethers.getSigners(); + + const counter = await ethers.getContractAt("Counter", Counter.address); + + let permit = await fhenixjs.generatePermit( + counter.address, + undefined, // use the internal provider + signer, + ); + + const eAmount = await counter.connect(signers[taskArguments.account]).getCounter(permit.publicKey); + const amount = fhenixjs.unseal(Counter.address, eAmount); + console.log("Current counter: ", amount); + }); \ No newline at end of file diff --git a/tasks/voting/getVote.ts b/tasks/voting/getVote.ts new file mode 100644 index 0000000..b531af6 --- /dev/null +++ b/tasks/voting/getVote.ts @@ -0,0 +1,30 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; +import {Voting} from "../../typechain-types"; + +task("task:getVote") + .addOptionalParam("account", "Specify which account [0, 9]", "0") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { fhenixjs, ethers, deployments } = hre; + + const Voting = await deployments.get("Voting"); + + const signers = await ethers.getSigners(); + const signer = signers[Number(taskArguments.account)]; + + console.log(`getting vote for contract at: ${Voting.address}, for signer: ${signer.address}`); + + const voting = await ethers.getContractAt("Voting", Voting.address); + let contractWithSigner = voting.connect(signer) as unknown as Voting; + + const permit = await fhenixjs.generatePermit( + Voting.address, + undefined, + signers[taskArguments.account] + ); + + const userVote = await contractWithSigner.getUserVote(permit); + const decryptedVote = fhenixjs.unseal(Voting.address, userVote); + + console.log(`Account voted: ${decryptedVote}`); + }); \ No newline at end of file diff --git a/tasks/voting/getWinning.ts b/tasks/voting/getWinning.ts new file mode 100644 index 0000000..59ef08a --- /dev/null +++ b/tasks/voting/getWinning.ts @@ -0,0 +1,18 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; + +task("task:getWin") + .addParam("account", "Specify which account [0, 9]") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { ethers, deployments } = hre; + + const Voting = await deployments.get("Voting"); + + const signers = await ethers.getSigners(); + + const voting = await ethers.getContractAt("Voting", Voting.address); + + const [winOption, winTally] = await voting.connect(signers[taskArguments.account]).winning(); + + console.log(`Winning option: ${winOption}\nWinning tally: ${winTally}`); + }); \ No newline at end of file diff --git a/tasks/voting/index.ts b/tasks/voting/index.ts new file mode 100644 index 0000000..4362914 --- /dev/null +++ b/tasks/voting/index.ts @@ -0,0 +1,7 @@ +export * from "./add"; +export * from "./finalize"; +export * from "./fundAccounts"; +export * from "./getCount"; +export * from "./getVote"; +export * from "./getWinning"; +export * from "./vote"; diff --git a/tasks/voting/vote.ts b/tasks/voting/vote.ts new file mode 100644 index 0000000..aa8463a --- /dev/null +++ b/tasks/voting/vote.ts @@ -0,0 +1,30 @@ +import { task } from "hardhat/config"; +import type { ArgumentType, TaskArguments } from "hardhat/types"; +import {Voting} from "../../typechain-types"; + +task("task:vote") + .addParam("option", "Option to choose") + .addOptionalParam("account", "Specify which account [0, 9]", "0") + .setAction(async function (taskArguments: TaskArguments, hre) { + const { fhenixjs, ethers, deployments } = hre; + + const Voting = await deployments.get("Voting"); + + const signers = await ethers.getSigners(); + const signer = signers[Number(taskArguments.account)]; + + const voting = await ethers.getContractAt("Voting", Voting.address); + + console.log(`contract at: ${Voting.address}, for signer: ${signer.address}`); + + await fhenixjs.getFunds(signer.address); + + const eOption = await fhenixjs.encrypt_uint8(Number(taskArguments.option)); + let contractWithSigner = voting.connect(signer) as unknown as Voting; + + console.time("voteDuration"); + const tx = await contractWithSigner.vote(eOption); + console.timeEnd("voteDuration"); + console.log(`Voted for option ${taskArguments.option}!`); + // console.log(`Result: ${JSON.stringify(tx)}`); + }); \ No newline at end of file