Skip to content

Commit

Permalink
feat: enable one time payments with single address (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xChin authored Oct 10, 2024
1 parent da25de8 commit b327f6a
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 40 deletions.
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 {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";

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

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 Down Expand Up @@ -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
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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));
Expand All @@ -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));
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

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 @@ 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,
Expand Down Expand Up @@ -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.
Expand All @@ -273,7 +275,7 @@ interface IGrateful {
*/
function createOneTimePayment(
address _merchant,
address _token,
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId,
Expand All @@ -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.
Expand All @@ -294,7 +296,7 @@ interface IGrateful {
*/
function createOneTimePayment(
address _merchant,
address _token,
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId,
Expand Down Expand Up @@ -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.
Expand All @@ -341,7 +348,7 @@ interface IGrateful {
*/
function computeOneTimeAddress(
address _merchant,
address _token,
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId,
Expand All @@ -352,15 +359,15 @@ 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.
* @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

0 comments on commit b327f6a

Please sign in to comment.