Skip to content

Commit

Permalink
feat: add custom fees
Browse files Browse the repository at this point in the history
  • Loading branch information
0xChin committed Oct 16, 2024
1 parent 7ede89c commit 8d11d8b
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 17 deletions.
29 changes: 23 additions & 6 deletions src/contracts/Grateful.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ contract Grateful is IGrateful, Ownable2Step {
/// @inheritdoc IGrateful
uint256 public fee;

/// @inheritdoc IGrateful
mapping(address => CustomFee) public override customFees;

/*//////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -103,11 +106,13 @@ contract Grateful is IGrateful, Ownable2Step {
}

/// @inheritdoc IGrateful
function applyFee(
uint256 amount
) public view returns (uint256) {
uint256 feeAmount = (amount * fee) / 10_000;
return amount - feeAmount;
function applyFee(address _merchant, uint256 _amount) public view returns (uint256) {
uint256 feePercentage = fee;
if (customFees[_merchant].isSet) {
feePercentage = customFees[_merchant].fee;
}
uint256 feeAmount = (_amount * feePercentage) / 10_000;
return _amount - feeAmount;
}

/*//////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -281,6 +286,18 @@ contract Grateful is IGrateful, Ownable2Step {
fee = _newFee;
}

/// @inheritdoc IGrateful
function setCustomFee(uint256 _newFee, address _merchant) external onlyOwner {
customFees[_merchant] = CustomFee({isSet: true, fee: _newFee});
}

/// @inheritdoc IGrateful
function unsetCustomFee(
address _merchant
) external onlyOwner {
delete customFees[_merchant];
}

/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -310,7 +327,7 @@ contract Grateful is IGrateful, Ownable2Step {
}

// Apply the fee
uint256 amountWithFee = applyFee(_amount);
uint256 amountWithFee = applyFee(_merchant, _amount);

