Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable one time payments with single address #1

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions src/contracts/Grateful.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";

import {OneTime} from "contracts/OneTime.sol";

import {console} from "forge-std/console.sol";

Check warning on line 9 in src/contracts/Grateful.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Variable "console" is unused
import {IERC20} from "forge-std/interfaces/IERC20.sol";
import {IGrateful} from "interfaces/IGrateful.sol";

Expand All @@ -13,7 +15,7 @@

/**
* @title Grateful Contract
* @notice Allows payments in whitelisted tokens with optional yield via AAVE, including payment splitting functionality.

Check warning on line 18 in src/contracts/Grateful.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Line length must be no more than 120 but current length is 121
*/
contract Grateful is IGrateful, Ownable2Step {
using Bytes32AddressLib for bytes32;
Expand Down Expand Up @@ -61,6 +63,17 @@
_;
}

modifier onlyWhenTokensWhitelisted(
address[] memory _tokens
) {
for (uint256 i = 0; i < _tokens.length; i++) {
if (!tokensWhitelisted[_tokens[i]]) {
revert Grateful_TokenNotWhitelisted();
}
}
_;
}

/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -107,7 +120,7 @@
//////////////////////////////////////////////////////////////*/

/// @inheritdoc IGrateful
function addToken(

Check warning on line 123 in src/contracts/Grateful.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Function order is incorrect, external function can not go after public view function (line 111)
address _token
) external onlyOwner {
tokensWhitelisted[_token] = true;
Expand Down Expand Up @@ -276,19 +289,19 @@
/// @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
Expand All @@ -309,7 +322,7 @@
/// @inheritdoc IGrateful
function computeOneTimeAddress(
address _merchant,
address _token,
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId,
Expand All @@ -318,7 +331,7 @@
) 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));
Expand All @@ -329,38 +342,49 @@
/// @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));
Expand Down Expand Up @@ -455,7 +479,7 @@
revert Grateful_VaultNotSet();
}
uint256 _shares = vault.deposit(recipientShare, address(this));
shares[recipient][_token] += _shares;

Check warning on line 482 in src/contracts/Grateful.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Possible reentrancy vulnerabilities. Avoid state changes after transfer
} else {
// Transfer tokens to recipient
if (!IERC20(_token).transfer(recipient, recipientShare)) {
Expand All @@ -471,7 +495,7 @@
revert Grateful_VaultNotSet();
}
uint256 _shares = vault.deposit(amountWithFee, address(this));
shares[_merchant][_token] += _shares;

Check warning on line 498 in src/contracts/Grateful.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Possible reentrancy vulnerabilities. Avoid state changes after transfer
} else {
// Transfer tokens to merchant
if (!IERC20(_token).transfer(_merchant, amountWithFee)) {
Expand Down
12 changes: 9 additions & 3 deletions src/contracts/OneTime.sol
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {console} from "forge-std/console.sol";

Check warning on line 4 in src/contracts/OneTime.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Variable "console" is unused
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);
}
}
}
}
31 changes: 19 additions & 12 deletions src/interfaces/IGrateful.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -62,10 +64,10 @@
/**
* @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,
Expand Down Expand Up @@ -169,7 +171,7 @@
* @notice Adds a token to the whitelist.
* @param _token Address of the token to be added.
*/
function addToken(

Check warning on line 174 in src/interfaces/IGrateful.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Function order is incorrect, external function can not go after external view function (line 164)
address _token
) external;

Expand Down Expand Up @@ -264,7 +266,7 @@
/**
* @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.
Expand All @@ -273,7 +275,7 @@
*/
function createOneTimePayment(
address _merchant,
address _token,
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId,
Expand All @@ -285,7 +287,7 @@
/**
* @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.
Expand All @@ -294,7 +296,7 @@
*/
function createOneTimePayment(
address _merchant,
address _token,
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId,
Expand Down Expand Up @@ -322,16 +324,21 @@
/**
* @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.
Expand All @@ -341,7 +348,7 @@
*/
function computeOneTimeAddress(
address _merchant,
address _token,
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId,
Expand All @@ -352,15 +359,15 @@
/**
* @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.
* @return oneTime Address of the computed OneTime contract.
*/
function computeOneTimeAddress(
address _merchant,
address _token,
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId
Expand Down
20 changes: 10 additions & 10 deletions test/integration/Grateful.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -131,26 +131,29 @@ 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);
_usdc.transfer(precomputed, _amount); // Only tx sent by the client, doesn't need contract interaction

// 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);
Expand All @@ -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);

Expand Down Expand Up @@ -225,19 +228,16 @@ 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);
_usdc.transfer(precomputed, _amount);

// 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);
Expand Down
4 changes: 3 additions & 1 deletion test/integration/IntegrationBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)),
Expand Down
Loading