diff --git a/contracts/infrastructure/dapp/GroDepositFilter.sol b/contracts/infrastructure/dapp/GroDepositFilter.sol new file mode 100644 index 000000000..0f65bb613 --- /dev/null +++ b/contracts/infrastructure/dapp/GroDepositFilter.sol @@ -0,0 +1,45 @@ +// Copyright (C) 2021 Argent Labs Ltd. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.3; + +import "./BaseFilter.sol"; + +/** + * @title GroDepositFilter + * @notice Filter used for deposits to Gro Protocol + * @author Olivier VDB - + */ +contract GroDepositFilter is BaseFilter { + + bytes4 private constant DEPOSIT1 = bytes4(keccak256("depositPwrd(uint256[],uint256,address)")); + bytes4 private constant DEPOSIT2 = bytes4(keccak256("depositGvt(uint256[],uint256,address)")); + bytes4 private constant ERC20_APPROVE = bytes4(keccak256("approve(address,uint256)")); + + function isValid(address /*_wallet*/, address _spender, address _to, bytes calldata _data) external view override returns (bool valid) { + // disable ETH transfer + if (_data.length < 4) { + return false; + } + + bytes4 methodId = getMethod(_data); + if(_spender == _to) { + return (methodId == DEPOSIT1 || methodId == DEPOSIT2); + } else { + return (methodId == ERC20_APPROVE); + } + } +} \ No newline at end of file diff --git a/contracts/infrastructure/dapp/GroWithdrawFilter.sol b/contracts/infrastructure/dapp/GroWithdrawFilter.sol new file mode 100644 index 000000000..fb84dd0a8 --- /dev/null +++ b/contracts/infrastructure/dapp/GroWithdrawFilter.sol @@ -0,0 +1,42 @@ +// Copyright (C) 2021 Argent Labs Ltd. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.3; + +import "./BaseFilter.sol"; + +/** + * @title GroWithdrawFilter + * @notice Filter used for withdrawals from Gro Protocol + * @author Olivier VDB - + */ +contract GroWithdrawFilter is BaseFilter { + + bytes4 private constant WITHDRAW1 = bytes4(keccak256("withdrawByStablecoin(bool,uint256,uint256,uint256)")); + bytes4 private constant WITHDRAW2 = bytes4(keccak256("withdrawByLPToken(bool,uint256,uint256[])")); + bytes4 private constant WITHDRAW_ALL1 = bytes4(keccak256("withdrawAllSingle(bool,uint256,uint256)")); + bytes4 private constant WITHDRAW_ALL2 = bytes4(keccak256("withdrawAllBalanced(bool,uint256[])")); + + function isValid(address /*_wallet*/, address _spender, address _to, bytes calldata _data) external view override returns (bool valid) { + // disable ETH transfer + if (_data.length < 4) { + return false; + } + + bytes4 methodId = getMethod(_data); + return (_spender == _to && (methodId == WITHDRAW1 || methodId == WITHDRAW_ALL1 || methodId == WITHDRAW2 || methodId == WITHDRAW_ALL2)); + } +} \ No newline at end of file diff --git a/lib_0.7/gro/DepositHandlerMock.sol b/lib_0.7/gro/DepositHandlerMock.sol new file mode 100644 index 000000000..87893cd74 --- /dev/null +++ b/lib_0.7/gro/DepositHandlerMock.sol @@ -0,0 +1,18 @@ +pragma solidity >=0.6.0 <0.8.0; + +contract DepositHandlerMock { + + function referral(address referee) external view returns (address) {} + + function depositGvt( + uint256[] calldata inAmounts, + uint256 minAmount, + address referral + ) external {} + + function depositPwrd( + uint256[] calldata inAmounts, + uint256 minAmount, + address referral + ) external {} +} \ No newline at end of file diff --git a/lib_0.7/gro/WithdrawHandlerMock.sol b/lib_0.7/gro/WithdrawHandlerMock.sol new file mode 100644 index 000000000..48e105ad2 --- /dev/null +++ b/lib_0.7/gro/WithdrawHandlerMock.sol @@ -0,0 +1,27 @@ +pragma solidity >=0.6.0 <0.8.0; + +contract WithdrawHandlerMock { + + function withdrawalFee(bool pwrd) external view returns (uint256) {} + + function withdrawByLPToken( + bool pwrd, + uint256 lpAmount, + uint256[] calldata minAmounts + ) external {} + + function withdrawByStablecoin( + bool pwrd, + uint256 index, + uint256 lpAmount, + uint256 minAmount + ) external {} + + function withdrawAllSingle( + bool pwrd, + uint256 index, + uint256 minAmount + ) external {} + + function withdrawAllBalanced(bool pwrd, uint256[] calldata minAmounts) external {} +} \ No newline at end of file diff --git a/test/gro.js b/test/gro.js new file mode 100644 index 000000000..ef13e0245 --- /dev/null +++ b/test/gro.js @@ -0,0 +1,227 @@ +/* global artifacts */ + +const ethers = require("ethers"); +const chai = require("chai"); +const BN = require("bn.js"); +const bnChai = require("bn-chai"); + +const { assert } = chai; +chai.use(bnChai(BN)); + +// UniswapV2 +const UniswapV2Factory = artifacts.require("UniswapV2FactoryMock"); +const UniswapV2Router01 = artifacts.require("UniswapV2Router01Mock"); +const WETH = artifacts.require("WETH9"); + +// Gro +const DepositHandler = artifacts.require("DepositHandlerMock"); +const WithdrawHandler = artifacts.require("WithdrawHandlerMock"); +const ERC20 = artifacts.require("TestERC20"); + +// Argent +const WalletFactory = artifacts.require("WalletFactory"); +const BaseWallet = artifacts.require("BaseWallet"); +const Registry = artifacts.require("ModuleRegistry"); +const TransferStorage = artifacts.require("TransferStorage"); +const GuardianStorage = artifacts.require("GuardianStorage"); +const ArgentModule = artifacts.require("ArgentModule"); +const DappRegistry = artifacts.require("DappRegistry"); +const DepositFilter = artifacts.require("GroDepositFilter"); +const WithdrawFilter = artifacts.require("GroWithdrawFilter"); + +// Utils +const utils = require("../utils/utilities.js"); +const { ETH_TOKEN, initNonce, encodeCalls, encodeTransaction } = require("../utils/utilities.js"); + +const ZERO_ADDRESS = ethers.constants.AddressZero; +const SECURITY_PERIOD = 2; +const SECURITY_WINDOW = 2; +const LOCK_PERIOD = 4; +const RECOVERY_PERIOD = 4; +const AMOUNT = web3.utils.toWei("0.01"); + +const RelayManager = require("../utils/relay-manager"); + +contract("yEarn Filter", (accounts) => { + let manager; + + const infrastructure = accounts[0]; + const owner = accounts[1]; + const guardian1 = accounts[2]; + const relayer = accounts[4]; + const refundAddress = accounts[7]; + + let registry; + let transferStorage; + let guardianStorage; + let module; + let wallet; + let factory; + let dappRegistry; + + let uniswapRouter; + + let weth; + let tokenA; + + let depositHandler; + let withdrawHandler; + + before(async () => { + // Deploy test token + weth = await WETH.new(); + tokenA = await ERC20.new([infrastructure], web3.utils.toWei("1000"), 18); + + // Deploy Gro + depositHandler = await DepositHandler.new(); + withdrawHandler = await WithdrawHandler.new(); + + // Deploy and fund UniswapV2 + const uniswapFactory = await UniswapV2Factory.new(ZERO_ADDRESS); + uniswapRouter = await UniswapV2Router01.new(uniswapFactory.address, weth.address); + + // deploy Argent + registry = await Registry.new(); + dappRegistry = await DappRegistry.new(0); + guardianStorage = await GuardianStorage.new(); + transferStorage = await TransferStorage.new(); + module = await ArgentModule.new( + registry.address, + guardianStorage.address, + transferStorage.address, + dappRegistry.address, + uniswapRouter.address, + SECURITY_PERIOD, + SECURITY_WINDOW, + RECOVERY_PERIOD, + LOCK_PERIOD); + await registry.registerModule(module.address, ethers.utils.formatBytes32String("ArgentModule")); + const depositFilter = await DepositFilter.new(); + const withdrawFilter = await WithdrawFilter.new(); + + await dappRegistry.addDapp(0, depositHandler.address, depositFilter.address); + await dappRegistry.addDapp(0, withdrawHandler.address, withdrawFilter.address); + await dappRegistry.addDapp(0, relayer, ZERO_ADDRESS); + + const walletImplementation = await BaseWallet.new(); + factory = await WalletFactory.new( + walletImplementation.address, + guardianStorage.address, + refundAddress); + await factory.addManager(infrastructure); + manager = new RelayManager(guardianStorage.address, ZERO_ADDRESS); + }); + + beforeEach(async () => { + // create wallet + const walletAddress = await utils.createWallet(factory.address, owner, [module.address], guardian1); + wallet = await BaseWallet.at(walletAddress); + + // fund wallet + await wallet.send(web3.utils.toWei("1")); + await tokenA.mint(wallet.address, web3.utils.toWei("1000")); + + await initNonce(wallet, module, manager, SECURITY_PERIOD); + }); + + const multiCall = async (transactions) => { + const txReceipt = await manager.relay( + module, + "multiCall", + [wallet.address, transactions], + wallet, + [owner], + 1, + ETH_TOKEN, + relayer); + return utils.parseRelayReceipt(txReceipt); + }; + + const depositGvt = async () => multiCall(encodeCalls([ + [tokenA, "approve", [depositHandler.address, AMOUNT]], + [depositHandler, "depositGvt", [[AMOUNT, 0, 0], 1, ZERO_ADDRESS]] + ])); + const depositPwrd = async () => multiCall(encodeCalls([ + [tokenA, "approve", [depositHandler.address, AMOUNT]], + [depositHandler, "depositPwrd", [[AMOUNT, 0, 0], 1, ZERO_ADDRESS]] + ])); + + const withdrawByLPToken = async () => multiCall(encodeCalls([ + [withdrawHandler, "withdrawByLPToken", [true, AMOUNT, [1, 1, 1]]] + ])); + const withdrawByStablecoin = async () => multiCall(encodeCalls([ + [withdrawHandler, "withdrawByStablecoin", [true, 0, AMOUNT, 1]] + ])); + const withdrawAllSingle = async () => multiCall(encodeCalls([ + [withdrawHandler, "withdrawAllSingle", [true, 0, 1]] + ])); + const withdrawAllBalanced = async () => multiCall(encodeCalls([ + [withdrawHandler, "withdrawAllBalanced", [true, [1, 1, 1]]] + ])); + + it("should allow deposits (1/2)", async () => { + const { success, error } = await depositGvt(); + assert.isTrue(success, `deposit1 failed: "${error}"`); + }); + it("should allow deposits (2/2)", async () => { + const { success, error } = await depositPwrd(); + assert.isTrue(success, `deposit2 failed: "${error}"`); + }); + + it("should allow withdrawals (1/4)", async () => { + await depositGvt(); + const { success, error } = await withdrawByLPToken(); + assert.isTrue(success, `withdraw1 failed: "${error}"`); + }); + it("should allow withdrawals (2/4)", async () => { + await depositGvt(); + const { success, error } = await withdrawByStablecoin(); + assert.isTrue(success, `withdraw2 failed: "${error}"`); + }); + it("should allow withdrawals (3/4)", async () => { + await depositGvt(); + const { success, error } = await withdrawAllSingle(); + assert.isTrue(success, `withdraw3 failed: "${error}"`); + }); + it("should allow withdrawals (4/4)", async () => { + await depositGvt(); + const { success, error } = await withdrawAllBalanced(); + assert.isTrue(success, `withdraw4 failed: "${error}"`); + }); + + it("should not allow direct transfers to deposit handler", async () => { + const { success, error } = await multiCall(encodeCalls([[weth, "transfer", [depositHandler.address, AMOUNT]]])); + assert.isFalse(success, "transfer should have failed"); + assert.equal(error, "TM: call not authorised"); + }); + + it("should not allow direct transfers to withdraw handler", async () => { + const { success, error } = await multiCall(encodeCalls([[weth, "transfer", [withdrawHandler.address, AMOUNT]]])); + assert.isFalse(success, "transfer should have failed"); + assert.equal(error, "TM: call not authorised"); + }); + + it("should not allow unsupported method (deposit handler)", async () => { + const { success, error } = await multiCall(encodeCalls([[depositHandler, "referral", [ZERO_ADDRESS]]])); + assert.isFalse(success, "referral() should have failed"); + assert.equal(error, "TM: call not authorised"); + }); + + it("should not allow unsupported method (withdraw handler)", async () => { + const { success, error } = await multiCall(encodeCalls([[withdrawHandler, "withdrawalFee", [true]]])); + assert.isFalse(success, "withdrawalFee() should have failed"); + assert.equal(error, "TM: call not authorised"); + }); + + it("should not allow sending ETH to deposit handler", async () => { + const { success, error } = await multiCall([encodeTransaction(depositHandler.address, AMOUNT, "0x")]); + assert.isFalse(success, "sending ETH to deposit handler should have failed"); + assert.equal(error, "TM: call not authorised"); + }); + + it("should not allow sending ETH to withdraw handler", async () => { + const { success, error } = await multiCall([encodeTransaction(withdrawHandler.address, AMOUNT, "0x")]); + assert.isFalse(success, "sending ETH to withdrawal handler should have failed"); + assert.equal(error, "TM: call not authorised"); + }); +});