From 29088e0e39103fb969ca07b4a419e91f41d140dd Mon Sep 17 00:00:00 2001 From: kalrashivam Date: Sat, 9 Dec 2023 23:58:55 +0530 Subject: [PATCH 1/5] initial commit --- contracts/contracts/StoplossPlugin.sol | 47 ++++++++++++++++++++++++++ contracts/package.json | 6 +++- contracts/yarn.lock | 36 ++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 contracts/contracts/StoplossPlugin.sol diff --git a/contracts/contracts/StoplossPlugin.sol b/contracts/contracts/StoplossPlugin.sol new file mode 100644 index 0000000..f87f657 --- /dev/null +++ b/contracts/contracts/StoplossPlugin.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.18; +import {ISafe} from "@safe-global/safe-core-protocol/contracts/interfaces/Accounts.sol"; +import {ISafeProtocolManager} from "@safe-global/safe-core-protocol/contracts/interfaces/Manager.sol"; +import {SafeTransaction, SafeProtocolAction} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol"; +import {BasePluginWithEventMetadata, PluginMetadata} from "./Base.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; + +contract StoplossPlugin is BasePluginWithEventMetadata { + + struct stopLoss { + uint256 stopLossLimit; + address tokenAddress; + address contractAddress; + bytes swapTx; + } + + // safe account => stopLoss + mapping(address => stopLoss) public stopLossBots; + + event AddStopLoss(address indexed safeAccount, address indexed tokenAddress, address contractAddress, uint256 stopLossLimit); + + constructor() + BasePluginWithEventMetadata( + PluginMetadata({name: "Stoploss Plugin", version: "1.0.0", requiresRootAccess: true, iconUrl: "", appUrl: ""}) + ) + {} + + function executeFromPlugin( + address _safeAddress, + ISafeProtocolManager manager, + ISafe safe + ) external returns (bytes memory data) { + SafeProtocolAction memory safeProtocolAction = SafeProtocolAction(payable(address(safe)), 0, txData); + SafeRootAccess memory safeTx = SafeRootAccess(safeProtocolAction, 0, ""); + (data) = manager.executeRootAccess(safe, safeTx); + + emit OwnerReplaced(address(safe), oldOwner, newOwner); + } + + function addStopLoss(uint256 _stopLossLimit, address _tokenAddress, address _contractAddress, bytes calldata _swapTx) external { + stopLossBots[msg.sender] = stopLoss(_stopLossLimit, _tokenAddress, _contractAddress, _swapTx); + emit AddStopLoss(msg.sender, _tokenAddress, _contractAddress, _stopLossLimit); + } + +} \ No newline at end of file diff --git a/contracts/package.json b/contracts/package.json index 4fd6614..facdb6f 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -69,5 +69,9 @@ "scripts", "test", "artifacts" - ] + ], + "dependencies": { + "@uniswap/v3-core": "^1.0.1", + "@uniswap/v3-periphery": "^1.4.4" + } } diff --git a/contracts/yarn.lock b/contracts/yarn.lock index ff252c9..d8137cf 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -786,6 +786,11 @@ "@nomicfoundation/solidity-analyzer-win32-ia32-msvc" "0.1.1" "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.1" +"@openzeppelin/contracts@3.4.2-solc-0.7": + version "3.4.2-solc-0.7" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz#38f4dbab672631034076ccdf2f3201fab1726635" + integrity sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA== + "@openzeppelin/contracts@4.8.0": version "4.8.0" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.0.tgz#6854c37df205dd2c056bdfa1b853f5d732109109" @@ -1194,6 +1199,32 @@ "@typescript-eslint/types" "6.6.0" eslint-visitor-keys "^3.4.1" +"@uniswap/lib@^4.0.1-alpha": + version "4.0.1-alpha" + resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-4.0.1-alpha.tgz#2881008e55f075344675b3bca93f020b028fbd02" + integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA== + +"@uniswap/v2-core@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" + integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== + +"@uniswap/v3-core@^1.0.0", "@uniswap/v3-core@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.1.tgz#b6d2bdc6ba3c3fbd610bdc502395d86cd35264a0" + integrity sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ== + +"@uniswap/v3-periphery@^1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.4.4.tgz#d2756c23b69718173c5874f37fd4ad57d2f021b7" + integrity sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw== + dependencies: + "@openzeppelin/contracts" "3.4.2-solc-0.7" + "@uniswap/lib" "^4.0.1-alpha" + "@uniswap/v2-core" "^1.0.1" + "@uniswap/v3-core" "^1.0.0" + base64-sol "1.0.1" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1508,6 +1539,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64-sol@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/base64-sol/-/base64-sol-1.0.1.tgz#91317aa341f0bc763811783c5729f1c2574600f6" + integrity sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg== + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" From 77c51df7729ae0af1b3830b1cbe3269ef622097e Mon Sep 17 00:00:00 2001 From: kalrashivam Date: Sun, 10 Dec 2023 01:22:48 +0530 Subject: [PATCH 2/5] change execution function --- contracts/contracts/StoplossPlugin.sol | 43 ++++++++++++++++++-------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/contracts/contracts/StoplossPlugin.sol b/contracts/contracts/StoplossPlugin.sol index f87f657..e5bdb0d 100644 --- a/contracts/contracts/StoplossPlugin.sol +++ b/contracts/contracts/StoplossPlugin.sol @@ -7,23 +7,38 @@ import {BasePluginWithEventMetadata, PluginMetadata} from "./Base.sol"; import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; +/// @title A safe plugin to implement stopLoss on a certain token in safe +/// @author https://github.com/kalrashivam +/// @notice Creates an event which can be used to create +/// a bot to track price and then trigger safe transaction through plugin. +/// @dev The plugin is made based on the safe-core-demo-template contract StoplossPlugin is BasePluginWithEventMetadata { - struct stopLoss { + struct StopLoss { uint256 stopLossLimit; address tokenAddress; address contractAddress; - bytes swapTx; + SafeTransaction safeSwapTx; } // safe account => stopLoss - mapping(address => stopLoss) public stopLossBots; + mapping(address => StopLoss) public Bots; + /// @notice Listen for this event to create your stop loss bot + /// @param safeAccount safe account address. + /// @param tokenAddress token address to apply stoploss on. + /// @param contractAddress address of the uniswap/cowswap pair to perform transaction on. + /// @param stopLossLimit the limit after which the swap should be triggered. event AddStopLoss(address indexed safeAccount, address indexed tokenAddress, address contractAddress, uint256 stopLossLimit); + // Listen for this event to remove the bot + event RemoveStopLoss(address indexed safeAccount, address indexed tokenAddress); + + // raised when the swap on uniswap fails, check for this in the bot. + error SwapFailure(bytes data); constructor() BasePluginWithEventMetadata( - PluginMetadata({name: "Stoploss Plugin", version: "1.0.0", requiresRootAccess: true, iconUrl: "", appUrl: ""}) + PluginMetadata({name: "Stoploss Plugin", version: "1.0.0", requiresRootAccess: false, iconUrl: "", appUrl: ""}) ) {} @@ -31,17 +46,19 @@ contract StoplossPlugin is BasePluginWithEventMetadata { address _safeAddress, ISafeProtocolManager manager, ISafe safe - ) external returns (bytes memory data) { - SafeProtocolAction memory safeProtocolAction = SafeProtocolAction(payable(address(safe)), 0, txData); - SafeRootAccess memory safeTx = SafeRootAccess(safeProtocolAction, 0, ""); - (data) = manager.executeRootAccess(safe, safeTx); + ) external { + StopLoss memory stopLossBot = Bots[_safeAddress]; - emit OwnerReplaced(address(safe), oldOwner, newOwner); + try manager.executeTransaction(safe, stopLossBot.safeSwapTx) returns (bytes[] memory) { + delete Bots[_safeAddress]; + emit RemoveStopLoss(_safeAddress, stopLossBot.tokenAddress); + } catch (bytes memory reason) { + revert SwapFailure(reason); + } } - function addStopLoss(uint256 _stopLossLimit, address _tokenAddress, address _contractAddress, bytes calldata _swapTx) external { - stopLossBots[msg.sender] = stopLoss(_stopLossLimit, _tokenAddress, _contractAddress, _swapTx); + function addStopLoss(uint256 _stopLossLimit, address _tokenAddress, address _contractAddress, SafeTransaction calldata _safeSwapTx) external { + Bots[msg.sender] = StopLoss(_stopLossLimit, _tokenAddress, _contractAddress, _safeSwapTx); emit AddStopLoss(msg.sender, _tokenAddress, _contractAddress, _stopLossLimit); } - -} \ No newline at end of file +} From 97d111ad6eb97e6c037b6814395d9df53035aad1 Mon Sep 17 00:00:00 2001 From: kalrashivam Date: Sun, 10 Dec 2023 02:43:00 +0530 Subject: [PATCH 3/5] add signature check --- contracts/contracts/StoplossPlugin.sol | 14 ++- contracts/test/StoplossPlugin.spec.ts | 133 +++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 contracts/test/StoplossPlugin.spec.ts diff --git a/contracts/contracts/StoplossPlugin.sol b/contracts/contracts/StoplossPlugin.sol index e5bdb0d..2ca9034 100644 --- a/contracts/contracts/StoplossPlugin.sol +++ b/contracts/contracts/StoplossPlugin.sol @@ -45,8 +45,13 @@ contract StoplossPlugin is BasePluginWithEventMetadata { function executeFromPlugin( address _safeAddress, ISafeProtocolManager manager, - ISafe safe + ISafe safe, + bytes32 _hashedMessage, + bytes32 _r, + bytes32 _s, + uint8 _v ) external { + verifyMessage(_hashedMessage, _v, _r, _s); StopLoss memory stopLossBot = Bots[_safeAddress]; try manager.executeTransaction(safe, stopLossBot.safeSwapTx) returns (bytes[] memory) { @@ -61,4 +66,11 @@ contract StoplossPlugin is BasePluginWithEventMetadata { Bots[msg.sender] = StopLoss(_stopLossLimit, _tokenAddress, _contractAddress, _safeSwapTx); emit AddStopLoss(msg.sender, _tokenAddress, _contractAddress, _stopLossLimit); } + + function verifyMessage(bytes32 _hashedMessage, uint8 _v, bytes32 _r, bytes32 _s) public pure returns (address) { + bytes memory prefix = "\x19Ethereum Signed Message:\n32"; + bytes32 prefixedHashMessage = keccak256(abi.encodePacked(prefix, _hashedMessage)); + address signer = ecrecover(prefixedHashMessage, _v, _r, _s); + return signer; + } } diff --git a/contracts/test/StoplossPlugin.spec.ts b/contracts/test/StoplossPlugin.spec.ts new file mode 100644 index 0000000..bd7046c --- /dev/null +++ b/contracts/test/StoplossPlugin.spec.ts @@ -0,0 +1,133 @@ +import hre, { deployments, ethers } from "hardhat"; +import { expect } from "chai"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { getWhiteListPlugin, getInstance } from "./utils/contracts"; +import { loadPluginMetadata } from "../src/utils/metadata"; +import { buildSingleTx } from "../src/utils/builder"; +import { ISafeProtocolManager__factory, MockContract } from "../typechain-types"; +import { MaxUint256, ZeroHash } from "ethers"; +import { getProtocolManagerAddress } from "../src/utils/protocol"; + +describe("StopLossPlugin", async () => { + let user1: SignerWithAddress, user2: SignerWithAddress; + + before(async () => { + [user1, user2] = await hre.ethers.getSigners(); + }); + + const setup = deployments.createFixture(async ({ deployments }) => { + await deployments.fixture(); + const manager = await ethers.getContractAt("MockContract", await getProtocolManagerAddress(hre)); + + const account = await (await ethers.getContractFactory("ExecutableMockContract")).deploy(); + const plugin = await getWhiteListPlugin(hre); + return { + account, + plugin, + manager, + }; + }); + + it("should be initialized correctly", async () => { + const { plugin } = await setup(); + expect(await plugin.name()).to.be.eq("Whitelist Plugin"); + expect(await plugin.version()).to.be.eq("1.0.0"); + expect(await plugin.requiresRootAccess()).to.be.false; + }); + + it("can retrieve meta data for module", async () => { + const { plugin } = await setup(); + expect(await loadPluginMetadata(hre, plugin)).to.be.deep.eq({ + name: "Whitelist Plugin", + version: "1.0.0", + requiresRootAccess: false, + iconUrl: "", + appUrl: "", + }); + }); + + it("should emit AddressWhitelisted when account is whitelisted", async () => { + const { plugin, account } = await setup(); + const data = plugin.interface.encodeFunctionData("addToWhitelist", [user1.address]); + expect(await account.executeCallViaMock(await plugin.getAddress(), 0, data, MaxUint256)) + .to.emit(plugin, "AddressWhitelisted") + .withArgs(user1.address); + }); + + it("Should not allow calls to non-whitelist address", async () => { + const { plugin, account, manager } = await setup(); + + // Required for isOwner(address) to return true + account.givenMethodReturnBool("0x2f54bf6e", true); + + const safeTx = buildSingleTx(user2.address, 0n, "0x", 0n, hre.ethers.randomBytes(32)); + + await expect( + plugin.executeFromPlugin(await manager.getAddress(), await account.getAddress(), safeTx), + ).to.be.revertedWithCustomError(plugin, "AddressNotWhiteListed"); + }); + + it("Should not allow non-owner to execute transaction to whitelisted address", async () => { + const { plugin, account, manager } = await setup(); + const safeAddress = await account.getAddress(); + const data = plugin.interface.encodeFunctionData("addToWhitelist", [user2.address]); + await account.executeCallViaMock(await plugin.getAddress(), 0, data, MaxUint256); + + // Required for isOwner(address) to return false + account.givenMethodReturnBool("0x2f54bf6e", false); + + const safeTx = buildSingleTx(user2.address, 0n, "0x", 0n, ZeroHash); + await expect(plugin.connect(user1).executeFromPlugin(manager.target, safeAddress, safeTx)) + .to.be.revertedWithCustomError(plugin, "CallerIsNotOwner") + .withArgs(safeAddress, user1.address); + + const managerInterface = ISafeProtocolManager__factory.createInterface(); + const expectedData = managerInterface.encodeFunctionData("executeTransaction", [account.target, safeTx]); + + expect(await manager.invocationCount()).to.be.eq(0); + expect(await manager.invocationCountForMethod(expectedData)).to.be.eq(0); + }); + + it("Should allow to execute transaction to whitelisted address", async () => { + const { plugin, account, manager } = await setup(); + const safeAddress = await account.getAddress(); + const data = plugin.interface.encodeFunctionData("addToWhitelist", [user2.address]); + await account.executeCallViaMock(await plugin.getAddress(), 0, data, MaxUint256); + // Required for isOwner(address) to return true + account.givenMethodReturnBool("0x2f54bf6e", true); + + const safeTx = buildSingleTx(user2.address, 0n, "0x", 0n, ZeroHash); + expect(await plugin.connect(user1).executeFromPlugin(manager.target, safeAddress, safeTx)); + + const managerInterface = ISafeProtocolManager__factory.createInterface(); + const expectedData = managerInterface.encodeFunctionData("executeTransaction", [account.target, safeTx]); + + expect(await manager.invocationCount()).to.be.eq(1); + expect(await manager.invocationCountForMethod(expectedData)).to.be.eq(1); + }); + + it("Should not allow to execute transaction after removing address from whitelist ", async () => { + const { plugin, account, manager } = await setup(); + const safeAddress = await account.getAddress(); + + // Required for isOwner(address) to return true + account.givenMethodReturnBool("0x2f54bf6e", true); + + const data = plugin.interface.encodeFunctionData("addToWhitelist", [user2.address]); + await account.executeCallViaMock(await plugin.getAddress(), 0, data, MaxUint256); + + const data2 = plugin.interface.encodeFunctionData("removeFromWhitelist", [user2.address]); + expect(await account.executeCallViaMock(await plugin.getAddress(), 0, data2, MaxUint256)) + .to.emit(plugin, "AddressRemovedFromWhitelist") + .withArgs(user1.address); + + const safeTx = buildSingleTx(user2.address, 0n, "0x", 0n, ZeroHash); + + await expect(plugin.connect(user1).executeFromPlugin(manager.target, safeAddress, safeTx)) + .to.be.revertedWithCustomError(plugin, "AddressNotWhiteListed") + .withArgs(user2.address); + + const mockInstance = await getInstance(hre, "MockContract", manager.target); + expect(await mockInstance.invocationCount()).to.be.eq(0); + }); +}); From 6c11b848ebfb0ce7691d18539a1734052c402287 Mon Sep 17 00:00:00 2001 From: kalrashivam Date: Sun, 10 Dec 2023 04:42:37 +0530 Subject: [PATCH 4/5] Add test cases for stoploss --- contracts/contracts/StoplossPlugin.sol | 25 +++++-- contracts/src/deploy/deploy_plugin.ts | 38 ++++++----- contracts/test/StoplossPlugin.spec.ts | 95 ++++---------------------- contracts/test/utils/contracts.ts | 2 + 4 files changed, 57 insertions(+), 103 deletions(-) diff --git a/contracts/contracts/StoplossPlugin.sol b/contracts/contracts/StoplossPlugin.sol index 2ca9034..f8859dc 100644 --- a/contracts/contracts/StoplossPlugin.sol +++ b/contracts/contracts/StoplossPlugin.sol @@ -18,7 +18,6 @@ contract StoplossPlugin is BasePluginWithEventMetadata { uint256 stopLossLimit; address tokenAddress; address contractAddress; - SafeTransaction safeSwapTx; } // safe account => stopLoss @@ -42,10 +41,27 @@ contract StoplossPlugin is BasePluginWithEventMetadata { ) {} + function addStopLoss(uint256 _stopLossLimit, address _tokenAddress, address _contractAddress) external { + Bots[msg.sender] = StopLoss(_stopLossLimit, _tokenAddress, _contractAddress); + emit AddStopLoss(msg.sender, _tokenAddress, _contractAddress, _stopLossLimit); + } + + /// @notice executes the transaction from the bot to swap or unstake, + /// checks if the bot is valid by checking the signature + /// @dev Can further be extened and add role access modifier by + /// zodiac (https://github.com/gnosis/zodiac-modifier-roles) + /// to check the functions that can be called from this on a given contract address + /// @param _safeAddress address of the safe + /// @param manager manager address + /// @param safe account + /// @param _hashedMessage hassed message to check the validity of the bot. + /// @param _safeSwapTx safe transaction to swap the token for a stable coin or + /// unstake the tokens from a platform. function executeFromPlugin( address _safeAddress, ISafeProtocolManager manager, ISafe safe, + SafeTransaction calldata _safeSwapTx, bytes32 _hashedMessage, bytes32 _r, bytes32 _s, @@ -54,7 +70,7 @@ contract StoplossPlugin is BasePluginWithEventMetadata { verifyMessage(_hashedMessage, _v, _r, _s); StopLoss memory stopLossBot = Bots[_safeAddress]; - try manager.executeTransaction(safe, stopLossBot.safeSwapTx) returns (bytes[] memory) { + try manager.executeTransaction(safe, _safeSwapTx) returns (bytes[] memory) { delete Bots[_safeAddress]; emit RemoveStopLoss(_safeAddress, stopLossBot.tokenAddress); } catch (bytes memory reason) { @@ -62,11 +78,6 @@ contract StoplossPlugin is BasePluginWithEventMetadata { } } - function addStopLoss(uint256 _stopLossLimit, address _tokenAddress, address _contractAddress, SafeTransaction calldata _safeSwapTx) external { - Bots[msg.sender] = StopLoss(_stopLossLimit, _tokenAddress, _contractAddress, _safeSwapTx); - emit AddStopLoss(msg.sender, _tokenAddress, _contractAddress, _stopLossLimit); - } - function verifyMessage(bytes32 _hashedMessage, uint8 _v, bytes32 _r, bytes32 _s) public pure returns (address) { bytes memory prefix = "\x19Ethereum Signed Message:\n32"; bytes32 prefixedHashMessage = keccak256(abi.encodePacked(prefix, _hashedMessage)); diff --git a/contracts/src/deploy/deploy_plugin.ts b/contracts/src/deploy/deploy_plugin.ts index e162085..8ebe12b 100644 --- a/contracts/src/deploy/deploy_plugin.ts +++ b/contracts/src/deploy/deploy_plugin.ts @@ -14,28 +14,34 @@ const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { // We don't use a trusted origin right now to make it easier to test. // For production networks it is strongly recommended to set one to avoid potential fee extraction. const trustedOrigin = ZeroAddress // hre.network.name === "hardhat" ? ZeroAddress : getGelatoAddress(hre.network.name) - await deploy("RelayPlugin", { - from: deployer, - args: [trustedOrigin, relayMethod], - log: true, - deterministicDeployment: true, - }); + // await deploy("RelayPlugin", { + // from: deployer, + // args: [trustedOrigin, relayMethod], + // log: true, + // deterministicDeployment: true, + // }); - await deploy("WhitelistPlugin", { - from: deployer, - args: [], - log: true, - deterministicDeployment: true, - }); + // await deploy("WhitelistPlugin", { + // from: deployer, + // args: [], + // log: true, + // deterministicDeployment: true, + // }); - await deploy("RecoveryWithDelayPlugin", { + // await deploy("RecoveryWithDelayPlugin", { + // from: deployer, + // args: [recoverer], + // log: true, + // deterministicDeployment: true, + // }); + + await deploy("StoplossPlugin", { from: deployer, - args: [recoverer], + args: [], log: true, deterministicDeployment: true, }); - }; deploy.tags = ["plugins"]; -export default deploy; \ No newline at end of file +export default deploy; diff --git a/contracts/test/StoplossPlugin.spec.ts b/contracts/test/StoplossPlugin.spec.ts index bd7046c..3e96956 100644 --- a/contracts/test/StoplossPlugin.spec.ts +++ b/contracts/test/StoplossPlugin.spec.ts @@ -1,7 +1,7 @@ import hre, { deployments, ethers } from "hardhat"; import { expect } from "chai"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { getWhiteListPlugin, getInstance } from "./utils/contracts"; +import { getStopLossPlugin, getInstance } from "./utils/contracts"; import { loadPluginMetadata } from "../src/utils/metadata"; import { buildSingleTx } from "../src/utils/builder"; import { ISafeProtocolManager__factory, MockContract } from "../typechain-types"; @@ -9,10 +9,10 @@ import { MaxUint256, ZeroHash } from "ethers"; import { getProtocolManagerAddress } from "../src/utils/protocol"; describe("StopLossPlugin", async () => { - let user1: SignerWithAddress, user2: SignerWithAddress; + // let user1: SignerWithAddress, user2: SignerWithAddress; before(async () => { - [user1, user2] = await hre.ethers.getSigners(); + // [user1, user2] = await hre.ethers.getSigners(); }); const setup = deployments.createFixture(async ({ deployments }) => { @@ -20,7 +20,7 @@ describe("StopLossPlugin", async () => { const manager = await ethers.getContractAt("MockContract", await getProtocolManagerAddress(hre)); const account = await (await ethers.getContractFactory("ExecutableMockContract")).deploy(); - const plugin = await getWhiteListPlugin(hre); + const plugin = await getStopLossPlugin(hre); return { account, plugin, @@ -30,7 +30,7 @@ describe("StopLossPlugin", async () => { it("should be initialized correctly", async () => { const { plugin } = await setup(); - expect(await plugin.name()).to.be.eq("Whitelist Plugin"); + expect(await plugin.name()).to.be.eq("Stoploss Plugin"); expect(await plugin.version()).to.be.eq("1.0.0"); expect(await plugin.requiresRootAccess()).to.be.false; }); @@ -38,7 +38,7 @@ describe("StopLossPlugin", async () => { it("can retrieve meta data for module", async () => { const { plugin } = await setup(); expect(await loadPluginMetadata(hre, plugin)).to.be.deep.eq({ - name: "Whitelist Plugin", + name: "Stoploss Plugin", version: "1.0.0", requiresRootAccess: false, iconUrl: "", @@ -46,88 +46,23 @@ describe("StopLossPlugin", async () => { }); }); - it("should emit AddressWhitelisted when account is whitelisted", async () => { + it("should emit AddStopLoss when stoploss is added", async () => { const { plugin, account } = await setup(); - const data = plugin.interface.encodeFunctionData("addToWhitelist", [user1.address]); + // const swapRouter2Uniswap = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; + const data = plugin.interface.encodeFunctionData("addStopLoss", [ethers.parseUnits("99", 6), "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"]); expect(await account.executeCallViaMock(await plugin.getAddress(), 0, data, MaxUint256)) - .to.emit(plugin, "AddressWhitelisted") - .withArgs(user1.address); + .to.emit(plugin, "AddStopLoss") }); - it("Should not allow calls to non-whitelist address", async () => { - const { plugin, account, manager } = await setup(); - - // Required for isOwner(address) to return true - account.givenMethodReturnBool("0x2f54bf6e", true); - - const safeTx = buildSingleTx(user2.address, 0n, "0x", 0n, hre.ethers.randomBytes(32)); - - await expect( - plugin.executeFromPlugin(await manager.getAddress(), await account.getAddress(), safeTx), - ).to.be.revertedWithCustomError(plugin, "AddressNotWhiteListed"); - }); - - it("Should not allow non-owner to execute transaction to whitelisted address", async () => { - const { plugin, account, manager } = await setup(); - const safeAddress = await account.getAddress(); - const data = plugin.interface.encodeFunctionData("addToWhitelist", [user2.address]); - await account.executeCallViaMock(await plugin.getAddress(), 0, data, MaxUint256); - - // Required for isOwner(address) to return false - account.givenMethodReturnBool("0x2f54bf6e", false); - - const safeTx = buildSingleTx(user2.address, 0n, "0x", 0n, ZeroHash); - await expect(plugin.connect(user1).executeFromPlugin(manager.target, safeAddress, safeTx)) - .to.be.revertedWithCustomError(plugin, "CallerIsNotOwner") - .withArgs(safeAddress, user1.address); - - const managerInterface = ISafeProtocolManager__factory.createInterface(); - const expectedData = managerInterface.encodeFunctionData("executeTransaction", [account.target, safeTx]); - - expect(await manager.invocationCount()).to.be.eq(0); - expect(await manager.invocationCountForMethod(expectedData)).to.be.eq(0); - }); - - it("Should allow to execute transaction to whitelisted address", async () => { + it("Should allow to execute transaction to for verified bot", async () => { const { plugin, account, manager } = await setup(); const safeAddress = await account.getAddress(); - const data = plugin.interface.encodeFunctionData("addToWhitelist", [user2.address]); + // AAVE ADDRESS = "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" + // UNISWAP ROUTER ADDRESS = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45" + const data = plugin.interface.encodeFunctionData("addStopLoss", [ethers.parseUnits("99", 6), "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"]); await account.executeCallViaMock(await plugin.getAddress(), 0, data, MaxUint256); // Required for isOwner(address) to return true account.givenMethodReturnBool("0x2f54bf6e", true); - - const safeTx = buildSingleTx(user2.address, 0n, "0x", 0n, ZeroHash); - expect(await plugin.connect(user1).executeFromPlugin(manager.target, safeAddress, safeTx)); - - const managerInterface = ISafeProtocolManager__factory.createInterface(); - const expectedData = managerInterface.encodeFunctionData("executeTransaction", [account.target, safeTx]); - - expect(await manager.invocationCount()).to.be.eq(1); - expect(await manager.invocationCountForMethod(expectedData)).to.be.eq(1); - }); - - it("Should not allow to execute transaction after removing address from whitelist ", async () => { - const { plugin, account, manager } = await setup(); - const safeAddress = await account.getAddress(); - - // Required for isOwner(address) to return true - account.givenMethodReturnBool("0x2f54bf6e", true); - - const data = plugin.interface.encodeFunctionData("addToWhitelist", [user2.address]); - await account.executeCallViaMock(await plugin.getAddress(), 0, data, MaxUint256); - - const data2 = plugin.interface.encodeFunctionData("removeFromWhitelist", [user2.address]); - expect(await account.executeCallViaMock(await plugin.getAddress(), 0, data2, MaxUint256)) - .to.emit(plugin, "AddressRemovedFromWhitelist") - .withArgs(user1.address); - - const safeTx = buildSingleTx(user2.address, 0n, "0x", 0n, ZeroHash); - - await expect(plugin.connect(user1).executeFromPlugin(manager.target, safeAddress, safeTx)) - .to.be.revertedWithCustomError(plugin, "AddressNotWhiteListed") - .withArgs(user2.address); - - const mockInstance = await getInstance(hre, "MockContract", manager.target); - expect(await mockInstance.invocationCount()).to.be.eq(0); + }); }); diff --git a/contracts/test/utils/contracts.ts b/contracts/test/utils/contracts.ts index 3efa608..36b0783 100644 --- a/contracts/test/utils/contracts.ts +++ b/contracts/test/utils/contracts.ts @@ -5,6 +5,7 @@ import { RelayPlugin, TestSafeProtocolRegistryUnrestricted, WhitelistPlugin, + StoplossPlugin } from "../../typechain-types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { getProtocolRegistryAddress } from "../../src/utils/protocol"; @@ -28,5 +29,6 @@ export const getRelayPlugin = (hre: HardhatRuntimeEnvironment) => getSingleton getInstance(hre, "TestSafeProtocolRegistryUnrestricted", await getProtocolRegistryAddress(hre)); export const getWhiteListPlugin = async (hre: HardhatRuntimeEnvironment) => getSingleton(hre, "WhitelistPlugin"); +export const getStopLossPlugin = async (hre: HardhatRuntimeEnvironment) => getSingleton(hre, "StoplossPlugin"); export const getRecoveryWithDelayPlugin = async (hre: HardhatRuntimeEnvironment) => getSingleton(hre, "RecoveryWithDelayPlugin"); From b37ad2652dfe4dacde9119c92ab360949f684115 Mon Sep 17 00:00:00 2001 From: kalrashivam Date: Sun, 10 Dec 2023 08:26:06 +0530 Subject: [PATCH 5/5] minor changes --- contracts/contracts/StoplossPlugin.sol | 13 +++++++------ contracts/test/StoplossPlugin.spec.ts | 3 ++- contracts/test/exampleBot.ts | 8 ++++++++ contracts/test/testBot.ts | 21 +++++++++++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 contracts/test/exampleBot.ts create mode 100644 contracts/test/testBot.ts diff --git a/contracts/contracts/StoplossPlugin.sol b/contracts/contracts/StoplossPlugin.sol index f8859dc..1c753a0 100644 --- a/contracts/contracts/StoplossPlugin.sol +++ b/contracts/contracts/StoplossPlugin.sol @@ -51,14 +51,12 @@ contract StoplossPlugin is BasePluginWithEventMetadata { /// @dev Can further be extened and add role access modifier by /// zodiac (https://github.com/gnosis/zodiac-modifier-roles) /// to check the functions that can be called from this on a given contract address - /// @param _safeAddress address of the safe /// @param manager manager address /// @param safe account /// @param _hashedMessage hassed message to check the validity of the bot. /// @param _safeSwapTx safe transaction to swap the token for a stable coin or /// unstake the tokens from a platform. function executeFromPlugin( - address _safeAddress, ISafeProtocolManager manager, ISafe safe, SafeTransaction calldata _safeSwapTx, @@ -67,12 +65,15 @@ contract StoplossPlugin is BasePluginWithEventMetadata { bytes32 _s, uint8 _v ) external { - verifyMessage(_hashedMessage, _v, _r, _s); - StopLoss memory stopLossBot = Bots[_safeAddress]; + address safeAddress = address(safe); + address signer = verifyMessage(_hashedMessage, _v, _r, _s); + require(signer == safeAddress, "ERROR_UNVERIFIED_BOT"); + + StopLoss memory stopLossBot = Bots[safeAddress]; try manager.executeTransaction(safe, _safeSwapTx) returns (bytes[] memory) { - delete Bots[_safeAddress]; - emit RemoveStopLoss(_safeAddress, stopLossBot.tokenAddress); + delete Bots[safeAddress]; + emit RemoveStopLoss(safeAddress, stopLossBot.tokenAddress); } catch (bytes memory reason) { revert SwapFailure(reason); } diff --git a/contracts/test/StoplossPlugin.spec.ts b/contracts/test/StoplossPlugin.spec.ts index 3e96956..43399e1 100644 --- a/contracts/test/StoplossPlugin.spec.ts +++ b/contracts/test/StoplossPlugin.spec.ts @@ -63,6 +63,7 @@ describe("StopLossPlugin", async () => { await account.executeCallViaMock(await plugin.getAddress(), 0, data, MaxUint256); // Required for isOwner(address) to return true account.givenMethodReturnBool("0x2f54bf6e", true); - + // TODO: test if a normal transaction works on safe. + }); }); diff --git a/contracts/test/exampleBot.ts b/contracts/test/exampleBot.ts new file mode 100644 index 0000000..f8e40f1 --- /dev/null +++ b/contracts/test/exampleBot.ts @@ -0,0 +1,8 @@ +import hre, { deployments, ethers } from "hardhat"; +import { expect } from "chai"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { getRelayPlugin } from "./utils/contracts"; +import { loadPluginMetadata } from "../src/utils/metadata"; +import { getProtocolManagerAddress } from "../src/utils/protocol"; +import { Interface, MaxUint256, ZeroAddress, ZeroHash, getAddress, keccak256 } from "ethers"; +import { ISafeProtocolManager__factory } from "../typechain-types"; \ No newline at end of file diff --git a/contracts/test/testBot.ts b/contracts/test/testBot.ts new file mode 100644 index 0000000..4d4e45b --- /dev/null +++ b/contracts/test/testBot.ts @@ -0,0 +1,21 @@ +import { Interface } from "@ethersproject/abi"; +import { Web3Function, Web3FunctionEventContext } from "@gelatonetwork/web3-functions-sdk"; + +const NFT_ABI = [ + "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)", +]; + +Web3Function.onRun(async (context: Web3FunctionEventContext) => { + // Get event log from Web3FunctionEventContext + const { log } = context; + + // Parse your event from ABI + const nft = new Interface(NFT_ABI); + const event = nft.parseLog(log); + + // Handle event data + const { from, to, tokenId } = event.args; + console.log(`Transfer of NFT #${tokenId} from ${from} to ${to} detected`); + + return { canExec: false, message: `Event processed ${log.transactionHash}` }; +}); \ No newline at end of file