From 78d34929d490b0ba26af038cfed98215cfc75a79 Mon Sep 17 00:00:00 2001 From: Brendan Asselstine Date: Wed, 18 May 2022 13:53:00 -0700 Subject: [PATCH] Added TokenVault (#266) --- contracts/TokenVault.sol | 60 +++++++++++++++++++++++++++++++ test/TokenVault.test.ts | 76 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 contracts/TokenVault.sol create mode 100644 test/TokenVault.test.ts diff --git a/contracts/TokenVault.sol b/contracts/TokenVault.sol new file mode 100644 index 00000000..a7575c51 --- /dev/null +++ b/contracts/TokenVault.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +import "@pooltogether/owner-manager-contracts/contracts/Manageable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title PoolTogether Vault + * @author PoolTogether Inc Team + */ +contract Vault is Manageable { + using SafeERC20 for IERC20; + + mapping(address => bool) public approved; + + /** + * @notice Constructs Vault + * @param _owner Owner address + */ + constructor(address _owner) Ownable(_owner) {} + + function setApproved(address _account, bool _approved) external onlyOwner { + approved[_account] = _approved; + } + + /** + * @notice Decrease allowance of ERC20 tokens held by this contract. + * @dev Only callable by the owner or asset manager. + * @dev Current allowance should be computed off-chain to avoid any underflow. + * @param token Address of the ERC20 token to decrease allowance for + * @param spender Address of the spender of the tokens + * @param amount Amount of tokens to decrease allowance by + */ + function decreaseERC20Allowance( + IERC20 token, + address spender, + uint256 amount + ) external onlyManagerOrOwner { + token.safeDecreaseAllowance(spender, amount); + } + + /** + * @notice Increase allowance of ERC20 tokens held by this contract. + * @dev Only callable by the owner or asset manager. + * @dev Current allowance should be computed off-chain to avoid any overflow. + * @param token Address of the ERC20 token to increase allowance for + * @param spender Address of the spender of the tokens + * @param amount Amount of tokens to increase allowance by + */ + function increaseERC20Allowance( + IERC20 token, + address spender, + uint256 amount + ) external onlyManagerOrOwner { + require(approved[spender], "Spender must be approved"); + token.safeIncreaseAllowance(spender, amount); + } +} diff --git a/test/TokenVault.test.ts b/test/TokenVault.test.ts new file mode 100644 index 00000000..7dbf2974 --- /dev/null +++ b/test/TokenVault.test.ts @@ -0,0 +1,76 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { deployMockContract } from 'ethereum-waffle'; +import { Contract, Signer } from 'ethers'; +import { ethers, artifacts } from 'hardhat'; + +const newDebug = require('debug'); + +const debug = newDebug('pt:Vault.test.ts'); + +const { getSigners } = ethers; + +describe('Vault', () => { + let vault: Contract; + let token: Contract; + + let wallet1: SignerWithAddress; + let wallet2: SignerWithAddress; + let wallet3: SignerWithAddress; + let wallet4: SignerWithAddress; + + beforeEach(async () => { + [wallet1, wallet2, wallet3, wallet4] = await getSigners(); + + const IERC20Artifact = await artifacts.readArtifact('IERC20'); + token = await deployMockContract(wallet1 as Signer, IERC20Artifact.abi); + + const VaultFactory = await ethers.getContractFactory('Vault', wallet1); + vault = await VaultFactory.deploy(wallet1.address); + }); + + describe('constructor', () => { + it('should set the owner', async () => { + expect(await vault.owner()).to.equal(wallet1.address) + }) + }) + + describe('setApproval()', () => { + it('should allow the owner to approve accounts', async () => { + await vault.setApproved(wallet2.address, true) + expect(await vault.approved(wallet2.address)).to.equal(true) + }) + }) + + describe('increaseERC20Allowance()', () => { + it('should allow owners to increase approval amount', async () => { + await vault.setApproved(wallet2.address, true) + await token.mock.allowance.withArgs(vault.address, wallet2.address).returns('0') + await token.mock.approve.withArgs(wallet2.address, '1111').returns(true) + await vault.increaseERC20Allowance(token.address, wallet2.address, '1111') + }) + + it('should allow managers to increase approval amount', async () => { + await vault.setManager(wallet3.address) + await vault.setApproved(wallet2.address, true) + await token.mock.allowance.withArgs(vault.address, wallet2.address).returns('0') + await token.mock.approve.withArgs(wallet2.address, '1111').returns(true) + await vault.connect(wallet3).increaseERC20Allowance(token.address, wallet2.address, '1111') + }) + }) + + describe('decreaseERC20Allowance()', () => { + it('should allow manager to decrease approval amount', async () => { + await token.mock.allowance.withArgs(vault.address, wallet2.address).returns('1111') + await token.mock.approve.withArgs(wallet2.address, '111').returns(true) + await vault.decreaseERC20Allowance(token.address, wallet2.address, '1000') + }) + + it('should allow manager to decrease approval amount', async () => { + await vault.setManager(wallet3.address) + await token.mock.allowance.withArgs(vault.address, wallet2.address).returns('1111') + await token.mock.approve.withArgs(wallet2.address, '0').returns(true) + await vault.connect(wallet3).decreaseERC20Allowance(token.address, wallet2.address, '1111') + }) + }) +});