Skip to content

Commit

Permalink
feat: add subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
0xblu committed Aug 21, 2024
1 parent eddbe27 commit 4585d95
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 15 deletions.
88 changes: 75 additions & 13 deletions src/contracts/Grateful.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity 0.8.26;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";

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

Check warning on line 7 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";
import {AaveV3ERC4626, IPool, IRewardsController} from "yield-daddy/aave-v3/AaveV3ERC4626.sol";

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

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Variable "IRewardsController" is unused
Expand All @@ -23,6 +25,11 @@ contract Grateful is IGrateful, Ownable2Step {
// @inheritdoc IGrateful
mapping(address => mapping(address => uint256)) public shares;

mapping(uint256 => Subscription) public subscriptions;

// @inheritdoc IGrateful
uint256 public subscriptionCount;

modifier onlyWhenTokenWhitelisted(address _token) {
if (!tokensWhitelisted[_token]) {
revert Grateful_TokenNotWhitelisted();
Expand Down Expand Up @@ -51,20 +58,45 @@ contract Grateful is IGrateful, Ownable2Step {
}

// @inheritdoc IGrateful
function pay(address _merchant, address _token, uint256 _amount) external onlyWhenTokenWhitelisted(_token) {
if (yieldingFunds[_merchant]) {
AaveV3ERC4626 vault = vaults[_token];
if (address(vault) == address(0)) {
revert Grateful_VaultNotSet();
}
IERC20(_token).transferFrom(msg.sender, address(this), _amount);
uint256 _shares = vault.deposit(_amount, address(this));
shares[_merchant][_token] += _shares;
} else {
if (!IERC20(_token).transferFrom(msg.sender, _merchant, _amount)) {
revert Grateful_TransferFailed();
}
function pay(address _merchant, address _token, uint256 _amount) public onlyWhenTokenWhitelisted(_token) {
_processPayment(msg.sender, _merchant, _token, _amount);
}

// @inheritdoc IGrateful
function subscribe(

Check warning on line 66 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 function (line 61)
address _token,
address _receiver,
uint256 _amount,
uint256 _interval
) external onlyWhenTokenWhitelisted(_token) returns (uint256 subscriptionId) {
subscriptionId = subscriptionCount++;
subscriptions[subscriptionId] = Subscription({
token: _token,
sender: msg.sender,
receiver: _receiver,
amount: _amount,
interval: _interval,
lastPaymentTime: block.timestamp
});

_processPayment(msg.sender, _receiver, _token, _amount);
}

// @inheritdoc IGrateful
function processSubscription(uint256 subscriptionId) external {
Subscription storage subscription = subscriptions[subscriptionId];

if (subscription.amount == 0) {
revert Grateful_SubscriptionDoesNotExist();
}
if (
block.timestamp < subscription.lastPaymentTime + subscription.interval // min timestamp for next payment
) {
revert Grateful_TooEarlyForNextPayment();
}

_processPayment(subscription.sender, subscription.receiver, subscription.token, subscription.amount);
subscription.lastPaymentTime = block.timestamp;
}

// @inheritdoc IGrateful
Expand All @@ -78,8 +110,38 @@ contract Grateful is IGrateful, Ownable2Step {
vault.redeem(_shares, msg.sender, address(this));
}

// @inheritdoc IGrateful
function cancelSubscription(uint256 subscriptionId) external {
Subscription storage subscription = subscriptions[subscriptionId];

if (subscription.amount == 0) {
revert Grateful_SubscriptionDoesNotExist();
}
if (subscription.sender != msg.sender) {
revert Grateful_OnlySenderCanCancelSubscription();
}

delete subscriptions[subscriptionId];
}

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

function _processPayment(address _sender, address _merchant, address _token, uint256 _amount) internal {
if (yieldingFunds[_merchant]) {
AaveV3ERC4626 vault = vaults[_token];
if (address(vault) == address(0)) {
revert Grateful_VaultNotSet();
}
IERC20(_token).transferFrom(_sender, address(this), _amount);
uint256 _shares = vault.deposit(_amount, address(this));
shares[_merchant][_token] += _shares;
} else {
if (!IERC20(_token).transferFrom(_sender, _merchant, _amount)) {
revert Grateful_TransferFailed();
}
}
}
}
61 changes: 61 additions & 0 deletions src/interfaces/IGrateful.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ 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 Subscription {
address token;
address sender;
address receiver;
uint256 amount;
uint256 interval;
uint256 lastPaymentTime;
}

/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -37,6 +50,21 @@ interface IGrateful {
*/
error Grateful_VaultTokenNotWhitelisted();

/**
* @notice Throws if the subscription does not exist
*/
error Grateful_SubscriptionDoesNotExist();

/**
* @notice Throws if the subscription is too early for the next payment
*/
error Grateful_TooEarlyForNextPayment();

/**
* @notice Throws if the sender is not the owner of the subscription
*/
error Grateful_OnlySenderCanCancelSubscription();

/*///////////////////////////////////////////////////////////////
VARIABLES
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -71,6 +99,12 @@ interface IGrateful {
*/
function shares(address _merchant, address _token) external view returns (uint256 _shares);

/**
* @notice Returns the number of subscriptions
* @return _subscriptionCount Number of subscriptions
*/
function subscriptionCount() external view returns (uint256 _subscriptionCount);

/*///////////////////////////////////////////////////////////////
LOGIC
//////////////////////////////////////////////////////////////*/
Expand All @@ -89,12 +123,39 @@ interface IGrateful {
*/
function pay(address _merchant, address _token, uint256 _amount) external;

/**
* @notice Subscribes to a token for a specific amount and interval
* @param _token Address of the token being subscribed
* @param _receiver Address of the receiver of the payments
* @param _amount Amount of the token to be paid
* @param _interval Interval in seconds between payments
* @return subscriptionId Id of the subscription
*/
function subscribe(
address _token,
address _receiver,
uint256 _amount,
uint256 _interval
) external returns (uint256 subscriptionId);

/**
* @notice Processes a subscription
* @param subscriptionId Id of the subscription to be processed
*/
function processSubscription(uint256 subscriptionId) external;

/**
* @notice Withdraws funds from the vault
* @param _token Address of the token being withdrawn
*/
function withdraw(address _token) external;

/**
* @notice Cancels a subscription
* @param subscriptionId Id of the subscription to be cancelled
*/
function cancelSubscription(uint256 subscriptionId) external;

/**
* @notice Switch the preference of the merchant to yield funds or not
*/
Expand Down
23 changes: 22 additions & 1 deletion test/integration/Grateful.t.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {IntegrationBase} from "test/integration/IntegrationBase.sol";
import {IGrateful, IntegrationBase} from "test/integration/IntegrationBase.sol";

contract IntegrationGreeter is IntegrationBase {
function test_Payment() public {
Expand Down Expand Up @@ -33,4 +33,25 @@ contract IntegrationGreeter is IntegrationBase {

assertGt(_dai.balanceOf(_merchant), _amount);
}

function test_Subscription() public {
vm.startPrank(_daiWhale);
_dai.approve(address(_grateful), _amount * 2);
uint256 subscriptionId = _grateful.subscribe(address(_dai), _merchant, _amount, 30 days);
vm.stopPrank();

// When subscription is created, a initial payment is made
assertEq(_dai.balanceOf(_merchant), _amount);

// Shouldn't be able to process the subscription before 30 days have passed
vm.expectRevert(IGrateful.Grateful_TooEarlyForNextPayment.selector);
_grateful.processSubscription(subscriptionId);

// Fast forward 30 days
vm.warp(block.timestamp + 30 days);

_grateful.processSubscription(subscriptionId);

assertEq(_dai.balanceOf(_merchant), _amount * 2);
}
}
2 changes: 1 addition & 1 deletion test/integration/IntegrationBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ contract IntegrationBase is Test {
IPool internal _aavePool = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
IGrateful internal _grateful;
AaveV3Vault internal _vault;
uint256 internal _amount = 1e25;
uint256 internal _amount = 10 * 10 ** 18; // 10 DAI

function setUp() public {
vm.startPrank(_owner);
Expand Down

0 comments on commit 4585d95

Please sign in to comment.