Skip to content

Commit

Permalink
feat: yield preference sent in payment
Browse files Browse the repository at this point in the history
  • Loading branch information
0xChin committed Nov 11, 2024
1 parent 49ca3af commit 9d11222
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 67 deletions.
43 changes: 24 additions & 19 deletions src/contracts/Grateful.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ contract Grateful is IGrateful, Ownable2Step {
/// @inheritdoc IGrateful
mapping(address => bool) public tokensWhitelisted;

/// @inheritdoc IGrateful
mapping(address => bool) public yieldingFunds;

/// @inheritdoc IGrateful
mapping(address => AaveV3ERC4626) public vaults;

Expand Down Expand Up @@ -149,9 +146,10 @@ contract Grateful is IGrateful, Ownable2Step {
address _merchant,
address _token,
uint256 _amount,
uint256 _id
uint256 _id,
bool _yieldFunds
) external onlyWhenTokenWhitelisted(_token) {
_processPayment(msg.sender, _merchant, _token, _amount, _id);
_processPayment(msg.sender, _merchant, _token, _amount, _id, _yieldFunds);
}

/// @inheritdoc IGrateful
Expand All @@ -161,19 +159,27 @@ contract Grateful is IGrateful, Ownable2Step {
uint256 _amount,
uint256 _salt,
uint256 _paymentId,
bool _yieldFunds,
address precomputed
) external onlyWhenTokensWhitelisted(_tokens) returns (OneTime oneTime) {
oneTimePayments[precomputed] = true;
oneTime = new OneTime{salt: bytes32(_salt)}(IGrateful(address(this)), _tokens, _merchant, _amount, _paymentId);
oneTime =
new OneTime{salt: bytes32(_salt)}(IGrateful(address(this)), _tokens, _merchant, _amount, _paymentId, _yieldFunds);
emit OneTimePaymentCreated(_merchant, _tokens, _amount);
}

/// @inheritdoc IGrateful
function receiveOneTimePayment(address _merchant, address _token, uint256 _paymentId, uint256 _amount) external {
function receiveOneTimePayment(
address _merchant,
address _token,
uint256 _paymentId,
uint256 _amount,
bool _yieldFunds
) external {
if (!oneTimePayments[msg.sender]) {
revert Grateful_OneTimeNotFound();
}
_processPayment(msg.sender, _merchant, _token, _amount, _paymentId);
_processPayment(msg.sender, _merchant, _token, _amount, _paymentId, _yieldFunds);
}

/// @inheritdoc IGrateful
Expand All @@ -182,10 +188,12 @@ contract Grateful is IGrateful, Ownable2Step {
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId
uint256 _paymentId,
bool _yieldFunds
) external view returns (OneTime oneTime) {
bytes memory bytecode =
abi.encodePacked(type(OneTime).creationCode, abi.encode(address(this), _tokens, _merchant, _amount, _paymentId));
bytes memory bytecode = abi.encodePacked(
type(OneTime).creationCode, abi.encode(address(this), _tokens, _merchant, _amount, _paymentId, _yieldFunds)
);
bytes32 bytecodeHash = keccak256(bytecode);
bytes32 addressHash = keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(_salt), bytecodeHash));
address computedAddress = address(uint160(uint256(addressHash)));
Expand Down Expand Up @@ -265,11 +273,6 @@ contract Grateful is IGrateful, Ownable2Step {
}
}

/// @inheritdoc IGrateful
function switchYieldingFunds() external {
yieldingFunds[msg.sender] = !yieldingFunds[msg.sender];
}

