diff --git a/contracts/Vault.sol b/contracts/Vault.sol new file mode 100644 index 0000000..aeb66b1 --- /dev/null +++ b/contracts/Vault.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./VaultShareToken.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Vault { + VaultShareToken public shareToken; + IERC20 public stableToken; + + address[] public supportedTokens; + + event Deposit(address indexed from, uint256 amount); + event Withdraw(address indexed to, uint256 amount); + event AddSupportedToken(address indexed token); + event RemoveSupportedToken(address indexed token); + + constructor(IERC20 _stableToken) { + stableToken = _stableToken; + shareToken = new VaultShareToken(); + supportedTokens.push(address(stableToken)); + + emit AddSupportedToken(address(stableToken)); + } + + function addSupportedToken(address _token) public { + supportedTokens.push(_token); + + emit AddSupportedToken(_token); + } + + function removeSupportedToken(address _token) public { + uint256 totalSupportedTokens = supportedTokens.length; + for (uint256 i = 0; i < totalSupportedTokens; i++) { + if (supportedTokens[i] == _token) { + supportedTokens[i] = supportedTokens[totalSupportedTokens - 1]; + supportedTokens.pop(); + emit RemoveSupportedToken(_token); + return; + } + } + + revert("Token not found"); + } + + function deposit(uint256 amount) public { + if (stableToken.allowance(msg.sender, address(this)) < amount) + revert("Insufficient allowance"); + + if (stableToken.balanceOf(msg.sender) < amount) + revert("Insufficient balance"); + + uint256 shares = calculateShares(amount); + + shareToken.mint(msg.sender, shares); + + stableToken.transferFrom(msg.sender, address(this), amount); + emit Deposit(msg.sender, amount); + } + + function withdraw(uint256 amount) public { + if (shareToken.balanceOf(msg.sender) < amount) + revert("Insufficient balance"); + uint256 totalSupply = shareToken.totalSupply(); + shareToken.burn(msg.sender, amount); + + uint256 totalSupportedTokens = supportedTokens.length; + for (uint256 i = 0; i < totalSupportedTokens; i++) { + address tokenAddress = supportedTokens[i]; + IERC20 token = IERC20(tokenAddress); + uint256 share = (token.balanceOf(address(this)) * amount) / + totalSupply; + token.transfer(msg.sender, share); + } + + emit Withdraw(msg.sender, amount); + } + + function calculateShares( + uint256 amount + ) public view returns (uint256 share) { + uint256 totalValue = calculateTotalValue(); + uint256 currentTotalSupply = shareToken.totalSupply(); + if (totalValue == 0 || currentTotalSupply == 0) share = amount; + else share = (currentTotalSupply * amount) / totalValue; + } + + function calculateTotalValue() public view returns (uint256 total) { + total = IERC20(supportedTokens[0]).balanceOf(address(this)); + + uint256 totalSupportedTokens = supportedTokens.length; + for (uint256 i = 1; i < totalSupportedTokens; i++) { + address tokenAddress = supportedTokens[i]; + total += IERC20(tokenAddress).balanceOf(address(this)) * 1; + } + } +} diff --git a/contracts/VaultShareToken.sol b/contracts/VaultShareToken.sol new file mode 100644 index 0000000..0f1f8d6 --- /dev/null +++ b/contracts/VaultShareToken.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/// @custom:security-contact contact@yashgoyal.dev +contract VaultShareToken is ERC20, Ownable { + constructor() ERC20("Vault Share Token", "VST") Ownable(msg.sender) {} + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public onlyOwner { + _burn(from, amount); + } +} diff --git a/test/Vault.ts b/test/Vault.ts new file mode 100644 index 0000000..46905e0 --- /dev/null +++ b/test/Vault.ts @@ -0,0 +1,114 @@ +import { + Account, + Chain, + GetContractReturnType, + PublicClient, + Transport, + WalletClient, + parseEther, +} from "viem"; +import { Vault$Type } from "../artifacts/contracts/Vault.sol/Vault"; +import { viem } from "hardhat"; +import { MockERC20$Type } from "../artifacts/contracts/test/MockERC20.sol/MockERC20"; +import { IERC20$Type } from "../artifacts/@openzeppelin/contracts/token/ERC20/IERC20.sol/IERC20"; +import { expect } from "chai"; + +describe("Vault", function () { + let wallets: WalletClient[]; + let publicClient: PublicClient; + let vault: GetContractReturnType< + Vault$Type["abi"], + PublicClient, + WalletClient + >; + let usdc: GetContractReturnType< + MockERC20$Type["abi"], + PublicClient, + WalletClient + >; + let vaultShare: GetContractReturnType< + IERC20$Type["abi"], + PublicClient, + WalletClient + >; + const accounts: Account[] = []; + + this.beforeAll(async () => { + wallets = await viem.getWalletClients(); + publicClient = await viem.getPublicClient(); + + for (const wallet of wallets) { + if (wallet.account) accounts.push(wallet.account); + } + + usdc = await viem.deployContract("MockERC20", []); + + vault = await viem.deployContract("Vault", [usdc.address]); + + const shareTokenAddress = await vault.read.shareToken(); + + vaultShare = await viem.getContractAt("IERC20", shareTokenAddress); + }); + + it("Should mint some usdc tokens", async () => { + await usdc.write.mint([accounts[0].address, parseEther("1000000")]); + await usdc.write.mint([accounts[1].address, parseEther("1000000")]); + await usdc.write.mint([accounts[2].address, parseEther("1000000")]); + await usdc.write.mint([accounts[3].address, parseEther("1000000")]); + await usdc.write.mint([accounts[4].address, parseEther("1000000")]); + await usdc.write.mint([accounts[5].address, parseEther("1000000")]); + }); + + it("Should not deposit if the usdc is not approved", async () => { + await expect(vault.write.deposit([parseEther("1000")])).to.be.rejectedWith( + "Insufficient allowance" + ); + }); + + it("Should not deposit if insufficient usdc balance", async () => { + await usdc.write.approve([vault.address, parseEther("100000000")]); + + await expect( + vault.write.deposit([parseEther("100000000")]) + ).to.be.rejectedWith("Insufficient balance"); + }); + + it("Should deposit some usdc in the vault contract", async () => { + await vault.write.deposit([parseEther("100000")]); + + expect(await vaultShare.read.balanceOf([accounts[0].address])).to.be.eq( + parseEther("100000") + ); + }); + + it("Should deposit some more tokens in the vault contract", async () => { + await usdc.write.approve([vault.address, parseEther("100000000")], { + account: accounts[1], + }); + await vault.write.deposit([parseEther("100000")], { account: accounts[1] }); + + expect(await vaultShare.read.balanceOf([accounts[1].address])).to.be.eq( + parseEther("100000") + ); + + expect(await vault.read.calculateTotalValue()).to.be.eq( + parseEther("200000") + ); + }); + + it("Should withdraw some tokens from the vault contract", async () => { + await vault.write.withdraw([parseEther("10000")]); + + expect(await vaultShare.read.balanceOf([accounts[0].address])).to.be.eq( + parseEther("90000") + ); + + expect(await vault.read.calculateTotalValue()).to.be.eq( + parseEther("190000") + ); + + expect(await usdc.read.balanceOf([accounts[0].address])).to.be.eq( + parseEther("910000") + ); + }); +}); diff --git a/test/chainlink-functions-simulators.ts b/test/chainlink-functions-simulators.ts index 24bbfc2..d7b0966 100644 --- a/test/chainlink-functions-simulators.ts +++ b/test/chainlink-functions-simulators.ts @@ -22,7 +22,6 @@ export async function startSimulator({ }) { const functionsRouter = await viem.deployContract("MockFunctionsRouter"); - // const eventFilter = functionsRouter.createEventFilter.RequestCreated(); functionsRouter.watchEvent.RequestCreated({ onLogs: async (logs) => { for (const log of logs) { @@ -42,7 +41,6 @@ export async function startSimulator({ decodedData[tag] = value; } - const code = ` class Functions { static encodeString(s) {