diff --git a/src/contracts/Grateful.sol b/src/contracts/Grateful.sol index 8373cf4..4d17fa0 100644 --- a/src/contracts/Grateful.sol +++ b/src/contracts/Grateful.sol @@ -5,12 +5,15 @@ 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"; -import {AaveV3ERC4626, IPool, IRewardsController} from "yield-daddy/aave-v3/AaveV3ERC4626.sol"; + +import {Bytes32AddressLib} from "solmate/utils/Bytes32AddressLib.sol"; +import {AaveV3ERC4626, IPool} from "yield-daddy/aave-v3/AaveV3ERC4626.sol"; contract Grateful is IGrateful, Ownable2Step { + using Bytes32AddressLib for bytes32; + // @inheritdoc IGrateful IPool public aavePool; @@ -28,6 +31,8 @@ contract Grateful is IGrateful, Ownable2Step { mapping(uint256 => Subscription) public subscriptions; + mapping(address => bool) public oneTimePayments; + // @inheritdoc IGrateful uint256 public subscriptionCount; @@ -125,15 +130,51 @@ contract Grateful is IGrateful, Ownable2Step { subscription.paymentsAmount--; } + // @inheritdoc IGrateful function createOneTimePayment( address _merchant, address _token, - uint256 _amount - ) external onlyWhenTokenWhitelisted(_token) returns (address oneTimeAddress) { - oneTimeAddress = address(new OneTime(IERC20(_token), _merchant, _amount)); + uint256 _amount, + uint256 _salt, + uint256 _paymentId, + address precomputed + ) external onlyWhenTokenWhitelisted(_token) returns (OneTime oneTime) { + oneTimePayments[precomputed] = true; + oneTime = + new OneTime{salt: bytes32(_salt)}(IGrateful(address(this)), IERC20(_token), _merchant, _amount, _paymentId); emit OneTimePaymentCreated(_merchant, _token, _amount); } + function receiveOneTimePayment(address _merchant, address _token, uint256 _paymentId, uint256 _amount) external { + if (!oneTimePayments[msg.sender]) { + revert Grateful_OneTimeNotFound(); + } + _processPayment(msg.sender, _merchant, _token, _amount, _paymentId, 0); + } + + function computeOneTimeAddress( + address _merchant, + address _token, + uint256 _amount, + uint256 _salt, + uint256 _paymentId + ) external view returns (OneTime oneTime) { + return OneTime( + keccak256( + abi.encodePacked( + bytes1(0xFF), + address(this), + bytes32(_salt), + keccak256( + abi.encodePacked( + type(OneTime).creationCode, abi.encode(address(this), _token, _merchant, _amount, _paymentId) + ) + ) + ) + ).fromLast20Bytes() + ); + } + // @inheritdoc IGrateful function withdraw(address _token) external onlyWhenTokenWhitelisted(_token) { AaveV3ERC4626 vault = vaults[_token]; diff --git a/src/contracts/OneTime.sol b/src/contracts/OneTime.sol index a7efb96..6d2e231 100644 --- a/src/contracts/OneTime.sol +++ b/src/contracts/OneTime.sol @@ -1,19 +1,9 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IGrateful} from "interfaces/IGrateful.sol"; contract OneTime { - IERC20 public token; - address public merchant; - uint256 public amount; - bool public paid; - - constructor(IERC20 _token, address _merchant, uint256 _amount) { - token = _token; - merchant = _merchant; - amount = _amount; - } - - function processPayment() public { - paid = true; - token.transfer(merchant, amount); + constructor(IGrateful _grateful, IERC20 _token, address _merchant, uint256 _amount, uint256 _paymentId) { + _token.approve(address(_grateful), _amount); + _grateful.receiveOneTimePayment(_merchant, address(_token), _paymentId, _amount); } } diff --git a/src/interfaces/IGrateful.sol b/src/interfaces/IGrateful.sol index 35a7867..45c09f8 100644 --- a/src/interfaces/IGrateful.sol +++ b/src/interfaces/IGrateful.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT 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"; @@ -91,6 +92,11 @@ interface IGrateful { */ error Grateful_PaymentsAmountReached(); + /** + * @notice Throws if the one-time payment is not found + */ + error Grateful_OneTimeNotFound(); + /*/////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////*/ @@ -170,12 +176,36 @@ interface IGrateful { /// @param _merchant Address of the merchant /// @param _token Address of the token /// @param _amount Amount of the token - /// @return oneTimeAddress Address of the one-time payment + /// @return oneTime Contract of the one-time payment function createOneTimePayment( address _merchant, address _token, - uint256 _amount - ) external returns (address oneTimeAddress); + uint256 _amount, + uint256 _salt, + uint256 _paymentId, + address precomputed + ) external returns (OneTime oneTime); + + /// @notice Receives a one-time payment + /// @param _merchant Address of the merchant + /// @param _token 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; + + /// @notice Computes the address of a one-time payment + /// @param _merchant Address of the merchant + /// @param _token Address of the token + /// @param _amount Amount of the token + /// @param _salt Salt used to compute the address + /// @return oneTime Address of the one-time payment + function computeOneTimeAddress( + address _merchant, + address _token, + uint256 _amount, + uint256 _salt, + uint256 _paymentId + ) external view returns (OneTime oneTime); /** * @notice Processes a subscription diff --git a/test/integration/Grateful.t.sol b/test/integration/Grateful.t.sol index 0b09a86..a019c30 100644 --- a/test/integration/Grateful.t.sol +++ b/test/integration/Grateful.t.sol @@ -68,17 +68,19 @@ contract IntegrationGreeter is IntegrationBase { } function test_OneTimePayment() public { - // 1. Merchant calls api to make one time payment to his address - vm.prank(_gratefulAutomation); - address oneTimeAddress = _grateful.createOneTimePayment(_merchant, address(_usdc), _amount); + // 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)); - // 2. Once the payment address is created, the client sends the payment + // 3. Once the payment address is precomputed, the client sends the payment vm.prank(_usdcWhale); - _usdc.transfer(oneTimeAddress, _amount); // Only tx sent by the client, doesn't need contract interaction + _usdc.transfer(precomputed, _amount); // Only tx sent by the client, doesn't need contract interaction - // 3. The payment is processed + // 4. Merchant calls api to make one time payment to his address vm.prank(_gratefulAutomation); - OneTime(oneTimeAddress).processPayment(); + _grateful.createOneTimePayment(_merchant, address(_usdc), _amount, 4, paymentId, precomputed); // Merchant receives the payment assertEq(_usdc.balanceOf(_merchant), _amount);