// Transfer fee to owner
if (!IERC20(_token).transfer(owner(), _amount - amountWithFee)) {
Expand Down
38 changes: 34 additions & 4 deletions src/interfaces/IGrateful.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import {AaveV3ERC4626, IPool} from "yield-daddy/aave-v3/AaveV3ERC4626.sol";
* @notice Interface for the Grateful contract that allows payments in whitelisted tokens with optional yield via AAVE.
*/
interface IGrateful {
/*///////////////////////////////////////////////////////////////
STRUCTS
//////////////////////////////////////////////////////////////*/

struct CustomFee {
bool isSet;
uint256 fee;
}

/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -108,6 +117,13 @@ interface IGrateful {
/// @return Fee in basis points (10000 = 100%).
function fee() external view returns (uint256);

/// @notice Returns the custom fee applied to the payments.
/// @return isSet If the custom fee has been set.
/// @return fee Custom fee
function customFees(
address _merchant
) external view returns (bool isSet, uint256 fee);

/*///////////////////////////////////////////////////////////////
LOGIC
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -294,12 +310,11 @@ interface IGrateful {

/**
* @notice Applies the fee to an amount.
* @param amount Amount before fee.
* @param _merchant Address of the merchant.
* @param _amount Amount before fee.
* @return amountWithFee Amount after fee is applied.
*/
function applyFee(
uint256 amount
) external view returns (uint256 amountWithFee);
function applyFee(address _merchant, uint256 _amount) external view returns (uint256 amountWithFee);

/**
* @notice Sets a new fee.
Expand All @@ -308,4 +323,19 @@ interface IGrateful {
function setFee(
uint256 _newFee
) external;

/**
* @notice Sets a new fee for a certain merchant.
* @param _newFee New fee to be applied (in basis points, 10000 = 100%).
* @param _merchant Address of the merchant.
*/
function setCustomFee(uint256 _newFee, address _merchant) external;

/**
* @notice Sets a new fee for a certain merchant.
* @param _merchant Address of the merchant.
*/
function unsetCustomFee(
address _merchant
) external;
}
93 changes: 86 additions & 7 deletions test/integration/Grateful.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract IntegrationGreeter is IntegrationBase {
);
vm.stopPrank();

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

function test_PaymentYieldingFunds() public {
Expand All @@ -35,7 +35,7 @@ contract IntegrationGreeter is IntegrationBase {
vm.prank(_merchant);
_grateful.withdraw(address(_usdc));

assertGt(_usdc.balanceOf(_merchant), _grateful.applyFee(_amount));
assertGt(_usdc.balanceOf(_merchant), _grateful.applyFee(_merchant, _amount));
}

function test_OneTimePayment() public {
Expand All @@ -54,7 +54,86 @@ contract IntegrationGreeter is IntegrationBase {
_grateful.createOneTimePayment(_merchant, _tokens, _amount, 4, paymentId, precomputed);

// Merchant receives the payment
assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_amount));
assertEq(_usdc.balanceOf(_merchant), _grateful.applyFee(_merchant, _amount));
}

function test_PaymentWithCustomFee() public {
// ------------------------------
// 1. Set custom fee of 2% (200 basis points) for the merchant
// ------------------------------
vm.prank(_owner);
_grateful.setCustomFee(200, _merchant);

// Process payment with custom fee of 2%
vm.startPrank(_usdcWhale);
_usdc.approve(address(_grateful), _amount);
uint256 paymentId1 = _grateful.calculateId(_usdcWhale, _merchant, address(_usdc), _amount);
_grateful.pay(_merchant, address(_usdc), _amount, paymentId1);
vm.stopPrank();

// Expected amounts
uint256 expectedCustomFee = (_amount * 200) / 10_000; // 2% fee
uint256 expectedMerchantAmount = _amount - expectedCustomFee;

// Verify balances after first payment
assertEq(_usdc.balanceOf(_merchant), expectedMerchantAmount, "Merchant balance mismatch after first payment");
assertEq(_usdc.balanceOf(_owner), expectedCustomFee, "Owner balance mismatch after first payment");

// ------------------------------
// 2. Set custom fee of 0% (no fee) for the _merchant
// ------------------------------
vm.prank(_owner);
_grateful.setCustomFee(0, _merchant);

// Process payment with custom fee of 0%
vm.startPrank(_usdcWhale);
_usdc.approve(address(_grateful), _amount);
uint256 paymentId2 = _grateful.calculateId(_usdcWhale, _merchant, address(_usdc), _amount);
_grateful.pay(_merchant, address(_usdc), _amount, paymentId2);
vm.stopPrank();

// Expected amounts
uint256 expectedZeroFee = 0; // 0% fee
uint256 expectedMerchantAmount2 = _amount;

// Verify balances after second payment
assertEq(
_usdc.balanceOf(_merchant),
expectedMerchantAmount + expectedMerchantAmount2,
"Merchant balance mismatch after second payment"
);
assertEq(
_usdc.balanceOf(_owner), expectedCustomFee + expectedZeroFee, "Owner balance mismatch after second payment"
);

// ------------------------------
// 3. Unset custom fee for the _merchant (should revert to default fee)
// ------------------------------
vm.prank(_owner);
_grateful.unsetCustomFee(_merchant);

// Process payment after unsetting custom fee
vm.startPrank(_usdcWhale);
_usdc.approve(address(_grateful), _amount);
uint256 paymentId3 = _grateful.calculateId(_usdcWhale, _merchant, address(_usdc), _amount);
_grateful.pay(_merchant, address(_usdc), _amount, paymentId3);
vm.stopPrank();

// Expected amounts
uint256 expectedFeeAfterUnset = (_amount * 100) / 10_000; // 1% fee
uint256 expectedMerchantAmount3 = _amount - expectedFeeAfterUnset;

// Verify balances after fourth payment
assertEq(
_usdc.balanceOf(_merchant),
expectedMerchantAmount + expectedMerchantAmount2 + expectedMerchantAmount3,
"Merchant balance mismatch after fourth payment"
);
assertEq(
_usdc.balanceOf(_owner),
expectedCustomFee + expectedZeroFee + expectedFeeAfterUnset,
"Owner balance mismatch after fourth payment"
);
}

function test_OneTimePaymentYieldingFunds() public {
Expand Down Expand Up @@ -87,10 +166,10 @@ contract IntegrationGreeter is IntegrationBase {
_grateful.withdraw(address(_usdc));

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

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

Expand All @@ -115,7 +194,7 @@ contract IntegrationGreeter is IntegrationBase {
vm.stopPrank();

// 4. Calculate expected amounts after fee
uint256 amountAfterFee = _grateful.applyFee(_amount);
uint256 amountAfterFee = _grateful.applyFee(_merchant, _amount);
uint256 expectedAmountRecipient0 = (amountAfterFee * percentages[0]) / 10_000;
uint256 expectedAmountRecipient1 = (amountAfterFee * percentages[1]) / 10_000;

Expand Down Expand Up @@ -153,7 +232,7 @@ contract IntegrationGreeter is IntegrationBase {
_grateful.createOneTimePayment(_merchant, _tokens, _amount, 4, paymentId, precomputed, recipients, percentages);

// 6. Calculate expected amounts after fee
uint256 amountAfterFee = _grateful.applyFee(_amount);
uint256 amountAfterFee = _grateful.applyFee(_merchant, _amount);
uint256 expectedAmountRecipient0 = (amountAfterFee * percentages[0]) / 10_000;
uint256 expectedAmountRecipient1 = (amountAfterFee * percentages[1]) / 10_000;

Expand Down

0 comments on commit 8d11d8b

Please sign in to comment.