vault: change data structure to be recipient oriented

This commit is contained in:
Mark Spanbroek 2025-01-14 12:09:48 +01:00
parent 5a2e183610
commit 7e6bc18b19
2 changed files with 81 additions and 48 deletions

View File

@ -8,27 +8,37 @@ using SafeERC20 for IERC20;
contract Vault { contract Vault {
IERC20 private immutable _token; IERC20 private immutable _token;
mapping(address => mapping(bytes32 => uint256)) private _amounts;
type Controller is address;
type Context is bytes32;
type Recipient is address;
mapping(Controller => mapping(Context => mapping(Recipient => uint256)))
private _available;
constructor(IERC20 token) { constructor(IERC20 token) {
_token = token; _token = token;
} }
function amount(bytes32 id) public view returns (uint256) { function balance(
return _amounts[msg.sender][id]; Context context,
Recipient recipient
) public view returns (uint256) {
Controller controller = Controller.wrap(msg.sender);
return _available[controller][context][recipient];
} }
function deposit(bytes32 id, address from, uint256 value) public { function deposit(Context context, address from, uint256 amount) public {
require(_amounts[msg.sender][id] == 0, DepositAlreadyExists(id)); Controller controller = Controller.wrap(msg.sender);
_amounts[msg.sender][id] = value; Recipient recipient = Recipient.wrap(from);
_token.safeTransferFrom(from, address(this), value); _available[controller][context][recipient] += amount;
_token.safeTransferFrom(from, address(this), amount);
} }
function withdraw(bytes32 id, address recipient) public { function withdraw(Context context, Recipient recipient) public {
uint256 value = _amounts[msg.sender][id]; Controller controller = Controller.wrap(msg.sender);
delete _amounts[msg.sender][id]; uint256 amount = _available[controller][context][recipient];
_token.safeTransfer(recipient, value); delete _available[controller][context][recipient];
_token.safeTransfer(Recipient.unwrap(recipient), amount);
} }
error DepositAlreadyExists(bytes32 id);
} }

View File

@ -5,72 +5,95 @@ const { randomBytes } = ethers.utils
describe("Vault", function () { describe("Vault", function () {
let token let token
let vault let vault
let payer let account
let recipient
beforeEach(async function () { beforeEach(async function () {
const TestToken = await ethers.getContractFactory("TestToken") const TestToken = await ethers.getContractFactory("TestToken")
token = await TestToken.deploy() token = await TestToken.deploy()
const Vault = await ethers.getContractFactory("Vault") const Vault = await ethers.getContractFactory("Vault")
vault = await Vault.deploy(token.address) vault = await Vault.deploy(token.address)
;[_, payer, recipient] = await ethers.getSigners() ;[, account] = await ethers.getSigners()
await token.mint(payer.address, 1_000_000) await token.mint(account.address, 1_000_000)
}) })
describe("depositing", function () { describe("depositing", function () {
const id = randomBytes(32) const context = randomBytes(32)
const amount = 42 const amount = 42
it("accepts deposits of tokens", async function () { it("accepts deposits of tokens", async function () {
await token.connect(payer).approve(vault.address, amount) await token.connect(account).approve(vault.address, amount)
await vault.deposit(id, payer.address, amount) await vault.deposit(context, account.address, amount)
expect(await vault.amount(id)).to.equal(amount) expect(await vault.balance(context, account.address)).to.equal(amount)
}) })
it("keeps custody of tokens that are deposited", async function () { it("keeps custody of tokens that are deposited", async function () {
await token.connect(payer).approve(vault.address, amount) await token.connect(account).approve(vault.address, amount)
await vault.deposit(id, payer.address, amount) await vault.deposit(context, account.address, amount)
expect(await token.balanceOf(vault.address)).to.equal(amount) expect(await token.balanceOf(vault.address)).to.equal(amount)
}) })
it("deposit fails when tokens cannot be transferred", async function () { it("deposit fails when tokens cannot be transferred", async function () {
await token.connect(payer).approve(vault.address, amount - 1) await token.connect(account).approve(vault.address, amount - 1)
const depositing = vault.deposit(id, payer.address, amount) const depositing = vault.deposit(context, account.address, amount)
await expect(depositing).to.be.revertedWith("insufficient allowance") await expect(depositing).to.be.revertedWith("insufficient allowance")
}) })
it("requires deposit ids to be unique", async function () { it("multiple deposits add to the balance", async function () {
await token.connect(payer).approve(vault.address, 2 * amount) await token.connect(account).approve(vault.address, amount)
await vault.deposit(id, payer.address, amount) await vault.deposit(context, account.address, amount / 2)
const depositing = vault.deposit(id, payer.address, amount) await vault.deposit(context, account.address, amount / 2)
await expect(depositing).to.be.revertedWith("DepositAlreadyExists") expect(await vault.balance(context, account.address)).to.equal(amount)
}) })
it("separates deposits from different owners", async function () { it("separates deposits from different contexts", async function () {
let [owner1, owner2] = await ethers.getSigners() const context1 = randomBytes(32)
await token.connect(payer).approve(vault.address, 3) const context2 = randomBytes(32)
await vault.connect(owner1).deposit(id, payer.address, 1) await token.connect(account).approve(vault.address, 3)
await vault.connect(owner2).deposit(id, payer.address, 2) await vault.deposit(context1, account.address, 1)
expect(await vault.connect(owner1).amount(id)).to.equal(1) await vault.deposit(context2, account.address, 2)
expect(await vault.connect(owner2).amount(id)).to.equal(2) expect(await vault.balance(context1, account.address)).to.equal(1)
expect(await vault.balance(context2, account.address)).to.equal(2)
})
it("separates deposits from different controllers", async function () {
const [, , controller1, controller2] = await ethers.getSigners()
const vault1 = vault.connect(controller1)
const vault2 = vault.connect(controller2)
await token.connect(account).approve(vault.address, 3)
await vault1.deposit(context, account.address, 1)
await vault2.deposit(context, account.address, 2)
expect(await vault1.balance(context, account.address)).to.equal(1)
expect(await vault2.balance(context, account.address)).to.equal(2)
}) })
}) })
describe("withdrawing", function () { describe("withdrawing", function () {
const id = randomBytes(32) const context = randomBytes(32)
const amount = 42
it("can withdraw a deposit", async function () { beforeEach(async function () {
const amount = 42 await token.connect(account).approve(vault.address, amount)
await token.connect(payer).approve(vault.address, amount) await vault.deposit(context, account.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 () { it("can withdraw a deposit", async function () {
await vault.withdraw(id, recipient.address) const before = await token.balanceOf(account.address)
expect(await token.balanceOf(recipient.address)).to.equal(0) await vault.withdraw(context, account.address)
const after = await token.balanceOf(account.address)
expect(after - before).to.equal(amount)
})
it("empties the balance when withdrawing", async function () {
await vault.withdraw(context, account.address)
expect(await vault.balance(context, account.address)).to.equal(0)
})
it("does not withdraw more than once", async function () {
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)
}) })
}) })
}) })