Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added erc 4626 token standard example (#538) #602

Merged
merged 17 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -252,19 +252,6 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newImplementation",
"type": "address"
}
],
"name": "upgradeTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,19 +265,6 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newImplementation",
"type": "address"
}
],
"name": "upgradeTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,19 +311,6 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newImplementation",
"type": "address"
}
],
"name": "upgradeTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,19 +311,6 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newImplementation",
"type": "address"
}
],
"name": "upgradeTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down
61 changes: 61 additions & 0 deletions contracts/oz/erc-4626/TokenVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.20;

import "../../erc-20/ERC20Mock.sol";

import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "@openzeppelin/contracts/access/Ownable.sol";


contract TokenVault is ERC4626 {

// a mapping that checks if a user has deposited the token
mapping(address => uint256) public shareHolders;

constructor(
IERC20 _asset,
string memory _name,
string memory _symbol
) ERC4626(_asset) ERC20(_name, _symbol) {

}

function _deposit(uint256 _assets) public {
// checks that the deposited amount is greater than zero.
require(_assets > 0, "Deposit is zero");
// calling the deposit function from the ERC-4626 library to perform all the necessary functionality
deposit(_assets, msg.sender);
// Increase the share of the user
shareHolders[msg.sender] += _assets;
}

function _withdraw(uint256 _shares, address _receiver) public {
// checks that the deposited amount is greater than zero.
require(_shares > 0, "withdraw must be greater than Zero");
// Checks that the _receiver address is not zero.
require(_receiver != address(0), "Zero Address");
// checks that the caller is a shareholder
require(shareHolders[msg.sender] > 0, "Not a shareHolder");
// checks that the caller has more shares than they are trying to withdraw.
require(shareHolders[msg.sender] >= _shares, "Not enough shares");
// Calculate 10% yield on the withdraw amount
uint256 percent = (10 * _shares) / 100;
// Calculate the total asset amount as the sum of the share amount plus 10% of the share amount.
uint256 assets = _shares + percent;
// calling the redeem function from the ERC-4626 library to perform all the necessary functionality
redeem(assets, _receiver, msg.sender);
Copy link
Member

@quiet-node quiet-node Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this redeem method sending assets to msg.sender? If so I think a checks-effects-interactions approach would be better in this case to avoid reentrant attacks. Just simply swap the line 49 with this line 47 should fix it I believe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

// Decrease the share of the user
shareHolders[msg.sender] -= _shares;
}

// returns total number of assets
function totalAssets() public view override returns (uint256) {
return super.totalAssets();
}

function totalAssetsOfUser(address _user) public view returns (uint256) {
return shareHolders[_user];
}

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra line at EOF

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

4 changes: 0 additions & 4 deletions contracts/solidity/inhetitance/Main.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,4 @@ contract Main is Base {
function returnSuper() public view virtual returns (string memory) {
return super.classIdentifier();
}

function destroyContract(address recipient) public {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to removed this last time iirc, and Nana told me that we should still keep this until new Cancun EVM release which will remove the support for selfdestruct opcode

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this got into the review. I may have run a rebase, but I didn't work on that contract.

selfdestruct(payable(recipient));
}
}
218 changes: 218 additions & 0 deletions test/oz/erc-4626/TokenVault.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
const { expect } = require("chai");
const { ethers } = require("hardhat");
const Constants = require('../../constants')

describe("TokenVault Contract", function () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the unit test flag to @OZTokenValut or @OZerc4626 or something like that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

let TokenVault;
let tokenVault;
let ERC20Mock;
let asset;
let owner;
let addr1;
let addr2;
let addrs;

async function parseRevertReason(txHash) {
const txResponse = await ethers.provider.getTransaction(txHash);
const txReceipt = await ethers.provider.getTransactionReceipt(txHash);

// Extract the revert reason
const code = txReceipt.logs[txReceipt.logs.length - 1].data;
const reason = ethers.utils.defaultAbiCoder.decode(["string"], code)[0];
return reason;
}

beforeEach(async function () {
// Deploy the mock ERC20 token
ERC20Mock = await ethers.getContractFactory("contracts/erc-20/ERC20Mock.sol:ERC20Mock");
asset = await ERC20Mock.deploy("MockToken", "MTK");
await asset.deployed();

// Deploy the TokenVault contract
TokenVault = await ethers.getContractFactory("TokenVault");
tokenVault = await TokenVault.deploy(asset.address, "MockToken", "MTK", Constants.GAS_LIMIT_1_000_000);
await tokenVault.deployed();

// Get signers
[owner, addr1, addr2, ...addrs] = await ethers.getSigners();

// Mint some tokens to the first address
await asset.mint(addr1.address, ethers.utils.parseUnits("1000", 18));
await asset.mint(addr2.address, ethers.utils.parseUnits("10", 18));

});

describe("Deployment", function () {
it("Should assign the total supply of tokens to the owner", async function () {
const ownerBalance = await tokenVault.balanceOf(owner.address);
expect(await tokenVault.totalSupply()).to.equal(ownerBalance);
});
});

describe("Transactions", function () {
it("Should deposit tokens and update shareHolders mapping", async function () {
const depositAmount = ethers.utils.parseEther("10");
await asset.connect(addr1).approve(tokenVault.address, depositAmount);
await expect(tokenVault.connect(addr1)._deposit(depositAmount))
.to.emit(tokenVault, "Deposit")
.withArgs(addr1.address, addr1.address, depositAmount, depositAmount);

expect(await tokenVault.shareHolders(addr1.address)).to.equal(depositAmount);
});

it("Should fail if deposit is less than zero", async function () {
const depositTxPromise = tokenVault.connect(addr1)._deposit(0);

// Expect the transaction to be reverted with the specific error message
// Revert reason is not available at this time, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
// await expect(depositTxPromise).to.be.revertedWith("Deposit is zero");

let receipt;
try {

const depositTx = await depositTxPromise;
receipt = await depositTx.wait();

} catch (error) {

// Handle the expected revert here until revert reason is available, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
if (error.code === ethers.errors.CALL_EXCEPTION) {
let reason = "Unknown Error";
if (error.transaction) {
expect(error.transaction.data).to.equal('0x9213b1240000000000000000000000000000000000000000000000000000000000000000');
} else {
assert.fail("No transaction data");
}
}

}
});

it("Should withdraw tokens and update shareHolders mapping", async function () {
const depositAmount = ethers.utils.parseEther("10");
const withdrawAmount = ethers.utils.parseEther("5");
const redemedAmount = ethers.utils.parseEther("5.5");

await asset.connect(addr2).approve(tokenVault.address, depositAmount);
await tokenVault.connect(addr2)._deposit(depositAmount);

await expect(tokenVault.connect(addr2)._withdraw(withdrawAmount, addr2.address))
.to.emit(tokenVault, "Withdraw")
.withArgs(addr2.address, addr2.address, addr2.address, redemedAmount, redemedAmount);

expect(await tokenVault.totalAssetsOfUser(addr2.address)).to.equal(depositAmount.sub(withdrawAmount));
});

it("Should fail if withdraw is zero", async function () {
// Expect the transaction to be reverted with the specific error message
// Revert reason is not available at this time, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
// await expect(tokenVault.connect(addr1)._withdraw(0, addr1.address)).to.be.revertedWith("withdraw must be greater than Zero");
let receipt;
try {

const depositTx = await tokenVault.connect(addr1)._withdraw(0, addr1.address);
receipt = await depositTx.wait();

} catch (error) {

// Handle the expected revert here until revert reason is available, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
if (error.code === ethers.errors.CALL_EXCEPTION) {
let reason = "Unknown Error";
if (error.transaction) {
expect(error.transaction.data).to.equal('0x293311ab0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000927e41ff8307835a1c081e0d7fd250625f2d4d0e');
} else {
assert.fail("No transaction data");
}
}
}
});

it("Should fail if withdraw is to zero address", async function () {
// Expect the transaction to be reverted with the specific error message
// Revert reason is not available at this time, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
// await expect(tokenVault.connect(addr1)._withdraw(1, ethers.constants.AddressZero)).to.be.revertedWith("Zero Address");
let receipt;
try {

const depositTx = await tokenVault.connect(addr1)._withdraw(1, ethers.constants.AddressZero);
receipt = await depositTx.wait();

} catch (error) {

// Handle the expected revert here until revert reason is available, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
if (error.code === ethers.errors.CALL_EXCEPTION) {
let reason = "Unknown Error";
if (error.transaction) {
expect(error.transaction.data).to.equal('0x293311ab00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000');
} else {
assert.fail("No transaction data");
}
}
}
});

it("Should fail if not a shareholder", async function () {
// Expect the transaction to be reverted with the specific error message
// Revert reason is not available at this time, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
// await expect(tokenVault.connect(addr2)._withdraw(1, addr2.address)).to.be.revertedWith("Not a shareHolder");
let receipt;
try {

const depositTx = await tokenVault.connect(addr2)._withdraw(1, addr2.address);
receipt = await depositTx.wait();

} catch (error) {

// Handle the expected revert here until revert reason is available, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
if (error.code === ethers.errors.CALL_EXCEPTION) {
let reason = "Unknown Error";
if (error.transaction) {
expect(error.transaction.data).to.equal('0x293311ab0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c37f417fa09933335240fca72dd257bfbde9c275');
} else {
assert.fail("No transaction data");
}
}
}
});

it("Should fail if not enough shares", async function () {
const depositAmount = ethers.utils.parseEther("10");
await asset.connect(addr1).approve(tokenVault.address, depositAmount);
await tokenVault.connect(addr1)._deposit(depositAmount);
// Expect the transaction to be reverted with the specific error message
// Revert reason is not available at this time, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
// await expect(tokenVault.connect(addr1)._withdraw(depositAmount.add(1), addr1.address)).to.be.revertedWith("Not enough shares");
let receipt;
try {

const depositTx = await tokenVault.connect(addr1)._withdraw(depositAmount.add(1), addr1.address);
receipt = await depositTx.wait();

} catch (error) {

// Handle the expected revert here until revert reason is available, https://github.com/hashgraph/hedera-json-rpc-relay/issues/1916
if (error.code === ethers.errors.CALL_EXCEPTION) {
let reason = "Unknown Error";
if (error.transaction) {
expect(error.transaction.data).to.equal('0x293311ab0000000000000000000000000000000000000000000000008ac7230489e80001000000000000000000000000927e41ff8307835a1c081e0d7fd250625f2d4d0e');
} else {
assert.fail("No transaction data");
}
}
}
});

});

describe("Views", function () {

it("Should return the total assets of a user", async function () {
const depositAmount = ethers.utils.parseEther("10");
await asset.connect(addr1).approve(tokenVault.address, depositAmount);
await tokenVault.connect(addr1)._deposit(depositAmount);

expect(await tokenVault.totalAssetsOfUser(addr1.address)).to.equal(depositAmount);
});
});

});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra line at EOF

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.