From e1f914726b1b646b3011302997c024b8428d8814 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Wed, 15 Jan 2025 11:43:18 +0100 Subject: [PATCH] vault: designate tokens for a single recipient --- contracts/Vault.sol | 42 +++++++++++++--- test/Vault.tests.js | 114 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 137 insertions(+), 19 deletions(-) diff --git a/contracts/Vault.sol b/contracts/Vault.sol index 5cb4e6f..0ac424b 100644 --- a/contracts/Vault.sol +++ b/contracts/Vault.sol @@ -15,6 +15,8 @@ contract Vault { mapping(Controller => mapping(Context => mapping(Recipient => uint256))) private _available; + mapping(Controller => mapping(Context => mapping(Recipient => uint256))) + private _designated; constructor(IERC20 token) { _token = token; @@ -25,7 +27,17 @@ contract Vault { Recipient recipient ) public view returns (uint256) { Controller controller = Controller.wrap(msg.sender); - return _available[controller][context][recipient]; + return + _available[controller][context][recipient] + + _designated[controller][context][recipient]; + } + + function designated( + Context context, + Recipient recipient + ) public view returns (uint256) { + Controller controller = Controller.wrap(msg.sender); + return _designated[controller][context][recipient]; } function deposit(Context context, address from, uint256 amount) public { @@ -35,17 +47,21 @@ contract Vault { _token.safeTransferFrom(from, address(this), amount); } - function withdraw(Context context, Recipient recipient) public { + function _delete(Context context, Recipient recipient) private { Controller controller = Controller.wrap(msg.sender); - uint256 amount = _available[controller][context][recipient]; delete _available[controller][context][recipient]; + delete _designated[controller][context][recipient]; + } + + function withdraw(Context context, Recipient recipient) public { + uint256 amount = balance(context, recipient); + _delete(context, recipient); _token.safeTransfer(Recipient.unwrap(recipient), amount); } function burn(Context context, Recipient recipient) public { - Controller controller = Controller.wrap(msg.sender); - uint256 amount = _available[controller][context][recipient]; - delete _available[controller][context][recipient]; + uint256 amount = balance(context, recipient); + _delete(context, recipient); _token.safeTransfer(address(0xdead), amount); } @@ -64,5 +80,19 @@ contract Vault { _available[controller][context][to] += amount; } + function designate( + Context context, + Recipient recipient, + uint256 amount + ) public { + Controller controller = Controller.wrap(msg.sender); + require( + amount <= _available[controller][context][recipient], + InsufficientBalance() + ); + _available[controller][context][recipient] -= amount; + _designated[controller][context][recipient] += amount; + } + error InsufficientBalance(); } diff --git a/test/Vault.tests.js b/test/Vault.tests.js index 6328dd1..aeb5d9e 100644 --- a/test/Vault.tests.js +++ b/test/Vault.tests.js @@ -38,7 +38,7 @@ describe("Vault", function () { await expect(depositing).to.be.revertedWith("insufficient allowance") }) - it("multiple deposits add to the balance", async function () { + it("adds multiple deposits to the balance", async function () { await token.connect(account).approve(vault.address, amount) await vault.deposit(context, account.address, amount / 2) await vault.deposit(context, account.address, amount / 2) @@ -132,48 +132,136 @@ describe("Vault", function () { const context = randomBytes(32) const amount = 42 - let other + let account2 + let account3 beforeEach(async function () { - ;[, , other] = await ethers.getSigners() + ;[, , account2, account3] = await ethers.getSigners() await token.connect(account).approve(vault.address, amount) await vault.deposit(context, account.address, amount) }) it("can transfer tokens from one recipient to the other", async function () { - await vault.transfer(context, account.address, other.address, amount) + await vault.transfer(context, account.address, account2.address, amount) expect(await vault.balance(context, account.address)).to.equal(0) - expect(await vault.balance(context, other.address)).to.equal(amount) + expect(await vault.balance(context, account2.address)).to.equal(amount) }) it("can transfer part of a balance", async function () { - await vault.transfer(context, account.address, other.address, 10) + await vault.transfer(context, account.address, account2.address, 10) expect(await vault.balance(context, account.address)).to.equal( amount - 10 ) - expect(await vault.balance(context, other.address)).to.equal(10) + expect(await vault.balance(context, account2.address)).to.equal(10) }) it("does not transfer more than the balance", async function () { await expect( - vault.transfer(context, account.address, other.address, amount + 1) + vault.transfer(context, account.address, account2.address, amount + 1) ).to.be.revertedWith("InsufficientBalance") }) it("can withdraw funds that were transfered in", async function () { - await vault.transfer(context, account.address, other.address, amount) - const before = await token.balanceOf(other.address) - await vault.withdraw(context, other.address) - const after = await token.balanceOf(other.address) + await vault.transfer(context, account.address, account2.address, amount) + const before = await token.balanceOf(account2.address) + await vault.withdraw(context, account2.address) + const after = await token.balanceOf(account2.address) expect(after - before).to.equal(amount) }) it("cannot withdraw funds that were transfered out", async function () { - await vault.transfer(context, account.address, other.address, amount) + await vault.transfer(context, account.address, account2.address, amount) const before = await token.balanceOf(account.address) await vault.withdraw(context, account.address) const after = await token.balanceOf(account.address) expect(after).to.equal(before) }) + + it("can transfer out funds that were transfered in", async function () { + await vault.transfer(context, account.address, account2.address, amount) + await vault.transfer(context, account2.address, account3.address, amount) + expect(await vault.balance(context, account2.address)).to.equal(0) + expect(await vault.balance(context, account3.address)).to.equal(amount) + }) + }) + + describe("designating", async function () { + const context = randomBytes(32) + const amount = 42 + + let account2 + + beforeEach(async function () { + ;[, , account2] = await ethers.getSigners() + await token.connect(account).approve(vault.address, amount) + await vault.deposit(context, account.address, amount) + }) + + it("can designate tokens for a single recipient", async function () { + await vault.designate(context, account.address, amount) + expect(await vault.designated(context, account.address)).to.equal(amount) + }) + + it("can designate part of the balance", async function () { + await vault.designate(context, account.address, 10) + expect(await vault.designated(context, account.address)).to.equal(10) + }) + + it("adds up designated tokens", async function () { + await vault.designate(context, account.address, 10) + await vault.designate(context, account.address, 10) + expect(await vault.designated(context, account.address)).to.equal(20) + }) + + it("cannot designate more than the undesignated balance", async function () { + await vault.designate(context, account.address, amount) + await expect( + vault.designate(context, account.address, 1) + ).to.be.revertedWith("InsufficientBalance") + }) + + it("does not change the balance", async function () { + await vault.designate(context, account.address, 10) + expect(await vault.balance(context, account.address)).to.equal(amount) + }) + + it("does not allow designated tokens to be transfered", async function () { + await vault.designate(context, account.address, 1) + await expect( + vault.transfer(context, account.address, account2.address, amount) + ).to.be.revertedWith("InsufficientBalance") + }) + + it("allows designated tokens to be withdrawn", async function () { + await vault.designate(context, account.address, 10) + const before = await token.balanceOf(account.address) + await vault.withdraw(context, account.address) + const after = await token.balanceOf(account.address) + expect(after - before).to.equal(amount) + }) + + it("does not withdraw designated tokens more than once", async function () { + await vault.designate(context, account.address, 10) + await vault.withdraw(context, account.address) + const before = await token.balanceOf(account.address) + await vault.withdraw(context, account.address) + const after = await token.balanceOf(account.address) + expect(after).to.equal(before) + }) + + it("allows designated tokens to be burned", async function () { + await vault.designate(context, account.address, 10) + await vault.burn(context, account.address) + expect(await vault.balance(context, account.address)).to.equal(0) + }) + + it("moves burned designated tokens to address 0xdead", async function () { + const dead = "0x000000000000000000000000000000000000dead" + await vault.designate(context, account.address, 10) + const before = await token.balanceOf(dead) + await vault.burn(context, account.address) + const after = await token.balanceOf(dead) + expect(after - before).to.equal(amount) + }) }) })