diff --git a/contracts/erc4626/IERC4626.sol b/contracts/erc4626/IERC4626.sol index 131a71f..d325e23 100644 --- a/contracts/erc4626/IERC4626.sol +++ b/contracts/erc4626/IERC4626.sol @@ -11,6 +11,8 @@ abstract contract IERC4626 is ERC20 { event Deposit(address indexed from, address indexed to, uint256 amount, uint256 shares); event Withdraw(address indexed from, address indexed to, uint256 amount, uint256 shares); + error ZeroShares(uint256 numberOfShares); + /*/////////////////////////////////////////////////////////////// Mutable Functions //////////////////////////////////////////////////////////////*/ diff --git a/contracts/erc4626/Vault.sol b/contracts/erc4626/Vault.sol index b0cb9d8..67ef228 100644 --- a/contracts/erc4626/Vault.sol +++ b/contracts/erc4626/Vault.sol @@ -16,18 +16,26 @@ contract HederaVault is IERC4626 { using Bits for uint256; ERC20 public immutable asset; - address newTokenAddress; + address public newTokenAddress; uint public totalTokens; - address[] tokenAddress; + address[] public tokenAddress; address public owner; - event createdToken(address indexed createdTokenAddress); + /** + * @notice CreatedToken event. + * @dev Emitted after contract initialization, when represented shares token is deployed. + * + * @param createdToken The address of created token. + */ + event CreatedToken(address indexed createdToken); constructor( ERC20 _underlying, string memory _name, string memory _symbol ) payable ERC20(_name, _symbol, _underlying.decimals()) { + owner = msg.sender; + SafeHTS.safeAssociateToken(address(_underlying), address(this)); uint256 supplyKeyType; uint256 adminKeyType; @@ -56,7 +64,7 @@ contract HederaVault is IERC4626 { newToken.expiry = expiry; newToken.tokenKeys = keys; newTokenAddress = SafeHTS.safeCreateFungibleToken(newToken, 0, _underlying.decimals()); - emit createdToken(newTokenAddress); + emit CreatedToken(newTokenAddress); asset = _underlying; } @@ -78,10 +86,15 @@ contract HederaVault is IERC4626 { DEPOSIT/WITHDRAWAL LOGIC //////////////////////////////////////////////////////////////*/ + /** + * @dev Deposits staking token to the Vault and returns shares. + * + * @param amount The amount of staking token to send. + * @param to The shares receiver address. + * @return shares The amount of shares to receive. + */ function deposit(uint256 amount, address to) public override returns (uint256 shares) { - require((shares = previewDeposit(amount)) != 0, "ZERO_SHARES"); - - asset.approve(address(this), amount); + if ((shares = previewDeposit(amount)) == 0) revert ZeroShares(amount); asset.safeTransferFrom(msg.sender, address(this), amount); @@ -96,6 +109,13 @@ contract HederaVault is IERC4626 { afterDeposit(amount); } + /** + * @dev Mints. + * + * @param shares The amount of shares to send. + * @param to The receiver of tokens. + * @return amount The amount of tokens to receive. + */ function mint(uint256 shares, address to) public override returns (uint256 amount) { _mint(to, amount = previewMint(shares)); @@ -110,6 +130,14 @@ contract HederaVault is IERC4626 { afterDeposit(amount); } + /** + * @dev Withdraws staking token and burns shares. + * + * @param amount The amount of shares. + * @param to The staking token receiver. + * @param from The . + * @return shares The amount of shares to burn. + */ function withdraw(uint256 amount, address to, address from) public override returns (uint256 shares) { beforeWithdraw(amount); @@ -125,6 +153,14 @@ contract HederaVault is IERC4626 { asset.safeTransfer(to, amount); } + /** + * @dev Redeems . + * + * @param shares The amount of shares. + * @param to The staking token receiver. + * @param from The . + * @return amount The amount of shares to burn. + */ function redeem(uint256 shares, address to, address from) public override returns (uint256 amount) { require((amount = previewRedeem(shares)) != 0, "ZERO_ASSETS"); @@ -141,17 +177,28 @@ contract HederaVault is IERC4626 { INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ + /** + * @dev Updates user state according to withdraw inputs. + * + * @param amount The amount of shares. + */ function beforeWithdraw(uint256 amount) internal { // claimAllReward(0); userContribution[msg.sender].num_shares -= amount; totalTokens -= amount; } + /** + * @dev Updates user state according to withdraw inputs. + * + * @param amount The amount of shares. + */ function afterDeposit(uint256 amount) internal { if (!userContribution[msg.sender].exist) { for (uint i; i < tokenAddress.length; i++) { address token = tokenAddress[i]; userContribution[msg.sender].lastClaimedAmountT[token] = rewardsAddress[token].amount; + SafeHTS.safeAssociateToken(token, msg.sender); } userContribution[msg.sender].num_shares = amount; userContribution[msg.sender].exist = true; @@ -223,15 +270,18 @@ contract HederaVault is IERC4626 { REWARDS LOGIC //////////////////////////////////////////////////////////////*/ - function addReward(address _token, uint _amount) internal { + function addReward(address _token, uint _amount) public payable { require(_amount != 0, "please provide amount"); require(totalTokens != 0, "no token staked yet"); + require(msg.sender == owner, "Only owner"); + uint perShareRewards; perShareRewards = _amount.mulDivDown(1, totalTokens); if (!rewardsAddress[_token].exist) { tokenAddress.push(_token); rewardsAddress[_token].exist = true; rewardsAddress[_token].amount = perShareRewards; + SafeHTS.safeAssociateToken(_token, address(this)); ERC20(_token).safeTransferFrom(address(msg.sender), address(this), _amount); } else { rewardsAddress[_token].amount += perShareRewards; diff --git a/hardhat.config.ts b/hardhat.config.ts index 9bead94..91af1d4 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,6 +1,7 @@ import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; import "@openzeppelin/hardhat-upgrades"; +import "@nomicfoundation/hardhat-chai-matchers"; import * as dotenv from "dotenv"; diff --git a/scripts/deployVault.ts b/scripts/deployVault.ts index eb4e909..1e8abdb 100644 --- a/scripts/deployVault.ts +++ b/scripts/deployVault.ts @@ -14,35 +14,35 @@ async function main() { let client = Client.forTestnet(); const operatorPrKey = PrivateKey.fromStringECDSA(process.env.PRIVATE_KEY || ''); - const operatorAccountId = AccountId.fromString(process.env.OPERATOR_ID || ''); + const operatorAccountId = AccountId.fromString(process.env.ACCOUNT_ID || ''); client.setOperator( operatorAccountId, operatorPrKey ); - const createERC4626 = await createFungibleToken( + const stakingToken = await createFungibleToken( "ERC4626 on Hedera", "HERC4626", - process.env.OPERATOR_ID, + process.env.ACCOUNT_ID, operatorPrKey.publicKey, client, operatorPrKey ); - const stakingTokenAddress = "0x" + createERC4626!.toSolidityAddress(); + const stakingTokenAddress = "0x" + stakingToken!.toSolidityAddress(); const HederaVault = await ethers.getContractFactory("HederaVault"); const hederaVault = await HederaVault.deploy( stakingTokenAddress, "TST", "TST", - { from: deployer.address, value: ethers.parseUnits("10", 18) } + { from: deployer.address, gasLimit: 3000000, value: ethers.parseUnits("12", 18) } ); console.log("Hash ", hederaVault.deploymentTransaction()?.hash); await hederaVault.waitForDeployment(); - console.log(await hederaVault.getAddress()); + console.log("Vault deployed with address: ", await hederaVault.getAddress()); } main().catch((error) => { diff --git a/scripts/utils.ts b/scripts/utils.ts index 2c5fcd4..c3d8116 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -85,7 +85,7 @@ export async function addToken( client: Client ) { let contractFunctionParameters = new ContractFunctionParameters() - .addAddress(tokenId.toSolidityAddress()) + .addAddress(tokenId) .addUint256(amount * 1e8); const notifyRewardTx = await new ContractExecuteTransaction() @@ -153,5 +153,7 @@ module.exports = { getClient, createAccount, mintToken, - TokenTransfer + TokenTransfer, + TokenBalance, + addToken, } diff --git a/test/erc4626/vault.test.ts b/test/erc4626/vault.test.ts index a89631c..a817376 100644 --- a/test/erc4626/vault.test.ts +++ b/test/erc4626/vault.test.ts @@ -1,138 +1,248 @@ import { anyValue, ethers, expect } from "../setup"; import { TokenTransfer, createFungibleToken, TokenBalance, createAccount, addToken, mintToken } from "../../scripts/utils"; import { PrivateKey, Client, AccountId, TokenAssociateTransaction, AccountBalanceQuery } from "@hashgraph/sdk"; +import hre from "hardhat"; // constants -let client; -let aliceKey; -let aliceAccountId; +const stakingTokenId = "0.0.3757626"; +const sharesTokenAddress = "0x0000000000000000000000000000000000395640"; +const revertCasesVaultAddress = "0xb3C24B140BA2a69099276e55dE1885e93517C6C6"; +const revertCasesVaultId = "0.0.3757631"; + +const newStakingTokenId = "0.0.4229238"; +const newVaultAddress = "0x26767C096B669b0A5Df59efeF0d6CbA3840E47F6" +const newRewardTokenId = "0.0.4229229"; +const rewardTokenAddress = "0x000000000000000000000000000000000040886d"; +const newSharesTokenAddress = "0x0000000000000000000000000000000000408879"; +const newSharesTokenId = "0.0.4229241"; +const newVaultId = "0.0.4229240"; + +const vaultEr = "0x8b594f719e36cc1bbf08a24b3edbcf50cdeecae6"; // Tests describe("Vault", function () { async function deployFixture() { const [ owner, - to, - admin, - ...otherAccounts ] = await ethers.getSigners(); let client = Client.forTestnet(); const operatorPrKey = PrivateKey.fromStringECDSA(process.env.PRIVATE_KEY || ''); const operatorAccountId = AccountId.fromString(process.env.ACCOUNT_ID || ''); + const stAccountId = AccountId.fromString("0.0.2673429"); client.setOperator( operatorAccountId, operatorPrKey ); - // aliceKey = PrivateKey.generateED25519(); - // aliceAccountId = await createAccount(client, aliceKey, 20); - // const alice = createAccount(client, aliceKey, aliceAccountId); + const erc20 = await hre.artifacts.readArtifact("contracts/erc4626/ERC20.sol:ERC20"); - // console.log("account creation success"); - - const stakingToken = await createFungibleToken( - "ERC4626 on Hedera", - "HERC4626", - process.env.ACCOUNT_ID, - operatorPrKey.publicKey, - client, - operatorPrKey - ); - - const stakingTokenAddress = "0x" + stakingToken!.toSolidityAddress(); + // const rewardToken = await createFungibleToken( + // "Reward Token 1", + // "RT1", + // process.env.ACCOUNT_ID, + // operatorPrKey.publicKey, + // client, + // operatorPrKey + // ); - const HederaVault = await ethers.getContractFactory("HederaVault"); - const hederaVault = await HederaVault.deploy( - stakingTokenAddress, - "TST", - "TST", - { from: owner.address, gasLimit: 3000000, value: ethers.parseUnits("10", 18) } - ); - await hederaVault.waitForDeployment(); + // console.log("Reward token addrress", rewardToken?.toSolidityAddress()); - // client.setOperator(aliceAccountId!, aliceKey); - // const tokenAssociate = await new TokenAssociateTransaction() - // .setAccountId(aliceAccountId!) - // .setTokenIds([stakingToken!]) + // const sharesTokenAssociate = await new TokenAssociateTransaction() + // .setAccountId(operatorAccountId) + // .setTokenIds([newSharesTokenId]) // .execute(client); - // console.log("association success"); - - // await mintToken(stakingToken, client, 100, operatorPrKey); - - // console.log("token mint success"); + // const stakingTokenAssociate = await new TokenAssociateTransaction() + // .setAccountId(operatorAccountId) + // .setTokenIds([newStakingTokenId]) + // .execute(client); - // let balanceCheckTreasury = await new AccountBalanceQuery() + // const rewardTokenAssociate = await new TokenAssociateTransaction() // .setAccountId(operatorAccountId) + // .setTokenIds([newRewardTokenId]) // .execute(client); - // console.log( - // " Treasury balance: " + balanceCheckTreasury.tokens - // ); + const hederaVaultRevertCases = await ethers.getContractAt( + "HederaVault", + revertCasesVaultAddress + ); + const hederaVault = await ethers.getContractAt( + "HederaVault", + vaultEr + ); - // const stToken = ethers.getContractAt("HederaVault", stakingTokenAddress); + const rewardToken = await ethers.getContractAt( + erc20.abi, + rewardTokenAddress + ); - // console.log("balance check", await stToken.(owner.address)); + const stakingToken = await ethers.getContractAt( + erc20.abi, + await hederaVault.asset() + ); - // client.setOperator( - // operatorAccountId, - // operatorPrKey - // ); - // await TokenTransfer(stakingToken, operatorAccountId, aliceAccountId, 50, client); + const sharesToken = await ethers.getContractAt( + erc20.abi, + newSharesTokenAddress + ); - // console.log("token transfer success"); + // await TokenTransfer(newStakingTokenId, operatorAccountId, stAccountId, 10, client); - // console.log( - // await TokenBalance(receiver.address, client) - // ); + // const stakingTokenOperatorBalance = await ( + // await TokenBalance(operatorAccountId, client) + // ).tokens!.get(newStakingTokenId); + // console.log("Staking token balance: ", stakingTokenOperatorBalance.toString()); + + // const tx = await rewardToken.approve(hederaVault.target, 100); + + // const rewTx = await hederaVault.addReward(rewardTokenAddress, 100, { gasLimit: 3000000 }); return { hederaVault, + hederaVaultRevertCases, + rewardToken, stakingToken, - // stToken, - // alice, - to, + sharesToken, client, owner, - admin, - otherAccounts, }; } describe("deposit", function () { - // it("Should deposit tokens and return shares", async function () { - // const { hederaVault, to, owner, client, stakingToken } = await deployFixture(); - // // const amountToDeposit = 1; - // // const amountToWithdraw = 1 * 1e8; + it.only("Should deposit tokens and return shares", async function () { + const { hederaVault, owner, stakingToken, rewardToken } = await deployFixture(); + const amountToDeposit = 3; + + console.log("Preview deposit ", await hederaVault.previewDeposit(amountToDeposit)); + + // await rewardToken.approve(hederaVault.target, 3 * 1e8); + + // const tx = await hederaVault.addReward(rewardTokenAddress, 3 * 1e8, { gasLimit: 3000000, value: ethers.parseUnits("5", 18) }); + // console.log(tx.hash); + + // console.log("TOTAL TOKENS", (await hederaVault.rewardsAddress(rewardTokenAddress)).amount); - // // console.log("work1"); - // // await addToken(hederaVault, stakingToken, 10, client); + await stakingToken.approve(hederaVault.target, amountToDeposit); - // // console.log("work"); + const tx = await hederaVault.connect(owner).deposit( + amountToDeposit, + owner.address, + { gasLimit: 3000000 } + ); - // // const tx = await hederaVault.connect(owner).withdraw( - // // amountToWithdraw, - // // owner.address, - // // owner.address - // // ); + console.log(tx.hash); - // // console.log("with"); + await expect( + tx + ).to.emit(hederaVault, "Deposit") + .withArgs(owner.address, owner.address, amountToDeposit, anyValue); + }); + + it("Should revert if zero shares", async function () { + const { hederaVaultRevertCases, owner } = await deployFixture(); + const amountToDeposit = 0; + + await expect( + hederaVaultRevertCases.connect(owner).deposit(amountToDeposit, owner.address) + ).to.be.reverted; + }); + }); + + describe("withdraw", function () { + it.only("Should withdraw tokens", async function () { + const { hederaVault, owner, sharesToken } = await deployFixture(); + const amountToWithdraw = 1; + + console.log("Preview Withdraw ", await hederaVault.previewWithdraw(amountToWithdraw)); + + await sharesToken.approve(hederaVault.target, amountToWithdraw) + + const tx = await hederaVault.withdraw( + amountToWithdraw, + owner.address, + owner.address, + { gasLimit: 3000000 } + ); + + console.log(tx.hash); + + await expect( + tx + ).to.emit(hederaVault, "Withdraw") + .withArgs(owner.address, owner.address, amountToWithdraw, anyValue); + }); + }); + + describe("mint", function () { + it("Should mint tokens", async function () { + const { hederaVault, owner, stakingToken } = await deployFixture(); + const amountOfShares = 1; - // // // const tx = await hederaVault.connect(owner).deposit(amountToDeposit, to.address); + const amount = await hederaVault.previewMint(amountOfShares); + console.log("Preview Mint ", amount); - // // await expect( - // // tx - // // ).to.emit(hederaVault, "Withdraw") - // // .withArgs(owner.address, owner.address, amountToWithdraw, anyValue); + await stakingToken.approve(hederaVault.target, amount); + + const tx = await hederaVault.connect(owner).mint( + amountOfShares, + owner.address, + { gasLimit: 3000000 } + ); + + console.log(tx.hash); + + await expect( + tx + ).to.emit(hederaVault, "Deposit") + .withArgs(owner.address, owner.address, anyValue, amountOfShares); + }); + }); + + describe("redeem", function () { + // it("Should redeem tokens", async function () { + // const { hederaVault, owner, stakingToken, sharesToken } = await deployFixture(); + // const amountOfShares = 1; + + // const tokensAmount = await hederaVault.previewRedeem(amountOfShares); + // console.log("Preview redeem ", tokensAmount); + + // console.log("TOTAL SUPPLY", await hederaVault.totalSupply()); + // console.log("TOTAL ASSETS", await hederaVault.totalAssets()); + // console.log("TOTAL TOKENS", await hederaVault.totalTokens()); + + // await stakingToken.approve(hederaVault.target, amountOfShares); + + // const tx = await hederaVault.connect(owner).redeem( + // amountOfShares, + // owner.address, + // owner.address, + // { gasLimit: 3000000 } + // ); + + // console.log(tx.hash); + + // await expect( + // tx + // ).to.emit(hederaVault, "Withdraw") + // .withArgs(owner.address, owner.address, tokensAmount, amountOfShares); // }); - it("preview", async function () { - const { hederaVault, to, owner } = await deployFixture(); - // const amountToDeposit = 1; + it("Should revert if zero assets", async function () { + const { hederaVaultRevertCases, owner } = await deployFixture(); + const amountToReedem = 0; + + console.log(await hederaVaultRevertCases.previewRedeem(amountToReedem)); - // console.log(await hederaVault.connect(owner).previewMint(amountToDeposit)); + await expect( + hederaVaultRevertCases.connect(owner).redeem( + amountToReedem, + owner.address, + owner.address, + { gasLimit: 3000000 } + ) + ).to.be.reverted; }); }); }); diff --git a/yarn.lock b/yarn.lock index c5708fd..dead1ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1205,7 +1205,12 @@ dependencies: "@types/chai" "*" -"@types/chai@*", "@types/chai@^4.2.0": +"@types/chai@*": + version "4.3.14" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.14.tgz#ae3055ea2be43c91c9fd700a36d67820026d96e6" + integrity sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w== + +"@types/chai@^4.2.0": version "4.3.12" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.12.tgz#b192fe1c553b54f45d20543adc2ab88455a07d5e" integrity sha512-zNKDHG/1yxm8Il6uCCVsm+dRdEsJlFoDu73X17y09bId6UwoYww+vFBsAcRzl8knM1sab3Dp1VRikFQwDOtDDw==