/// @inheritdoc IGrateful
function setFee(
uint256 _newFee
Expand Down Expand Up @@ -300,13 +303,15 @@ contract Grateful is IGrateful, Ownable2Step {
* @param _token Address of the token.
* @param _amount Amount of the token.
* @param _paymentId ID of the payment
* @param _yieldFunds Whether to yield funds or not
*/
function _processPayment(
address _sender,
address _merchant,
address _token,
uint256 _amount,
uint256 _paymentId
uint256 _paymentId,
bool _yieldFunds
) internal {
// Transfer the full amount from the sender to this contract
IERC20(_token).safeTransferFrom(_sender, address(this), _amount);
Expand All @@ -325,7 +330,7 @@ contract Grateful is IGrateful, Ownable2Step {
IERC20(_token).safeTransfer(owner(), _amount - amountWithFee);

// Proceed as before, paying the merchant
if (yieldingFunds[_merchant]) {
if (_yieldFunds) {
AaveV3ERC4626 vault = vaults[_token];
if (address(vault) == address(0)) {
IERC20(_token).safeTransfer(_merchant, amountWithFee);
Expand All @@ -338,6 +343,6 @@ contract Grateful is IGrateful, Ownable2Step {
IERC20(_token).safeTransfer(_merchant, amountWithFee);
}

emit PaymentProcessed(_sender, _merchant, _token, _amount, yieldingFunds[_merchant], _paymentId);
emit PaymentProcessed(_sender, _merchant, _token, _amount, _yieldFunds, _paymentId);
}
}
11 changes: 9 additions & 2 deletions src/contracts/OneTime.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@ contract OneTime {

IGrateful immutable grateful;

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

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Explicitly mark visibility of state

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

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Immutable variables name should be capitalized SNAKE_CASE

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

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

'grateful' should start with _

constructor(IGrateful _grateful, address[] memory _tokens, address _merchant, uint256 _amount, uint256 _paymentId) {
constructor(
IGrateful _grateful,
address[] memory _tokens,
address _merchant,
uint256 _amount,
uint256 _paymentId,
bool _yieldFunds
) {
grateful = _grateful;

for (uint256 i = 0; i < _tokens.length; i++) {
IERC20 token = IERC20(_tokens[i]);
if (token.balanceOf(address(this)) >= _amount) {
token.safeIncreaseAllowance(address(_grateful), _amount);
_grateful.receiveOneTimePayment(_merchant, address(token), _paymentId, _amount);
_grateful.receiveOneTimePayment(_merchant, address(token), _paymentId, _amount, _yieldFunds);
}
}
}
Expand Down
30 changes: 15 additions & 15 deletions src/interfaces/IGrateful.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,6 @@ interface IGrateful {
address _token
) external view returns (bool);

/// @notice Returns the yielding preference of a merchant.
/// @param _merchant Address of the merchant.
/// @return True if the merchant prefers yielding funds, false otherwise.
function yieldingFunds(
address _merchant
) external view returns (bool);

/// @notice Returns the vault associated with a token.
/// @param _token Address of the token.
/// @return Address of the vault contract.
Expand Down Expand Up @@ -174,8 +167,9 @@ interface IGrateful {
* @param _token Address of the token used for payment.
* @param _amount Amount of the token to be paid.
* @param _id ID of the payment.
* @param _yieldFunds Whether to yield funds or not
*/
function pay(address _merchant, address _token, uint256 _amount, uint256 _id) external;
function pay(address _merchant, address _token, uint256 _amount, uint256 _id, bool _yieldFunds) external;

/**
* @notice Creates a one-time payment without payment splitting.
Expand All @@ -184,6 +178,7 @@ interface IGrateful {
* @param _amount Amount of the token.
* @param _salt Salt used for address computation.
* @param _paymentId ID of the payment.
* @param _yieldFunds Whether to yield funds or not
* @param precomputed Precomputed address of the OneTime contract.
* @return oneTime Address of the created OneTime contract.
*/
Expand All @@ -193,6 +188,7 @@ interface IGrateful {
uint256 _amount,
uint256 _salt,
uint256 _paymentId,
bool _yieldFunds,
address precomputed
) external returns (OneTime oneTime);

Expand All @@ -201,9 +197,16 @@ interface IGrateful {
* @param _merchant Address of the merchant.
* @param _token Token address.
* @param _paymentId ID of the payment.
* @param _yieldFunds Whether to yield funds or not
* @param _amount Amount of the token.
*/
function receiveOneTimePayment(address _merchant, address _token, uint256 _paymentId, uint256 _amount) external;
function receiveOneTimePayment(
address _merchant,
address _token,
uint256 _paymentId,
uint256 _amount,
bool _yieldFunds
) external;

/**
* @notice Computes the address of a one-time payment contract without payment splitting.
Expand All @@ -212,14 +215,16 @@ interface IGrateful {
* @param _amount Amount of the token.
* @param _salt Salt used for address computation.
* @param _paymentId ID of the payment.
* @param _yieldFunds Whether to yield funds or not
* @return oneTime Address of the computed OneTime contract.
*/
function computeOneTimeAddress(
address _merchant,
address[] memory _tokens,
uint256 _amount,
uint256 _salt,
uint256 _paymentId
uint256 _paymentId,
bool _yieldFunds
) external view returns (OneTime oneTime);

/**
Expand Down Expand Up @@ -252,11 +257,6 @@ interface IGrateful {
*/
function withdrawMultiple(address[] memory _tokens, uint256[] memory _assets) external;

/**
* @notice Toggles the merchant's preference to yield funds.
*/
function switchYieldingFunds() external;

/**
* @notice Calculates the ID of a payment.
* @param _sender Address of the sender.
Expand Down
54 changes: 23 additions & 31 deletions test/integration/Grateful.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,24 @@ import {OneTime} from "contracts/OneTime.sol";
import {IntegrationBase} from "test/integration/IntegrationBase.sol";

contract IntegrationGreeter is IntegrationBase {
function _approveAndPay(address payer, address merchant, uint256 amount) internal {
function _approveAndPay(address payer, address merchant, uint256 amount, bool _yieldFunds) internal {
uint256 paymentId = _grateful.calculateId(payer, merchant, address(_usdc), amount);
vm.startPrank(payer);
_usdc.approve(address(_grateful), amount);
_grateful.pay(merchant, address(_usdc), amount, paymentId);
_grateful.pay(merchant, address(_usdc), amount, paymentId, _yieldFunds);
vm.stopPrank();
}

function test_Payment() public {
_approveAndPay(_payer, _merchant, _AMOUNT_USDC);
_approveAndPay(_payer, _merchant, _AMOUNT_USDC, false);

assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_merchant, _AMOUNT_USDC));
}

function test_PaymentYieldingFunds() public {
assertEq(_grateful.yieldingFunds(_merchant), false);
_approveAndPay(_payer, _merchant, _AMOUNT_USDC, true);

vm.prank(_merchant);
_grateful.switchYieldingFunds();

assertEq(_grateful.yieldingFunds(_merchant), true);

_approveAndPay(_payer, _merchant, _AMOUNT_USDC);

vm.warp(block.timestamp + 60 days);
vm.warp(block.timestamp + 1 days);

vm.prank(_merchant);
_grateful.withdraw(address(_usdc));
Expand All @@ -42,15 +35,16 @@ contract IntegrationGreeter is IntegrationBase {
uint256 paymentId = _grateful.calculateId(_payer, _merchant, address(_usdc), _AMOUNT_USDC);

// 2. Precompute address
address precomputed = address(_grateful.computeOneTimeAddress(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId));
address precomputed =
address(_grateful.computeOneTimeAddress(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId, false));

// 3. Once the payment address is precomputed, the client sends the payment
vm.prank(_payer);
_usdc.transfer(precomputed, _AMOUNT_USDC); // 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, _tokens, _AMOUNT_USDC, 4, paymentId, precomputed);
_grateful.createOneTimePayment(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId, false, precomputed);

// Merchant receives the payment
assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_merchant, _AMOUNT_USDC));
Expand All @@ -61,15 +55,17 @@ contract IntegrationGreeter is IntegrationBase {
uint256 paymentId = _grateful.calculateId(_payer, _merchant, address(_usdc), _AMOUNT_USDC);

// 2. Precompute address
address precomputed = address(_grateful.computeOneTimeAddress(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId));
address precomputed =
address(_grateful.computeOneTimeAddress(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId, false));

// 3. Once the payment address is precomputed, the client sends the payment
vm.prank(_payer);
_usdc.transfer(precomputed, _AMOUNT_USDC * 2); // 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);
OneTime _oneTime = _grateful.createOneTimePayment(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId, precomputed);
OneTime _oneTime =
_grateful.createOneTimePayment(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId, false, precomputed);

// Merchant receives the payment
assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_merchant, _AMOUNT_USDC));
Expand All @@ -95,7 +91,7 @@ contract IntegrationGreeter is IntegrationBase {
_grateful.setCustomFee(200, _merchant);

// Process payment with custom fee of 2%
_approveAndPay(_payer, _merchant, _AMOUNT_USDC);
_approveAndPay(_payer, _merchant, _AMOUNT_USDC, false);

// Expected amounts
uint256 expectedCustomFee = (_AMOUNT_USDC * 200) / 10_000; // 2% fee
Expand All @@ -115,7 +111,7 @@ contract IntegrationGreeter is IntegrationBase {
vm.warp(block.timestamp + 1);

// Process payment with custom fee of 0%
_approveAndPay(_payer, _merchant, _AMOUNT_USDC);
_approveAndPay(_payer, _merchant, _AMOUNT_USDC, false);

// Expected amounts
uint256 expectedZeroFee = 0; // 0% fee
Expand All @@ -141,7 +137,7 @@ contract IntegrationGreeter is IntegrationBase {
vm.warp(block.timestamp + 1);

// Process payment after unsetting custom fee
_approveAndPay(_payer, _merchant, _AMOUNT_USDC);
_approveAndPay(_payer, _merchant, _AMOUNT_USDC, false);

// Expected amounts
uint256 expectedFeeAfterUnset = (_AMOUNT_USDC * 100) / 10_000; // 1% fee
Expand All @@ -151,7 +147,7 @@ contract IntegrationGreeter is IntegrationBase {
assertEq(
_usdc.balanceOf(_merchant),
expectedMerchantAmount + expectedMerchantAmount2 + expectedMerchantAmount3,
"Merchant balance mismatch after fourth payment"
"Merchant balance mismatch after third payment"
);
assertEq(
_usdc.balanceOf(_owner),
Expand All @@ -168,31 +164,27 @@ contract IntegrationGreeter is IntegrationBase {
uint256 paymentId = _grateful.calculateId(_payer, _merchant, address(_usdc), _AMOUNT_USDC);

// 2. Precompute address
address precomputed = address(_grateful.computeOneTimeAddress(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId));
address precomputed = address(_grateful.computeOneTimeAddress(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId, true));

Check warning on line 167 in test/integration/Grateful.t.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

// 3. Once the payment address is precomputed, the client sends the payment
vm.prank(_payer);
_usdc.transfer(precomputed, _AMOUNT_USDC);

// 4. Set merchant to yield funds
vm.prank(_merchant);
_grateful.switchYieldingFunds();

// 5. Grateful automation calls api to make one time payment to his address
// 4. Grateful automation calls api to make one time payment to his address
vm.prank(_gratefulAutomation);
_grateful.createOneTimePayment(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId, precomputed);
_grateful.createOneTimePayment(_merchant, _tokens, _AMOUNT_USDC, 4, paymentId, true, precomputed);

// 6. Advance time
// 5. Advance time so yield is generated
vm.warp(block.timestamp + 1 days);

// 7. Merchant withdraws funds
// 6. Merchant withdraws funds
vm.prank(_merchant);
_grateful.withdraw(address(_usdc));

// 8. Check if merchant's balance is greater than the amount with fee applied
// 7. Check if merchant's balance is greater than the amount with fee applied
assertGt(_usdc.balanceOf(_merchant), _grateful.applyFee(_merchant, _AMOUNT_USDC));

// 9. Check that owner holds the fee amount
// 8. Check that owner holds the fee amount
uint256 feeAmount = _AMOUNT_USDC - _grateful.applyFee(_merchant, _AMOUNT_USDC);
assertEq(_usdc.balanceOf(_owner), feeAmount);
}
Expand Down

0 comments on commit 9d11222

Please sign in to comment.