diff --git a/.gitmodules b/.gitmodules index 690924b..24abb34 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,9 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/Vectorized/solady +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/README.md b/README.md index e05f3b3..b4d2ffb 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ [![Foundry][foundry-badge]][foundry] [![License: GPL-3.0][license-badge]][license] -[gha]: https://github.com/Kwenta/foundry-scaffold/actions -[gha-badge]: https://github.com/Kwenta/foundry-scaffold/actions/workflows/test.yml/badge.svg + +[gha]: https://github.com/Kwenta/KSX/actions +[gha-badge]: https://github.com/Kwenta/KSX/actions/workflows/test.yml/badge.svg [foundry]: https://getfoundry.sh/ [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg [license]: https://opensource.org/license/GPL-3.0/ diff --git a/foundry.toml b/foundry.toml index 7670d09..8a7c62a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,6 +10,11 @@ optimizer_runs = 1_000_000 [fmt] line_length = 80 number_underscore = "thousands" +multiline_func_header = "all" +sort_imports = true +contract_new_lines = true +override_spacing = false +wrap_comments = true [rpc_endpoints] mainnet = "${MAINNET_RPC_URL}" diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..723f8ca --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 diff --git a/lib/solady b/lib/solady new file mode 160000 index 0000000..678c916 --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 678c9163550810b08f0ffb09624c9f7532392303 diff --git a/package.json b/package.json index 7b10517..02e7f1b 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "coverage": "forge coverage --fork-url $(grep OPTIMISM_GOERLI_RPC_URL .env | cut -d '=' -f2)", "coverage:generate-lcov": "forge coverage --fork-url $(grep OPTIMISM_GOERLI_RPC_URL .env | cut -d '=' -f2) --report lcov", "analysis:slither": "slither .", - "gas-snapshot": "forge snapshot --fork-url $(grep OPTIMISM_GOERLI_RPC_URL .env | cut -d '=' -f2)", - "decode-custom-error": "npx @usecannon/cli decode synthetix-perps-market" + "gas-snapshot": "forge snapshot --fork-url $(grep OPTIMISM_GOERLI_RPC_URL .env | cut -d '=' -f2)" }, "repository": { "type": "git", diff --git a/remappings.txt b/remappings.txt index eede3c1..5e817ed 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1 +1,2 @@ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ \ No newline at end of file +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ \ No newline at end of file diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index f8e746c..09fdffa 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,81 +1,80 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; -// TODO: adapt deploy script to deploy the KSXVault contract -/* -import {BaseGoerliParameters} from - "script/utils/parameters/BaseGoerliParameters.sol"; -import {BaseParameters} from "script/utils/parameters/BaseParameters.sol"; +// proxy +import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC1967Proxy as Proxy} from + "lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +// contracts +import {KSXVault} from "src/KSXVault.sol"; + +// parameters import {OptimismGoerliParameters} from "script/utils/parameters/OptimismGoerliParameters.sol"; import {OptimismParameters} from "script/utils/parameters/OptimismParameters.sol"; + +// forge utils import {Script} from "lib/forge-std/src/Script.sol"; -import {Counter} from "src/Counter.sol"; -/// @title Kwenta deployment script +/// @title Kwenta KSX deployment script /// @author Flocqst (florian@kwenta.io) contract Setup is Script { - function deploySystem() public returns (address) { - Counter counter = new Counter(); - return address(counter); - } -} -/// @dev steps to deploy and verify on Base: -/// (1) load the variables in the .env file via `source .env` -/// (2) run `forge script script/Deploy.s.sol:DeployBase --rpc-url $BASE_RPC_URL --etherscan-api-key $BASESCAN_API_KEY --broadcast --verify -vvvv` -contract DeployBase is Setup, BaseParameters { - function run() public { - uint256 privateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(privateKey); - - Setup.deploySystem(); - - vm.stopBroadcast(); + function deploySystem( + address token, + address pDAO + ) + public + returns (KSXVault ksxVault) + { + // Deploy KSX Vault Implementation + address ksxVaultImplementation = address(new KSXVault(pDAO)); + ksxVault = KSXVault( + address( + new Proxy( + ksxVaultImplementation, + abi.encodeWithSignature("initialize(address)", token) + ) + ) + ); } -} -/// @dev steps to deploy and verify on Base Goerli: -/// (1) load the variables in the .env file via `source .env` -/// (2) run `forge script script/Deploy.s.sol:DeployBaseGoerli --rpc-url $BASE_GOERLI_RPC_URL --etherscan-api-key $BASESCAN_API_KEY --broadcast --verify -vvvv` -contract DeployBaseGoerli is Setup, BaseGoerliParameters { - function run() public { - uint256 privateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(privateKey); - - Setup.deploySystem(); - - vm.stopBroadcast(); - } } /// @dev steps to deploy and verify on Optimism: /// (1) load the variables in the .env file via `source .env` -/// (2) run `forge script script/Deploy.s.sol:DeployOptimism --rpc-url $OPTIMISM_RPC_URL --etherscan-api-key $OPTIMISM_ETHERSCAN_API_KEY --broadcast --verify -vvvv` +/// (2) run `forge script script/Deploy.s.sol:DeployOptimism --rpc-url +/// $OPTIMISM_RPC_URL --etherscan-api-key $OPTIMISM_ETHERSCAN_API_KEY +/// --broadcast --verify -vvvv` contract DeployOptimism is Setup, OptimismParameters { + function run() public { uint256 privateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(privateKey); - Setup.deploySystem(); + Setup.deploySystem({token: KWENTA, pDAO: PDAO}); vm.stopBroadcast(); } + } /// @dev steps to deploy and verify on Optimism Goerli: /// (1) load the variables in the .env file via `source .env` -/// (2) run `forge script script/Deploy.s.sol:DeployOptimismGoerli --rpc-url $OPTIMISM_GOERLI_RPC_URL --etherscan-api-key $OPTIMISM_ETHERSCAN_API_KEY --broadcast --verify -vvvv` - +/// (2) run `forge script script/Deploy.s.sol:DeployOptimismGoerli --rpc-url +/// $OPTIMISM_GOERLI_RPC_URL --etherscan-api-key $OPTIMISM_ETHERSCAN_API_KEY +/// --broadcast --verify -vvvv` contract DeployOptimismGoerli is Setup, OptimismGoerliParameters { + function run() public { uint256 privateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(privateKey); - Setup.deploySystem(); + Setup.deploySystem({token: KWENTA, pDAO: PDAO}); vm.stopBroadcast(); } + } -*/ diff --git a/script/utils/parameters/BaseGoerliParameters.sol b/script/utils/parameters/BaseGoerliParameters.sol deleted file mode 100644 index fb96ba3..0000000 --- a/script/utils/parameters/BaseGoerliParameters.sol +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.25; - -contract BaseGoerliParameters {} diff --git a/script/utils/parameters/BaseParameters.sol b/script/utils/parameters/BaseParameters.sol deleted file mode 100644 index 2e1f179..0000000 --- a/script/utils/parameters/BaseParameters.sol +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.25; - -contract BaseParameters {} diff --git a/script/utils/parameters/OptimismGoerliParameters.sol b/script/utils/parameters/OptimismGoerliParameters.sol index 04cecd1..02e381d 100644 --- a/script/utils/parameters/OptimismGoerliParameters.sol +++ b/script/utils/parameters/OptimismGoerliParameters.sol @@ -1,4 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; -contract OptimismGoerliParameters {} +contract OptimismGoerliParameters { + + /// @dev this is an EOA used on testnet only + address public constant PDAO = 0x1b4fCFE451A15218aEeC811B508B4aa3f2A35904; + + // https://developers.circle.com/stablecoins/docs/usdc-on-test-networks#usdc-on-op-goerli + address public constant USDC = 0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6; + + address public constant KWENTA = 0x920Cf626a271321C151D027030D5d08aF699456b; + +} diff --git a/script/utils/parameters/OptimismParameters.sol b/script/utils/parameters/OptimismParameters.sol index 7f54df5..341440e 100644 --- a/script/utils/parameters/OptimismParameters.sol +++ b/script/utils/parameters/OptimismParameters.sol @@ -1,4 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; -contract OptimismParameters {} +contract OptimismParameters { + + address public constant PDAO = 0xe826d43961a87fBE71C91d9B73F7ef9b16721C07; + + // https://optimistic.etherscan.io/token/0x0b2c639c533813f4aa9d7837caf62653d097ff85 + address public constant USDC = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; + + // https://optimistic.etherscan.io/token/0x920cf626a271321c151d027030d5d08af699456b + address public constant KWENTA = 0x920Cf626a271321C151D027030D5d08aF699456b; + +} diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 4993b24..ca97d58 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -1,16 +1,74 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.25; +import {ERC4626Upgradeable} from + "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; -import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20Metadata} from + "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import {IKSXVault} from "src/interfaces/IKSXVault.sol"; + +import {UUPSUpgradeable} from + "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -/// @title Kwenta Example Contract +/// @title KSXVault Contract /// @notice KSX ERC4626 Vault /// @author Flocqst (florian@kwenta.io) -contract KSXVault is ERC4626 { - constructor(address _token) - ERC4626(IERC20(_token)) - ERC20("KSX Vault", "KSX") - {} +contract KSXVault is IKSXVault, ERC4626Upgradeable, UUPSUpgradeable { + + /*////////////////////////////////////////////////////////////// + IMMUTABLES + //////////////////////////////////////////////////////////////*/ + + /// @notice Kwenta owned/operated multisig address that + /// can authorize upgrades + /// @dev if this address is the zero address, then the + /// KSX vault will no longer be upgradeable + /// @dev making immutable because the pDAO address + /// will *never* change + address internal immutable pDAO; + + /*/////////////////////////////////////////////////////////////// + CONSTRUCTOR / INITIALIZER + ///////////////////////////////////////////////////////////////*/ + + /// @dev disable default constructor to disable the implementation contract + /// Actual contract construction will take place in the initialize function + /// via proxy + /// @custom:oz-upgrades-unsafe-allow constructor + /// @param _pDAO Kwenta owned/operated multisig address that can authorize + /// upgrades + constructor(address _pDAO) { + _disableInitializers(); + + /// @dev pDAO address can be the zero address to + /// make the KSX vault non-upgradeable + pDAO = _pDAO; + } + + /// @notice Initializes the contract + /// @param _token The address for the KWENTA ERC20 token + function initialize(address _token) external initializer { + __ERC20_init("KSX Vault", "KSX"); + __ERC4626_init(IERC20(_token)); + __UUPSUpgradeable_init(); + } + + /*////////////////////////////////////////////////////////////// + UPGRADE MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc UUPSUpgradeable + function _authorizeUpgrade(address /* _newImplementation */ ) + internal + view + override + { + if (pDAO == address(0)) revert NonUpgradeable(); + if (msg.sender != pDAO) revert OnlyPDAO(); + } + } diff --git a/src/interfaces/IKSXVault.sol b/src/interfaces/IKSXVault.sol new file mode 100644 index 0000000..8855b56 --- /dev/null +++ b/src/interfaces/IKSXVault.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; + +/// @title Kwenta KSXVault Interface +/// @author Flocqst (florian@kwenta.io) +interface IKSXVault { + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice thrown when attempting to update + /// the KSXVault when caller is not the Kwenta pDAO + error OnlyPDAO(); + + /// @notice thrown when attempting to upgrade + /// the KSXVault when the KSXVault is not upgradeable + /// @dev the KSXVault is not upgradeable when + /// the pDAO has been set to the zero address + error NonUpgradeable(); + +} diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index a980305..f91b036 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -1,20 +1,38 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.25; -import {Test} from "forge-std/Test.sol"; -import {KSXVault} from "../src/KSXVault.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Test} from "forge-std/Test.sol"; +import {Bootstrap, KSXVault} from "test/utils/Bootstrap.sol"; + +contract KSXVaultTest is Bootstrap { + + function setUp() public { + MockERC20 depositToken = new MockERC20("Deposit Token", "DT"); + initializeLocal(address(depositToken), PDAOADDR); + } + + function test_share_name() public { + assertEq(ksxVault.name(), "KSX Vault"); + } + + function test_share_symbol() public { + assertEq(ksxVault.symbol(), "KSX"); + } -contract KSXVaultTest is Test { - function setUp() public {} } contract MockERC20 is ERC20 { - constructor(string memory name_, string memory symbol_) + + constructor( + string memory name_, + string memory symbol_ + ) ERC20(name_, symbol_) {} function mint(address to, uint256 amount) external { _mint(to, amount); } + } diff --git a/test/Upgrade.t.sol b/test/Upgrade.t.sol new file mode 100644 index 0000000..a683ae6 --- /dev/null +++ b/test/Upgrade.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {Initializable} from + "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IKSXVault} from "src/interfaces/IKSXVault.sol"; +import {Bootstrap, KSXVault} from "test/utils/Bootstrap.sol"; +import {MockVaultUpgrade} from "test/utils/mocks/MockVaultUpgrade.sol"; + +contract UpgradeTest is Bootstrap { + + address public _pDAO = 0xe826d43961a87fBE71C91d9B73F7ef9b16721C07; + address public _token = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; + + function setUp() public { + initializeLocal(_token, _pDAO); + } + +} + +contract MockUpgrade is UpgradeTest { + + MockVaultUpgrade mockVaultUpgrade; + + function deployMockVaultImplementation() internal { + mockVaultUpgrade = new MockVaultUpgrade(address(pDAO)); + } + + function test_upgrade(string memory message) public { + bool success; + bytes memory response; + + (success,) = address(ksxVault).call( + abi.encodeWithSelector(MockVaultUpgrade.echo.selector, message) + ); + assert(!success); + + deployMockVaultImplementation(); + + vm.prank(pDAO); + + ksxVault.upgradeToAndCall(address(mockVaultUpgrade), ""); + + (success, response) = address(ksxVault).call( + abi.encodeWithSelector(mockVaultUpgrade.echo.selector, message) + ); + assert(success); + assertEq(abi.decode(response, (string)), message); + } + + function test_upgrade_only_pDAO() public { + deployMockVaultImplementation(); + + vm.prank(BAD_ACTOR); + + vm.expectRevert(abi.encodeWithSelector(IKSXVault.OnlyPDAO.selector)); + + ksxVault.upgradeToAndCall(address(mockVaultUpgrade), ""); + } + + function test_removeUpgradability() public { + mockVaultUpgrade = new MockVaultUpgrade( + address(0) // set pDAO to zero address to effectively remove + // upgradability + ); + + vm.prank(pDAO); + + ksxVault.upgradeToAndCall(address(mockVaultUpgrade), ""); + + vm.prank(pDAO); + + vm.expectRevert( + abi.encodeWithSelector(IKSXVault.NonUpgradeable.selector) + ); + + ksxVault.upgradeToAndCall(address(mockVaultUpgrade), ""); + } + + function test_initializerDisabledAfterDeployment() public { + // Vault is already initialized in the setUp() + + // Try to initialize again + vm.expectRevert( + abi.encodeWithSelector(Initializable.InvalidInitialization.selector) + ); + + ksxVault.initialize(address(0x00)); + + // Verify that the token address did not change + assertEq(address(ksxVault.asset()), _token); + } + +} diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index 9d264d1..5a4e891 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -1,69 +1,76 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; -// TODO: adapt deploy script to deploy the KSXVault contract -/* +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Test} from "lib/forge-std/src/Test.sol"; import {console2} from "lib/forge-std/src/console2.sol"; import { - Counter, OptimismGoerliParameters, OptimismParameters, Setup } from "script/Deploy.s.sol"; -import {Test} from "lib/forge-std/src/Test.sol"; +import {KSXVault} from "src/KSXVault.sol"; +import {Constants} from "test/utils/Constants.sol"; + +contract Bootstrap is Test, Constants { -contract Bootstrap is Test { using console2 for *; - Counter internal counter; + // pDAO address + address public pDAO; - function initializeLocal() internal { - BootstrapLocal bootstrap = new BootstrapLocal(); - (address counterAddress) = bootstrap.init(); + // deployed contracts + KSXVault internal ksxVault; - counter = Counter(counterAddress); - } + IERC20 public TOKEN; - function initializeOptimismGoerli() internal { - BootstrapOptimismGoerli bootstrap = new BootstrapOptimismGoerli(); - (address counterAddress) = bootstrap.init(); + function initializeLocal(address _token, address _pDAO) internal { + BootstrapLocal bootstrap = new BootstrapLocal(); + (address ksxVaultAddress) = bootstrap.init(_token, _pDAO); - counter = Counter(counterAddress); + pDAO = _pDAO; + TOKEN = IERC20(_token); + ksxVault = KSXVault(ksxVaultAddress); } function initializeOptimism() internal { - BootstrapOptimismGoerli bootstrap = new BootstrapOptimismGoerli(); - (address counterAddress) = bootstrap.init(); + BootstrapOptimism bootstrap = new BootstrapOptimism(); + (address ksxVaultAddress, address _TokenAddress, address _pDAOAddress) = + bootstrap.init(); - counter = Counter(counterAddress); + pDAO = _pDAOAddress; + TOKEN = IERC20(_TokenAddress); + ksxVault = KSXVault(ksxVaultAddress); } - /// @dev add other networks here as needed (ex: Base, BaseGoerli) } contract BootstrapLocal is Setup { - function init() public returns (address) { - address counterAddress = Setup.deploySystem(); - return counterAddress; + function init(address _token, address _pDAO) public returns (address) { + (KSXVault ksxvault) = Setup.deploySystem({token: _token, pDAO: _pDAO}); + + return (address(ksxvault)); } + } contract BootstrapOptimism is Setup, OptimismParameters { - function init() public returns (address) { - address counterAddress = Setup.deploySystem(); - return counterAddress; + function init() public returns (address, address, address) { + (KSXVault ksxvault) = Setup.deploySystem({token: KWENTA, pDAO: PDAO}); + + return (address(ksxvault), KWENTA, PDAO); } + } contract BootstrapOptimismGoerli is Setup, OptimismGoerliParameters { - function init() public returns (address) { - address counterAddress = Setup.deploySystem(); - return counterAddress; + function init() public returns (address, address, address) { + (KSXVault ksxvault) = Setup.deploySystem({token: KWENTA, pDAO: PDAO}); + + return (address(ksxvault), KWENTA, PDAO); } -} -// add other networks here as needed (ex: Base, BaseGoerli) -*/ +} diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol new file mode 100644 index 0000000..443a52a --- /dev/null +++ b/test/utils/Constants.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +/// @title Contract for defining constants used in testing +contract Constants { + + uint256 public constant BASE_BLOCK_NUMBER = 8_225_680; + + address internal constant ACTOR = address(0xa1); + + address internal constant BAD_ACTOR = address(0xa2); + + address public constant PDAOADDR = + 0xe826d43961a87fBE71C91d9B73F7ef9b16721C07; + +} diff --git a/test/utils/mocks/MockVaultUpgrade.sol b/test/utils/mocks/MockVaultUpgrade.sol new file mode 100644 index 0000000..ee91299 --- /dev/null +++ b/test/utils/mocks/MockVaultUpgrade.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {KSXVault} from "src/KSXVault.sol"; + +/// @title Example upgraded Vault contract for testing purposes +/// @author Flocqst (florian@kwenta.io) +contract MockVaultUpgrade is KSXVault { + + constructor(address _pDAO) KSXVault(_pDAO) {} + + function echo(string memory message) public pure returns (string memory) { + return message; + } + +}