diff --git a/.solhint.json b/.solhint.json index 94f58b2..fd31b8b 100644 --- a/.solhint.json +++ b/.solhint.json @@ -3,7 +3,7 @@ "rules": { "compiler-version": ["off"], "constructor-syntax": "warn", - "quotes": ["error", "single"], + "quotes": ["error", "double"], "func-visibility": ["warn", { "ignoreConstructors": true }], "not-rely-on-time": "off", "no-inline-assembly": "off", diff --git a/.solhint.tests.json b/.solhint.tests.json index fb26b4e..3b71630 100644 --- a/.solhint.tests.json +++ b/.solhint.tests.json @@ -3,7 +3,7 @@ "rules": { "compiler-version": ["off"], "constructor-syntax": "warn", - "quotes": ["error", "single"], + "quotes": ["error", "double"], "func-visibility": ["warn", { "ignoreConstructors": true }], "not-rely-on-time": "off", "style-guide-casing": "off", diff --git a/foundry.toml b/foundry.toml index b9d9beb..e0b9146 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,13 +3,13 @@ line_length = 120 tab_width = 2 bracket_spacing = false int_types = 'long' -quote_style = 'single' +quote_style = 'double' number_underscore = 'thousands' multiline_func_header = 'params_first' sort_imports = true [profile.default] -solc_version = '0.8.23' +solc_version = '0.8.26' libs = ['node_modules', 'lib'] optimizer_runs = 10_000 diff --git a/package.json b/package.json index d3f7e2a..583126e 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,10 @@ "package.json": "sort-package-json" }, "dependencies": { - "@aave/core-v3": "^1.19.3" + "solmate": "^6.2.0" }, "devDependencies": { + "@aave/core-v3": "^1.19.3", "@commitlint/cli": "19.3.0", "@commitlint/config-conventional": "19.2.2", "@defi-wonderland/natspec-smells": "1.1.1", @@ -47,6 +48,7 @@ "husky": ">=8", "lint-staged": ">=10", "solhint-community": "4.0.0", - "sort-package-json": "2.10.0" + "sort-package-json": "2.10.0", + "yield-daddy": "timeless-fi/yield-daddy" } } diff --git a/remappings.txt b/remappings.txt index 572e6a8..6583dcd 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,6 +2,8 @@ forge-std/=node_modules/forge-std/src halmos-cheatcodes=node_modules/halmos-cheatcodes openzeppelin-contracts/=node_modules/@openzeppelin/contracts aave/core-v3/=node_modules/@aave/core-v3/contracts +yield-daddy/=node_modules/yield-daddy/src +solmate/=node_modules/solmate/src contracts/=src/contracts interfaces/=src/interfaces diff --git a/script/Deploy.sol b/script/Deploy.sol index f77d371..39c7e2f 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; -import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; -import {Grateful} from 'contracts/Grateful.sol'; -import {Script} from 'forge-std/Script.sol'; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {Grateful} from "contracts/Grateful.sol"; +import {Script} from "forge-std/Script.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IPool} from "yield-daddy/aave-v3/AaveV3ERC4626.sol"; contract Deploy is Script { struct DeploymentParams { diff --git a/src/contracts/Grateful.sol b/src/contracts/Grateful.sol index b7f23a5..26ed6f9 100644 --- a/src/contracts/Grateful.sol +++ b/src/contracts/Grateful.sol @@ -1,21 +1,25 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; -import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; -import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; -import {Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; -import {IGrateful} from 'interfaces/IGrateful.sol'; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IGrateful} from "interfaces/IGrateful.sol"; +import {AaveV3ERC4626, IPool, IRewardsController} from "yield-daddy/aave-v3/AaveV3ERC4626.sol"; contract Grateful is IGrateful, Ownable2Step { - // @inheritdoc IGrateful IPool public aavePool; - - // inheritdoc IGrateful mapping(address => bool) public tokensWhitelisted; - - // @inheritdoc IGrateful mapping(address => bool) public yieldingFunds; + mapping(address => AaveV3ERC4626) public vaults; + mapping(address => mapping(address => uint256)) public shares; + + modifier onlyWhenTokenWhitelisted(address _token) { + if (!tokensWhitelisted[_token]) { + revert Grateful_TokenNotWhitelisted(); + } + _; + } constructor(address[] memory _tokens, IPool _aavePool) Ownable(msg.sender) { aavePool = _aavePool; @@ -26,14 +30,20 @@ contract Grateful is IGrateful, Ownable2Step { } // @inheritdoc IGrateful - function pay(address _merchant, address _token, uint256 _amount) external { - if (!tokensWhitelisted[_token]) { - revert Grateful_TokenNotWhitelisted(); - } + function addVault(address _token, address _vault) external onlyWhenTokenWhitelisted(_token) onlyOwner { + vaults[_token] = AaveV3ERC4626(_vault); + } + // @inheritdoc IGrateful + function pay(address _merchant, address _token, uint256 _amount) external onlyWhenTokenWhitelisted(_token) { if (yieldingFunds[_merchant]) { + AaveV3ERC4626 vault = vaults[_token]; + if (address(vault) == address(0)) { + revert Grateful_VaultNotSet(); + } IERC20(_token).transferFrom(msg.sender, address(this), _amount); - aavePool.supply(_token, _amount, address(this), 0); + uint256 _shares = vault.deposit(_amount, address(this)); + shares[_merchant][_token] += _shares; } else { if (!IERC20(_token).transferFrom(msg.sender, _merchant, _amount)) { revert Grateful_TransferFailed(); diff --git a/src/interfaces/IGrateful.sol b/src/interfaces/IGrateful.sol index b6dfafb..43d5a74 100644 --- a/src/interfaces/IGrateful.sol +++ b/src/interfaces/IGrateful.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; -import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {AaveV3ERC4626, IPool} from "yield-daddy/aave-v3/AaveV3ERC4626.sol"; /** - * @title Grateful Contract - * @author Chin - * @notice Contract for allowing payments in whitelisted tokens. Payments can be done using Uniswap's Permit2. Merchants can choose to yield their payments in AAVE and withdraw them at any time. Recurring payments are enabled by using Chainlink Keepers + * @title Grateful Contract Interface + * @notice Interface for the Grateful contract that allows payments in whitelisted tokens with optional yield via AAVE. */ interface IGrateful { /*/////////////////////////////////////////////////////////////// @@ -17,6 +16,7 @@ interface IGrateful { /*/////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ + /** * @notice Throws if the token is not whitelisted */ @@ -27,9 +27,20 @@ interface IGrateful { */ error Grateful_TransferFailed(); + /** + * @notice Throws if the vault for a token is not set + */ + error Grateful_VaultNotSet(); + + /** + * @notice Throws if the token is not whitelisted when adding a vault + */ + error Grateful_VaultTokenNotWhitelisted(); + /*/////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////*/ + /** * @notice Aave pool for yielding merchants funds * @return _aavePool Aave pool @@ -48,11 +59,27 @@ interface IGrateful { */ function yieldingFunds(address _merchant) external view returns (bool _isYieldingFunds); + /** + * @notice Returns the vault associated with a token + * @return _vault Address of the vault contract + */ + function vaults(address _token) external view returns (AaveV3ERC4626 _vault); + + /** + * @notice Returns the amount of shares for a merchant + * @return _shares Amount of shares + */ + function shares(address _merchant, address _token) external view returns (uint256 _shares); + /*/////////////////////////////////////////////////////////////// LOGIC //////////////////////////////////////////////////////////////*/ + /** - * @notice Makes a payment to merchant + * @notice Makes a payment to a merchant + * @param _merchant Address of the merchant receiving payment + * @param _token Address of the token being used for payment + * @param _amount Amount of the token to be paid */ function pay(address _merchant, address _token, uint256 _amount) external; @@ -60,4 +87,11 @@ interface IGrateful { * @notice Switch the preference of the merchant to yield funds or not */ function switchYieldingFunds() external; + + /** + * @notice Adds a vault for a specific token + * @param _token Address of the token for which the vault is being set + * @param _vault Address of the vault contract + */ + function addVault(address _token, address _vault) external; } diff --git a/test/integration/Grateful.t.sol b/test/integration/Grateful.t.sol index dbf262e..4cfe019 100644 --- a/test/integration/Grateful.t.sol +++ b/test/integration/Grateful.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; -import {console} from 'forge-std/console.sol'; -import {IntegrationBase} from 'test/integration/IntegrationBase.sol'; +import {console} from "forge-std/console.sol"; +import {IntegrationBase} from "test/integration/IntegrationBase.sol"; contract IntegrationGreeter is IntegrationBase { function test_Payment() public { diff --git a/test/integration/IntegrationBase.sol b/test/integration/IntegrationBase.sol index 2dab78e..048cdb6 100644 --- a/test/integration/IntegrationBase.sol +++ b/test/integration/IntegrationBase.sol @@ -1,26 +1,36 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; -import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; -import {Grateful, IGrateful} from 'contracts/Grateful.sol'; -import {Test} from 'forge-std/Test.sol'; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {Grateful, IGrateful} from "contracts/Grateful.sol"; +import {Test} from "forge-std/Test.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {AaveV3ERC4626, IPool, IRewardsController} from "yield-daddy/aave-v3/AaveV3ERC4626.sol"; contract IntegrationBase is Test { uint256 internal constant _FORK_BLOCK = 18_920_905; - string internal _initialGreeting = 'hola'; - address internal _user = makeAddr('user'); - address internal _merchant = makeAddr('merchant'); - address internal _owner = makeAddr('owner'); + string internal _initialGreeting = "hola"; + address internal _user = makeAddr("user"); + address internal _merchant = makeAddr("merchant"); + address internal _owner = makeAddr("owner"); address internal _daiWhale = 0xbf702ea18BB1AB2A710394993a576eC61476cCf3; address[] internal _tokens; IERC20 internal _dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + address _aDai = 0x018008bfb33d285247A21d44E50697654f754e63; IPool internal _aavePool = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2); IGrateful internal _grateful; + AaveV3ERC4626 internal _vault = new AaveV3ERC4626( + ERC20(address(_dai)), + ERC20(_aDai), + _aavePool, + address(0), + IRewardsController(address(_dai)) // TODO: put real rewards controller + ); function setUp() public { - vm.createSelectFork(vm.rpcUrl('mainnet'), _FORK_BLOCK); + vm.createSelectFork(vm.rpcUrl("mainnet"), _FORK_BLOCK); vm.prank(_owner); _tokens = new address[](1); _tokens[0] = address(_dai); diff --git a/yarn.lock b/yarn.lock index c8604b7..aa5bbaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1711,6 +1711,11 @@ solhint-community@4.0.0: optionalDependencies: prettier "^2.8.3" +solmate@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/solmate/-/solmate-6.2.0.tgz#edd29b5f3d6faafafdcf65fe4d1d959b4841cfa8" + integrity sha512-AM38ioQ2P8zRsA42zenb9or6OybRjOLXIu3lhIT8rhddUuduCt76pUEuLxOIg9GByGojGz+EbpFdCB6B+QZVVA== + sort-object-keys@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" @@ -1990,6 +1995,10 @@ yargs@17.7.2, yargs@^17.0.0: y18n "^5.0.5" yargs-parser "^21.1.1" +yield-daddy@timeless-fi/yield-daddy: + version "0.0.0" + resolved "https://codeload.github.com/timeless-fi/yield-daddy/tar.gz/88753dfb04a0951df679c4199a865fe29646d285" + yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"