From f969b57842021adbda9c42d0decb62b62382b2da Mon Sep 17 00:00:00 2001 From: Nikolay Atanasow Date: Wed, 8 Jan 2025 18:01:26 +0200 Subject: [PATCH] feat: add WHBAR contract (#1146) * chore: add whbar contract Signed-off-by: nikolay * chore: add whbar contract Signed-off-by: nikolay * chore: edit readme Signed-off-by: nikolay * chore: fix readme Signed-off-by: nikolay * chore: fix readme Signed-off-by: nikolay * chore: fix readme Signed-off-by: nikolay * chore: add tests for receive and fallback methods Signed-off-by: nikolay --------- Signed-off-by: nikolay --- .../wrapped-tokens/WHBAR.sol/WHBAR.json | 290 ++++++++++++++++++ contracts/wrapped-tokens/README.md | 30 ++ contracts/wrapped-tokens/WHBAR.sol | 72 +++++ test/wrapped-tokens/WHBAR.js | 156 ++++++++++ 4 files changed, 548 insertions(+) create mode 100644 contracts-abi/contracts/wrapped-tokens/WHBAR.sol/WHBAR.json create mode 100644 contracts/wrapped-tokens/README.md create mode 100644 contracts/wrapped-tokens/WHBAR.sol create mode 100644 test/wrapped-tokens/WHBAR.js diff --git a/contracts-abi/contracts/wrapped-tokens/WHBAR.sol/WHBAR.json b/contracts-abi/contracts/wrapped-tokens/WHBAR.sol/WHBAR.json new file mode 100644 index 000000000..1c0d3920d --- /dev/null +++ b/contracts-abi/contracts/wrapped-tokens/WHBAR.sol/WHBAR.json @@ -0,0 +1,290 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/contracts/wrapped-tokens/README.md b/contracts/wrapped-tokens/README.md new file mode 100644 index 000000000..8c505f0da --- /dev/null +++ b/contracts/wrapped-tokens/README.md @@ -0,0 +1,30 @@ +:warning: :bangbang: ***All examples under this folder are exploration code and have NOT been audited. Use them at your own risk!*** :bangbang: :warning: + +--- + +### WHBAR + +The WHBAR contract for Wrapped HBAR to help transactions that use native token payments. + +##### Properties: +- name - ```string``` "Wrapped HBAR" +- symbol - ```string``` "WHBAR"decimals +- decimals - ```uint8``` 8 +- balanceOf - ``` mapping(address => uint256) balanceOf``` +- allowance - ```mapping(address => mapping(address => uint256)) allowance``` + +##### Events: +- Approval - ```event Approval(address src, address guy, uint256 wad)``` +- Transfer - ``` event Transfer(address src, address dst, uint256 wad)``` +- Deposit - ``` event Deposit(address dst, uint256 wad)``` +- Withdrawal - ``` event Withdrawal(address src, uint256 wad)``` + +##### Methods: +- receive - ```receive() external payable``` +- fallback - ```fallback() external payable``` +- deposit - ```function deposit() external payable``` +- withdraw - ```function withdraw(uint256 wad) external``` +- totalSupply - ```function totalSupply() public view returns (uint256)``` +- approve - ```function approve(address guy, uint256 wad) public returns (bool)``` +- transfer - ```function transfer(address dst, uint256 wad) public returns (bool)``` +- transferFrom - ```function transferFrom(address src, address dst, uint256 wad) public returns (bool)``` diff --git a/contracts/wrapped-tokens/WHBAR.sol b/contracts/wrapped-tokens/WHBAR.sol new file mode 100644 index 000000000..c681852c5 --- /dev/null +++ b/contracts/wrapped-tokens/WHBAR.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +contract WHBAR { + string public name = "Wrapped HBAR"; + string public symbol = "WHBAR"; + uint8 public decimals = 8; + + event Approval(address indexed src, address indexed guy, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + mapping(address => uint) public balanceOf; + mapping(address => mapping(address => uint)) public allowance; + + fallback() external payable { + deposit(); + } + + receive() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint wad) public { + require(balanceOf[msg.sender] >= wad); + + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint) { + return address(this).balance; + } + + function approve(address guy, uint wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + + emit Approval(msg.sender, guy, wad); + + return true; + } + + function transfer(address dst, uint wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint wad) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != + type(uint256).max) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/test/wrapped-tokens/WHBAR.js b/test/wrapped-tokens/WHBAR.js new file mode 100644 index 000000000..ab10bbeb5 --- /dev/null +++ b/test/wrapped-tokens/WHBAR.js @@ -0,0 +1,156 @@ +/* + * + * Hedera Smart Contracts + * + * Copyright (C) 2025 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 { expect } = require('chai'); +const hre = require('hardhat'); +const { ethers } = hre; + +const ONE_HBAR = 1n * 100_000_000n; +const WEIBAR_COEF = 10_000_000_000n; +const ONE_HBAR_AS_WEIBAR = ONE_HBAR * WEIBAR_COEF; + +describe('WHBAR', function() { + let signers; + let contract; + + before(async function() { + signers = await ethers.getSigners(); + }); + + it('should deploy the WHBAR contract', async function() { + const contractFactory = await ethers.getContractFactory('WHBAR'); + contract = await contractFactory.deploy(); + + await contract.waitForDeployment(); + expect(contract).to.not.be.undefined; + }); + + it('should get name', async function() { + expect(await contract.name()).to.equal('Wrapped HBAR'); + }); + + it('should get symbol', async function() { + expect(await contract.symbol()).to.equal('WHBAR'); + }); + + it('should get decimals', async function() { + expect(await contract.decimals()).to.equal(8); + }); + + it('should get totalSupply', async function() { + expect(await contract.totalSupply()).to.equal(0); + }); + + it('should deposit 1 hbar', async function() { + + const hbarBalanceBefore = await ethers.provider.getBalance(signers[0].address); + const whbarBalanceBefore = await contract.balanceOf(signers[0].address); + const totalSupplyBefore = await contract.totalSupply(); + + const txDeposit = await contract.deposit({ + value: ONE_HBAR_AS_WEIBAR + }); + await txDeposit.wait(); + + const hbarBalanceAfter = await ethers.provider.getBalance(signers[0].address); + const whbarBalanceAfter = await contract.balanceOf(signers[0].address); + const totalSupplyAfter = await contract.totalSupply(); + + expect(hbarBalanceBefore - hbarBalanceAfter).to.be.greaterThanOrEqual(ONE_HBAR_AS_WEIBAR); + expect(whbarBalanceAfter - whbarBalanceBefore).to.equal(ONE_HBAR); + expect(totalSupplyBefore + ONE_HBAR).to.equal(totalSupplyAfter); + }); + + it('should withdraw 1 hbar', async function() { + const txDeposit = await contract.deposit({ + value: ONE_HBAR_AS_WEIBAR + }); + await txDeposit.wait(); + + const hbarBalanceBefore = await ethers.provider.getBalance(signers[0].address); + const whbarBalanceBefore = await contract.balanceOf(signers[0].address); + const totalSupplyBefore = await contract.totalSupply(); + + const txWithdraw = await contract.withdraw(ONE_HBAR); + await txWithdraw.wait(); + + const hbarBalanceAfter = await ethers.provider.getBalance(signers[0].address); + const whbarBalanceAfter = await contract.balanceOf(signers[0].address); + const totalSupplyAfter = await contract.totalSupply(); + + expect(hbarBalanceBefore - hbarBalanceAfter).to.be.lessThanOrEqual(ONE_HBAR_AS_WEIBAR); + expect(whbarBalanceBefore - ONE_HBAR).to.equal(whbarBalanceAfter); + expect(totalSupplyBefore - ONE_HBAR).to.equal(totalSupplyAfter); + }); + + it('should be able to transfer', async function() { + const receiver = (ethers.Wallet.createRandom()).address; + const receiverBalanceBefore = await contract.balanceOf(receiver); + + const txDeposit = await contract.deposit({ + value: ONE_HBAR_AS_WEIBAR + }); + await txDeposit.wait(); + + const txTransfer = await contract.transfer(receiver, ONE_HBAR); + await txTransfer.wait(); + + const receiverBalanceAfter = await contract.balanceOf(receiver); + expect(receiverBalanceBefore).to.equal(0); + expect(receiverBalanceAfter).to.equal(ONE_HBAR); + }); + + it('should be able to approve', async function() { + const receiverAddress = (ethers.Wallet.createRandom()).address; + const amount = 5644; + + const txApprove = await contract.approve(receiverAddress, amount); + await txApprove.wait(); + + expect(await contract.allowance(signers[0].address, receiverAddress)).to.equal(amount); + }); + + it('should be able to deposit via contract`s fallback method', async function () { + const whbarSigner0Before = await contract.balanceOf(signers[0].address); + + const txFallback = await signers[0].sendTransaction({ + to: contract.target, + data: '0x5644aa', // non-existing contract's function, will call fallback() + value: ONE_HBAR_AS_WEIBAR + }); + await txFallback.wait(); + + const whbarSigner0After = await contract.balanceOf(signers[0].address); + expect(whbarSigner0After - whbarSigner0Before).to.equal(ONE_HBAR); + }); + + it('should be able to deposit via contract`s receive method', async function () { + const whbarSigner0Before = await contract.balanceOf(signers[0].address); + + const txReceive = await signers[0].sendTransaction({ + to: contract.target, + value: ONE_HBAR_AS_WEIBAR // missing data but passing value, will call receive() + }); + await txReceive.wait(); + + const whbarSigner0After = await contract.balanceOf(signers[0].address); + expect(whbarSigner0After - whbarSigner0Before).to.equal(ONE_HBAR); + }); +});