-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
504aa13
commit 52cdc27
Showing
4 changed files
with
229 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 [email protected] | ||
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Transport, Chain>, | ||
WalletClient<Transport, Chain, Account> | ||
>; | ||
let usdc: GetContractReturnType< | ||
MockERC20$Type["abi"], | ||
PublicClient<Transport, Chain>, | ||
WalletClient<Transport, Chain, Account> | ||
>; | ||
let vaultShare: GetContractReturnType< | ||
IERC20$Type["abi"], | ||
PublicClient<Transport, Chain>, | ||
WalletClient<Transport, Chain, Account> | ||
>; | ||
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") | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters