From db6afbb089fd5b4b085cc4589f87995c3f85a494 Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Wed, 25 Oct 2023 11:39:09 -0500 Subject: [PATCH] feat: added modular solidity example contract (#494) (#523) Signed-off-by: Logan Nguyen --- contracts/solidity/modular/Token.sol | 61 +++++++++++++ test/solidity/modular/Token.js | 123 +++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 contracts/solidity/modular/Token.sol create mode 100644 test/solidity/modular/Token.js diff --git a/contracts/solidity/modular/Token.sol b/contracts/solidity/modular/Token.sol new file mode 100644 index 000000000..1c887ebd6 --- /dev/null +++ b/contracts/solidity/modular/Token.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.20; + +library Balances { + function move(mapping(address => uint256) storage balances, address from, address to, uint amount) internal { + require(balances[from] >= amount); + require(balances[to] + amount >= balances[to]); + balances[from] -= amount; + balances[to] += amount; + } +} + +contract Token { + /// storage states + using Balances for *; + mapping(address => uint256) balances; + mapping(address => mapping(address => uint256)) allowed; + + /// events + event Transfer(address from, address to, uint amount); + event Approval(address owner, address spender, uint amount); + + /// constructor + constructor(uint256 amount) { + balances[msg.sender] = amount; + } + + /// transfer `amount` from `msg.sender` to `to` + function transfer(address to, uint amount) external returns (bool success) { + balances.move(msg.sender, to, amount); + emit Transfer(msg.sender, to, amount); + return true; + } + + /// transfer `amount` from `from` to `to` + function transferFrom(address from, address to, uint amount) external returns (bool success) { + require(allowed[from][msg.sender] >= amount); + allowed[from][msg.sender] -= amount; + balances.move(from, to, amount); + emit Transfer(from, to, amount); + return true; + } + + /// approve an `amount` of allowance for `spender` + function approve(address spender, uint amount) external returns (bool success) { + require(allowed[msg.sender][spender] == 0, ""); + allowed[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + /// util method to check balances of addresses + function balanceOf(address tokenOwner) external view returns (uint balance) { + return balances[tokenOwner]; + } + + /// util method to check allowances + function allowance(address owner, address spender) external view returns (uint balance) { + return allowed[owner][spender]; + } +} \ No newline at end of file diff --git a/test/solidity/modular/Token.js b/test/solidity/modular/Token.js new file mode 100644 index 000000000..562f090b5 --- /dev/null +++ b/test/solidity/modular/Token.js @@ -0,0 +1,123 @@ +/*- + * + * 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 { expect } = require('chai') +const { ethers } = require('hardhat') + +describe('@solidityequiv4 Modular Token', () => { + const INITIAL_AMOUNT = 12000 + let modularTokenContract, signers, accountA, accountB + + beforeEach(async () => { + signers = await ethers.getSigners() + accountA = await signers[0].getAddress() + accountB = await signers[1].getAddress() + + const modularTokenContractFactory = await ethers.getContractFactory('Token') + + modularTokenContract = await modularTokenContractFactory.deploy( + INITIAL_AMOUNT + ) + }) + + it('Deployment', async () => { + const initialBalance = await modularTokenContract.balanceOf( + accountA // deployer + ) + + expect(initialBalance).to.eq(INITIAL_AMOUNT) + expect(ethers.utils.isAddress(modularTokenContract.address)).to.be.true + }) + + it('Should transfer an `amount` of token from `msg.sender` to `to` address', async () => { + const TRANSFER_AMOUNT = 3000 + + // execute transaction + const tx = await modularTokenContract.transfer(accountB, TRANSFER_AMOUNT) + + // retrieve states from event + const receipt = await tx.wait() + const event = receipt.events.map((e) => e.event === 'Transfer' && e)[0] + const [from, to, amount] = event.args + + // retrieve balances after transfer + const accountABalance = await modularTokenContract.balanceOf(accountA) + const accountBBalance = await modularTokenContract.balanceOf(accountB) + + // assertion + expect(from).to.eq(accountA) + expect(to).to.eq(accountB) + expect(amount).to.eq(TRANSFER_AMOUNT) + expect(accountABalance).to.eq(INITIAL_AMOUNT - TRANSFER_AMOUNT) + expect(accountBBalance).to.eq(TRANSFER_AMOUNT) + }) + + it('Should let `msg.sender` approve an `amount` of allowance for `spender`', async () => { + const ALLOWANCE = 3000 + + // execute transaction + const tx = await modularTokenContract.approve(accountB, ALLOWANCE) + + // retrieve states from event + const receipt = await tx.wait() + const event = receipt.events.map((e) => e.event === 'Approval' && e)[0] + const [owner, spender, allowance] = event.args + + // retrieve allowance from contract + const storageAllowance = await modularTokenContract.allowance( + accountA, + accountB + ) + + // assertion + expect(owner).to.eq(accountA) + expect(spender).to.eq(accountB) + expect(allowance).to.eq(ALLOWANCE) + expect(storageAllowance).to.eq(ALLOWANCE) + }) + + it('Should let `msg.sender` transfer an `amount` to `to` on behalf of `from`', async () => { + const ALLOWANCE = 3000 + + // accountA first need to approve an allowance for accountB + await modularTokenContract.approve(accountB, ALLOWANCE) + + // execute transferFrom by signer[1] (i.e. accountB) + const tx = await modularTokenContract + .connect(signers[1]) + .transferFrom(accountA, accountB, ALLOWANCE) + + // retrieve states from event + const receipt = await tx.wait() + const event = receipt.events.map((e) => e.event === 'Transfer' && e)[0] + const [from, to, amount] = event.args + + // retrieve balances and allowance from storage + const accountABalance = await modularTokenContract.balanceOf(accountA) + const accountBBalance = await modularTokenContract.balanceOf(accountB) + + // assertion + expect(to).to.eq(accountB) + expect(from).to.eq(accountA) + expect(amount).to.eq(ALLOWANCE) + expect(accountBBalance).to.eq(ALLOWANCE) + expect(accountABalance).to.eq(INITIAL_AMOUNT - ALLOWANCE) + }) +})