-
Notifications
You must be signed in to change notification settings - Fork 55
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
Changes from 12 commits
4acdef3
8c26c56
ef55ecd
dc15e01
2de2dcf
e586193
20f4858
97966be
68ecfdb
ec33d2d
39a9c1f
c010eef
4533527
c5c1493
3b3b8a7
3d9f724
54d0d41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
// 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]; | ||
} | ||
|
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extra line at EOF There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,8 +14,4 @@ contract Main is Base { | |
function returnSuper() public view virtual returns (string memory) { | ||
return super.classIdentifier(); | ||
} | ||
|
||
function destroyContract(address recipient) public { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} | ||
} |
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 () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please update the unit test flag to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}); | ||
}); | ||
|
||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extra line at EOF There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.