From fe2ecf437edb0ad3a065cfb6bcf3fcd1ee6802dc Mon Sep 17 00:00:00 2001 From: 0xchin Date: Thu, 10 Oct 2024 10:29:55 -0300 Subject: [PATCH] feat: enable one time payments with single address --- src/contracts/Grateful.sol | 52 ++++++++++++++++++++-------- src/contracts/OneTime.sol | 12 +++++-- src/interfaces/IGrateful.sol | 31 ++++++++++------- test/integration/Grateful.t.sol | 20 +++++------ test/integration/IntegrationBase.sol | 4 ++- 5 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/contracts/Grateful.sol b/src/contracts/Grateful.sol index 55d3d0a..1006289 100644 --- a/src/contracts/Grateful.sol +++ b/src/contracts/Grateful.sol @@ -5,6 +5,8 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {OneTime} from "contracts/OneTime.sol"; + +import {console} from "forge-std/console.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {IGrateful} from "interfaces/IGrateful.sol"; @@ -61,6 +63,17 @@ contract Grateful is IGrateful, Ownable2Step { _; } + modifier onlyWhenTokensWhitelisted( + address[] memory _tokens + ) { + for (uint256 i = 0; i < _tokens.length; i++) { + if (!tokensWhitelisted[_tokens[i]]) { + revert Grateful_TokenNotWhitelisted(); + } + } + _; + } + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -276,19 +289,19 @@ contract Grateful is IGrateful, Ownable2Step { /// @inheritdoc IGrateful function createOneTimePayment( address _merchant, - address _token, + address[] memory _tokens, uint256 _amount, uint256 _salt, uint256 _paymentId, address precomputed, address[] calldata _recipients, uint256[] calldata _percentages - ) external onlyWhenTokenWhitelisted(_token) returns (OneTime oneTime) { + ) external onlyWhenTokensWhitelisted(_tokens) returns (OneTime oneTime) { oneTimePayments[precomputed] = true; oneTime = new OneTime{salt: bytes32(_salt)}( - IGrateful(address(this)), IERC20(_token), _merchant, _amount, _paymentId, _recipients, _percentages + IGrateful(address(this)), _tokens, _merchant, _amount, _paymentId, _recipients, _percentages ); - emit OneTimePaymentCreated(_merchant, _token, _amount); + emit OneTimePaymentCreated(_merchant, _tokens, _amount); } /// @inheritdoc IGrateful @@ -309,7 +322,7 @@ contract Grateful is IGrateful, Ownable2Step { /// @inheritdoc IGrateful function computeOneTimeAddress( address _merchant, - address _token, + address[] memory _tokens, uint256 _amount, uint256 _salt, uint256 _paymentId, @@ -318,7 +331,7 @@ contract Grateful is IGrateful, Ownable2Step { ) external view returns (OneTime oneTime) { bytes memory bytecode = abi.encodePacked( type(OneTime).creationCode, - abi.encode(address(this), _token, _merchant, _amount, _paymentId, _recipients, _percentages) + abi.encode(address(this), _tokens, _merchant, _amount, _paymentId, _recipients, _percentages) ); bytes32 bytecodeHash = keccak256(bytecode); bytes32 addressHash = keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(_salt), bytecodeHash)); @@ -329,38 +342,49 @@ contract Grateful is IGrateful, Ownable2Step { /// @inheritdoc IGrateful function createOneTimePayment( address _merchant, - address _token, + address[] memory _tokens, uint256 _amount, uint256 _salt, uint256 _paymentId, address precomputed - ) external onlyWhenTokenWhitelisted(_token) returns (OneTime oneTime) { + ) external onlyWhenTokensWhitelisted(_tokens) returns (OneTime oneTime) { oneTimePayments[precomputed] = true; oneTime = new OneTime{salt: bytes32(_salt)}( - IGrateful(address(this)), IERC20(_token), _merchant, _amount, _paymentId, new address[](0), new uint256[](0) + IGrateful(address(this)), _tokens, _merchant, _amount, _paymentId, new address[](0), new uint256[](0) ); - emit OneTimePaymentCreated(_merchant, _token, _amount); + emit OneTimePaymentCreated(_merchant, _tokens, _amount); } /// @inheritdoc IGrateful - function receiveOneTimePayment(address _merchant, address _token, uint256 _paymentId, uint256 _amount) external { + function receiveOneTimePayment( + address _merchant, + IERC20[] memory _tokens, + uint256 _paymentId, + uint256 _amount + ) external { if (!oneTimePayments[msg.sender]) { revert Grateful_OneTimeNotFound(); } - _processPayment(msg.sender, _merchant, _token, _amount, _paymentId, 0, new address[](0), new uint256[](0)); + for (uint256 i = 0; i < _tokens.length; i++) { + if (_tokens[i].balanceOf(msg.sender) >= _amount) { + _processPayment( + msg.sender, _merchant, address(_tokens[i]), _amount, _paymentId, 0, new address[](0), new uint256[](0) + ); + } + } } /// @inheritdoc IGrateful function computeOneTimeAddress( address _merchant, - address _token, + address[] memory _tokens, uint256 _amount, uint256 _salt, uint256 _paymentId ) external view returns (OneTime oneTime) { bytes memory bytecode = abi.encodePacked( type(OneTime).creationCode, - abi.encode(address(this), _token, _merchant, _amount, _paymentId, new address[](0), new uint256[](0)) + abi.encode(address(this), _tokens, _merchant, _amount, _paymentId, new address[](0), new uint256[](0)) ); bytes32 bytecodeHash = keccak256(bytecode); bytes32 addressHash = keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(_salt), bytecodeHash)); diff --git a/src/contracts/OneTime.sol b/src/contracts/OneTime.sol index 8e1faa0..6c03831 100644 --- a/src/contracts/OneTime.sol +++ b/src/contracts/OneTime.sol @@ -1,20 +1,26 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {console} from "forge-std/console.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {IGrateful} from "interfaces/IGrateful.sol"; contract OneTime { constructor( IGrateful _grateful, - IERC20 _token, + address[] memory _tokens, address _merchant, uint256 _amount, uint256 _paymentId, address[] memory _recipients, uint256[] memory _percentages ) { - _token.approve(address(_grateful), _amount); - _grateful.receiveOneTimePayment(_merchant, address(_token), _paymentId, _amount, _recipients, _percentages); + for (uint256 i = 0; i < _tokens.length; i++) { + IERC20 token = IERC20(_tokens[i]); + if (token.balanceOf(address(this)) >= _amount) { + token.approve(address(_grateful), _amount); + _grateful.receiveOneTimePayment(_merchant, address(token), _paymentId, _amount, _recipients, _percentages); + } + } } } diff --git a/src/interfaces/IGrateful.sol b/src/interfaces/IGrateful.sol index a6bfc55..2fd8e13 100644 --- a/src/interfaces/IGrateful.sol +++ b/src/interfaces/IGrateful.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.26; import {OneTime} from "contracts/OneTime.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {AaveV3ERC4626, IPool} from "yield-daddy/aave-v3/AaveV3ERC4626.sol"; /** @@ -62,10 +64,10 @@ interface IGrateful { /** * @notice Emitted when a one-time payment is created. * @param merchant Address of the merchant. - * @param token Address of the token. + * @param tokens Address of the token. * @param amount Amount of the token. */ - event OneTimePaymentCreated(address indexed merchant, address indexed token, uint256 amount); + event OneTimePaymentCreated(address indexed merchant, address[] indexed tokens, uint256 amount); event SubscriptionCreated( uint256 indexed subscriptionId, @@ -264,7 +266,7 @@ interface IGrateful { /** * @notice Creates a one-time payment. * @param _merchant Address of the merchant. - * @param _token Address of the token. + * @param _tokens Address of the token. * @param _amount Amount of the token. * @param _salt Salt used for address computation. * @param _paymentId ID of the payment. @@ -273,7 +275,7 @@ interface IGrateful { */ function createOneTimePayment( address _merchant, - address _token, + address[] memory _tokens, uint256 _amount, uint256 _salt, uint256 _paymentId, @@ -285,7 +287,7 @@ interface IGrateful { /** * @notice Creates a one-time payment without recipients and percentages. * @param _merchant Address of the merchant. - * @param _token Address of the token. + * @param _tokens Address of the token. * @param _amount Amount of the token. * @param _salt Salt used for address computation. * @param _paymentId ID of the payment. @@ -294,7 +296,7 @@ interface IGrateful { */ function createOneTimePayment( address _merchant, - address _token, + address[] memory _tokens, uint256 _amount, uint256 _salt, uint256 _paymentId, @@ -322,16 +324,21 @@ interface IGrateful { /** * @notice Receives a one-time payment without recipients and percentages. * @param _merchant Address of the merchant. - * @param _token Address of the token. + * @param _tokens Address of the token. * @param _paymentId ID of the payment. * @param _amount Amount of the token. */ - function receiveOneTimePayment(address _merchant, address _token, uint256 _paymentId, uint256 _amount) external; + function receiveOneTimePayment( + address _merchant, + IERC20[] memory _tokens, + uint256 _paymentId, + uint256 _amount + ) external; /** * @notice Computes the address of a one-time payment contract. * @param _merchant Address of the merchant. - * @param _token Address of the token. + * @param _tokens Address of the token. * @param _amount Amount of the token. * @param _salt Salt used for address computation. * @param _paymentId ID of the payment. @@ -341,7 +348,7 @@ interface IGrateful { */ function computeOneTimeAddress( address _merchant, - address _token, + address[] memory _tokens, uint256 _amount, uint256 _salt, uint256 _paymentId, @@ -352,7 +359,7 @@ interface IGrateful { /** * @notice Computes the address of a one-time payment contract without recipients and percentages. * @param _merchant Address of the merchant. - * @param _token Address of the token. + * @param _tokens Address of the token. * @param _amount Amount of the token. * @param _salt Salt used for address computation. * @param _paymentId ID of the payment. @@ -360,7 +367,7 @@ interface IGrateful { */ function computeOneTimeAddress( address _merchant, - address _token, + address[] memory _tokens, uint256 _amount, uint256 _salt, uint256 _paymentId diff --git a/test/integration/Grateful.t.sol b/test/integration/Grateful.t.sol index 4179464..8426744 100644 --- a/test/integration/Grateful.t.sol +++ b/test/integration/Grateful.t.sol @@ -131,7 +131,7 @@ contract IntegrationGreeter is IntegrationBase { uint256 paymentId = _grateful.calculateId(_usdcWhale, _merchant, address(_usdc), _amount); // 2. Precompute address - address precomputed = address(_grateful.computeOneTimeAddress(_merchant, address(_usdc), _amount, 4, paymentId)); + address precomputed = address(_grateful.computeOneTimeAddress(_merchant, _tokens, _amount, 4, paymentId)); // 3. Once the payment address is precomputed, the client sends the payment vm.prank(_usdcWhale); @@ -139,18 +139,21 @@ contract IntegrationGreeter is IntegrationBase { // 4. Merchant calls api to make one time payment to his address vm.prank(_gratefulAutomation); - _grateful.createOneTimePayment(_merchant, address(_usdc), _amount, 4, paymentId, precomputed); + _grateful.createOneTimePayment(_merchant, _tokens, _amount, 4, paymentId, precomputed); // Merchant receives the payment assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_amount)); } function test_OneTimePaymentYieldingFunds() public { + address[] memory _tokens2 = new address[](1); + _tokens2[0] = _tokens[0]; + // 1. Calculate payment id uint256 paymentId = _grateful.calculateId(_usdcWhale, _merchant, address(_usdc), _amount); // 2. Precompute address - address precomputed = address(_grateful.computeOneTimeAddress(_merchant, address(_usdc), _amount, 4, paymentId)); + address precomputed = address(_grateful.computeOneTimeAddress(_merchant, _tokens, _amount, 4, paymentId)); // 3. Once the payment address is precomputed, the client sends the payment vm.prank(_usdcWhale); @@ -162,7 +165,7 @@ contract IntegrationGreeter is IntegrationBase { // 5. Grateful automation calls api to make one time payment to his address vm.prank(_gratefulAutomation); - _grateful.createOneTimePayment(_merchant, address(_usdc), _amount, 4, paymentId, precomputed); + _grateful.createOneTimePayment(_merchant, _tokens, _amount, 4, paymentId, precomputed); // 6. Advance time vm.warp(block.timestamp + 1 days); @@ -225,9 +228,8 @@ contract IntegrationGreeter is IntegrationBase { uint256 paymentId = _grateful.calculateId(_usdcWhale, _merchant, address(_usdc), _amount); // 3. Precompute address - address precomputed = address( - _grateful.computeOneTimeAddress(_merchant, address(_usdc), _amount, 4, paymentId, recipients, percentages) - ); + address precomputed = + address(_grateful.computeOneTimeAddress(_merchant, _tokens, _amount, 4, paymentId, recipients, percentages)); // 4. Once the payment address is precomputed, the client sends the payment vm.prank(_usdcWhale); @@ -235,9 +237,7 @@ contract IntegrationGreeter is IntegrationBase { // 5. Merchant calls api to make one time payment to his address vm.prank(_gratefulAutomation); - _grateful.createOneTimePayment( - _merchant, address(_usdc), _amount, 4, paymentId, precomputed, recipients, percentages - ); + _grateful.createOneTimePayment(_merchant, _tokens, _amount, 4, paymentId, precomputed, recipients, percentages); // 6. Calculate expected amounts after fee uint256 amountAfterFee = _grateful.applyFee(_amount); diff --git a/test/integration/IntegrationBase.sol b/test/integration/IntegrationBase.sol index f1977c8..3e74b6f 100644 --- a/test/integration/IntegrationBase.sol +++ b/test/integration/IntegrationBase.sol @@ -21,6 +21,7 @@ contract IntegrationBase is Test { address internal _usdcWhale = 0x555d73f2002A457211d690313f942B065eAD1FFF; address[] internal _tokens; IERC20 internal _usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IERC20 internal _usdt = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); address internal _aUsdc = 0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c; address internal _rewardsController = 0x8164Cc65827dcFe994AB23944CBC90e0aa80bFcb; IPool internal _aavePool = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2); @@ -34,8 +35,9 @@ contract IntegrationBase is Test { vm.startPrank(_owner); vm.createSelectFork(vm.rpcUrl("mainnet"), _FORK_BLOCK); vm.label(address(_vault), "Vault"); - _tokens = new address[](1); + _tokens = new address[](2); _tokens[0] = address(_usdc); + _tokens[1] = address(_usdt); _grateful = new Grateful(_tokens, _aavePool, _fee); _vault = new AaveV3Vault( ERC20(address(_usdc)),