From d8049faf22933170f7b9a830bdd65a348e6e48f0 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Mon, 13 Jan 2025 12:04:03 +0100 Subject: [PATCH] vault: deposit and withdraw --- contracts/Vault.sol | 34 ++++++++++++++++++++ test/Vault.tests.js | 76 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 contracts/Vault.sol create mode 100644 test/Vault.tests.js diff --git a/contracts/Vault.sol b/contracts/Vault.sol new file mode 100644 index 0000000..406af87 --- /dev/null +++ b/contracts/Vault.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +using SafeERC20 for IERC20; + +contract Vault { + IERC20 private immutable _token; + mapping(address => mapping(bytes32 => uint256)) private _amounts; + + constructor(IERC20 token) { + _token = token; + } + + function amount(bytes32 id) public view returns (uint256) { + return _amounts[msg.sender][id]; + } + + function deposit(bytes32 id, address from, uint256 value) public { + require(_amounts[msg.sender][id] == 0, DepositAlreadyExists(id)); + _amounts[msg.sender][id] = value; + _token.safeTransferFrom(from, address(this), value); + } + + function withdraw(bytes32 id, address recipient) public { + uint256 value = _amounts[msg.sender][id]; + delete _amounts[msg.sender][id]; + _token.safeTransfer(recipient, value); + } + + error DepositAlreadyExists(bytes32 id); +} diff --git a/test/Vault.tests.js b/test/Vault.tests.js new file mode 100644 index 0000000..148af00 --- /dev/null +++ b/test/Vault.tests.js @@ -0,0 +1,76 @@ +const { expect } = require("chai") +const { ethers } = require("hardhat") +const { randomBytes } = ethers.utils + +describe("Vault", function () { + let token + let vault + let payer + let recipient + + beforeEach(async function () { + const TestToken = await ethers.getContractFactory("TestToken") + token = await TestToken.deploy() + const Vault = await ethers.getContractFactory("Vault") + vault = await Vault.deploy(token.address) + ;[_, payer, recipient] = await ethers.getSigners() + await token.mint(payer.address, 1_000_000) + }) + + describe("depositing", function () { + const id = randomBytes(32) + const amount = 42 + + it("accepts deposits of tokens", async function () { + await token.connect(payer).approve(vault.address, amount) + await vault.deposit(id, payer.address, amount) + expect(await vault.amount(id)).to.equal(amount) + }) + + it("keeps custody of tokens that are deposited", async function () { + await token.connect(payer).approve(vault.address, amount) + await vault.deposit(id, payer.address, amount) + expect(await token.balanceOf(vault.address)).to.equal(amount) + }) + + it("deposit fails when tokens cannot be transferred", async function () { + await token.connect(payer).approve(vault.address, amount - 1) + const depositing = vault.deposit(id, payer.address, amount) + await expect(depositing).to.be.revertedWith("insufficient allowance") + }) + + it("requires deposit ids to be unique", async function () { + await token.connect(payer).approve(vault.address, 2 * amount) + await vault.deposit(id, payer.address, amount) + const depositing = vault.deposit(id, payer.address, amount) + await expect(depositing).to.be.revertedWith("DepositAlreadyExists") + }) + + it("separates deposits from different owners", async function () { + let [owner1, owner2] = await ethers.getSigners() + await token.connect(payer).approve(vault.address, 3) + await vault.connect(owner1).deposit(id, payer.address, 1) + await vault.connect(owner2).deposit(id, payer.address, 2) + expect(await vault.connect(owner1).amount(id)).to.equal(1) + expect(await vault.connect(owner2).amount(id)).to.equal(2) + }) + }) + + describe("withdrawing", function () { + const id = randomBytes(32) + + it("can withdraw a deposit", async function () { + const amount = 42 + await token.connect(payer).approve(vault.address, amount) + await vault.deposit(id, payer.address, amount) + await vault.withdraw(id, recipient.address) + expect(await vault.amount(id)).to.equal(0) + expect(await token.balanceOf(recipient.address)).to.equal(amount) + }) + + it("ignores withdrawal of an empty deposit", async function () { + await vault.withdraw(id, recipient.address) + expect(await token.balanceOf(recipient.address)).to.equal(0) + }) + }) +})