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

Saving/Depost Feature #39

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ typechain-types
cache
artifacts

package-lock.json
yarn.lock
104 changes: 80 additions & 24 deletions contracts/Goalz.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@aave/core-v3/contracts/interfaces/IPool.sol";
import "./interfaces/IQuoter.sol";
import "./interfaces/ISwapRouter.sol";
import "./GoalzToken.sol";
//import "./IGoalzToken.sol";
import "./gelato/AutomateTaskCreator.sol";
Expand All @@ -26,6 +28,7 @@ contract Goalz is ERC721, ERC721Enumerable, AutomateTaskCreator, ReentrancyGuard
address depositToken;
bool complete;
uint256 startInterestIndex;
address savingToken;
}

struct AutomatedDeposit {
Expand All @@ -41,7 +44,12 @@ contract Goalz is ERC721, ERC721Enumerable, AutomateTaskCreator, ReentrancyGuard
mapping(uint => SavingsGoal) public savingsGoals;
mapping(uint => AutomatedDeposit) public automatedDeposits;

event GoalCreated(address indexed saver, uint indexed goalId, string what, string why, uint targetAmount, uint targetDate, address depositToken, uint256 interestIndex);
ISwapRouter public immutable swapRouter;
IQuoter public immutable quoter;
uint24 public constant poolFee = 3000;
uint256 public constant SLIPPAGE_TOLERANCE = 50; // 0.5%

event GoalCreated(address indexed saver, uint indexed goalId, string what, string why, uint targetAmount, uint targetDate, address depositToken, address savingToken, uint256 interestIndex);
event GoalDeleted(address indexed saver, uint indexed goalId);
event GoalzTokenCreated(address indexed depositToken, address indexed goalzToken);
event DepositMade(address indexed saver, uint indexed goalId, uint amount);
Expand All @@ -50,7 +58,7 @@ contract Goalz is ERC721, ERC721Enumerable, AutomateTaskCreator, ReentrancyGuard
event AutomatedDepositCanceled(address indexed saver, uint indexed goalId);
event GoalCompleted(address indexed saver, uint indexed goalId, uint targetAmount);

constructor(address[] memory _initialDepositTokens, address[] memory _initialATokens, address _automate, address _lendingPool)
constructor(address[] memory _initialDepositTokens, address[] memory _initialATokens, address _automate, address _lendingPool, address _swapRouter, address _quoter)
ERC721("Goalz", "GOALZ")
AutomateTaskCreator(_automate)
{
Expand All @@ -59,6 +67,8 @@ contract Goalz is ERC721, ERC721Enumerable, AutomateTaskCreator, ReentrancyGuard
_addDepositToken(_initialDepositTokens[i], _initialATokens[i]);
}
lendingPool = IPool(_lendingPool);
swapRouter = ISwapRouter(_swapRouter);
quoter = IQuoter(_quoter);
}

function _addDepositToken(address _depositToken, address _aToken) internal {
Expand Down Expand Up @@ -93,19 +103,21 @@ contract Goalz is ERC721, ERC721Enumerable, AutomateTaskCreator, ReentrancyGuard
string memory why,
uint targetAmount,
uint targetDate,
address depositToken
address depositToken,
address savingToken
) external {
require(targetAmount > 0, "Target amount should be greater than 0");
require(targetDate > block.timestamp, "Target date should be in the future");
require(address(goalzTokens[depositToken]) != address(0), "Deposit token should be USDC or WETH");
require(address(goalzTokens[savingToken]) != address(0), "Invalid saving token");

uint goalId = _tokenIdCounter.current();
uint256 startInterestIndex = goalzTokens[depositToken].getInterestIndex();
savingsGoals[goalId] = SavingsGoal(what, why, targetAmount, 0, targetDate, depositToken, false, startInterestIndex);
uint256 startInterestIndex = goalzTokens[savingToken].getInterestIndex();
savingsGoals[goalId] = SavingsGoal(what, why, targetAmount, 0, targetDate, depositToken, false, startInterestIndex, savingToken);
_mint(msg.sender, goalId);
_tokenIdCounter.increment();

emit GoalCreated(msg.sender, goalId, what, why, targetAmount, targetDate, depositToken, startInterestIndex);
emit GoalCreated(msg.sender, goalId, what, why, targetAmount, targetDate, depositToken, savingToken, startInterestIndex);
}

function deleteGoal(uint goalId) external goalExists(goalId) isGoalOwner(goalId) {
Expand All @@ -123,11 +135,6 @@ contract Goalz is ERC721, ERC721Enumerable, AutomateTaskCreator, ReentrancyGuard

SavingsGoal storage goal = savingsGoals[goalId];
require(goal.depositToken != address(0), "Invalid deposit token");
// if the deposit is more than the target amount, we should allow it, because compounding interest will make it more than the target amount
// require(goal.currentAmount + amount <= goal.targetAmount, "Deposit exceeds the goal target amount");
if (goal.currentAmount == 0) {
goal.startInterestIndex = goalzTokens[goal.depositToken].getInterestIndex();
}

if(goal.currentAmount + amount >= goal.targetAmount) {
goal.complete = true;
Expand All @@ -142,8 +149,18 @@ contract Goalz is ERC721, ERC721Enumerable, AutomateTaskCreator, ReentrancyGuard
SavingsGoal storage goal = savingsGoals[goalId];
require(goal.currentAmount > 0, "No funds to withdraw");
require(goal.depositToken != address(0), "Invalid deposit token");

// uint power = 10 ** ERC20(goal.depositToken).decimals();
// uint amount = goal.currentAmount;
address depositToken = goal.depositToken;
GoalzToken goalzToken = goalzTokens[depositToken];
// require(address(goalzToken) != address(0), "Invalid GoalzToken");

// Update interest index and calculate accured interest
// (uint256 accruedInterest, uint256 newInterestIndex) = goalzToken.updateAndCalculateAccruedInterest(
// goal.currentAmount, goal.startInterestIndex);
// goal.startInterestIndex = newInterestIndex;
// uint256 withdrawAmount = goal.currentAmount + accruedInterest;

// Update interest index and calculate accrued interest
(uint256 accruedInterest, uint256 newInterestIndex) = goalzToken.updateAndCalculateAccruedInterest(goal.currentAmount, goal.startInterestIndex);
Expand All @@ -154,7 +171,9 @@ contract Goalz is ERC721, ERC721Enumerable, AutomateTaskCreator, ReentrancyGuard
uint withdrawAmount = goal.currentAmount;
// uint power = 10 ** ERC20(depositToken).decimals();
goal.currentAmount = 0;
goalzToken.burn(msg.sender, withdrawAmount);
// Burn ONLY the current amount because the accrued interest was NOT minted
goalzToken.burn(msg.sender, goal.currentAmount);
// Withdraw the current amount + accrued interest
lendingPool.withdraw(depositToken, withdrawAmount, msg.sender);

emit WithdrawMade(msg.sender, goalId, withdrawAmount);
Expand Down Expand Up @@ -229,31 +248,68 @@ contract Goalz is ERC721, ERC721Enumerable, AutomateTaskCreator, ReentrancyGuard
}

function _deposit(address account, SavingsGoal storage goal, uint amount) internal nonReentrant {
address _depositToken = goal.depositToken;
require(_depositToken != address(0), "Invalid deposit token");
// address _depositToken = goal.depositToken;
// require(_depositToken != address(0), "Invalid deposit token");
address depositToken = goal.depositToken;
address savingToken = goal.savingToken;
require(depositToken != address(0) && savingToken != address(0), "Invalid tokens");
require(account != address(0), "Invalid account address");
require(amount > 0, "Deposit amount should be greater than 0");
require(IERC20(_depositToken).balanceOf(account) >= amount, "Insufficient balance");
require(IERC20(depositToken).balanceOf(account) >= amount, "Insufficient balance");

GoalzToken goalzToken = goalzTokens[_depositToken];
IERC20(depositToken).safeTransferFrom(account, address(this), amount);

// Update interest index and calculate accrued interest
uint256 accruedInterest;
uint256 newInterestIndex;
(accruedInterest, newInterestIndex) = goalzToken.updateAndCalculateAccruedInterest(goal.currentAmount, goal.startInterestIndex);
uint256 amountOut;
if (depositToken != savingToken) {
amountOut = _swapTokens(depositToken, savingToken, amount);
} else {
amountOut = amount;
}

GoalzToken goalzToken = goalzTokens[savingToken];

(uint256 accruedInterest, uint256 newInterestIndex) = goalzToken.updateAndCalculateAccruedInterest(
goal.currentAmount,
goal.startInterestIndex
);

goal.currentAmount += accruedInterest;
goal.startInterestIndex = newInterestIndex;

IERC20(_depositToken).safeTransferFrom(account, address(this), amount);
goalzToken.mint(account, amount + accruedInterest);
goal.currentAmount += (amount + accruedInterest);
_depositToAave(_depositToken, amount);
goalzToken.mint(account, amountOut + accruedInterest);
goal.currentAmount += (amountOut + accruedInterest);
_depositToAave(savingToken, amountOut);
}

function _depositToAave(address token, uint amount) internal {
IERC20(token).approve(address(lendingPool), amount);
lendingPool.deposit(token, amount, address(this), 0);
}

function _swapTokens(address tokenIn, address tokenOut, uint256 amountIn) internal returns (uint256 amountOut) {
uint256 expectedAmountOut = _getQuote(tokenIn, tokenOut, amountIn);
uint256 minAmountOut = expectedAmountOut * (10000 - SLIPPAGE_TOLERANCE) / 10000;

IERC20(tokenIn).approve(address(swapRouter), amountIn);

ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: poolFee,
recipient: address(this),
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: minAmountOut,
sqrtPriceLimitX96: 0
});

amountOut = swapRouter.exactInputSingle(params);
}

function _getQuote(address tokenIn, address tokenOut, uint256 amountIn) internal view returns (uint256) {
return 0;
}

function _withdrawFromAave(address token, uint amount) internal {
lendingPool.withdraw(token, amount, address(this));
}
Expand Down
7 changes: 2 additions & 5 deletions contracts/GoalzToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,9 @@ contract GoalzToken is ERC20, ReentrancyGuard, Ownable {
function updateAndCalculateAccruedInterest(uint256 amount, uint256 startInterestIndex) external onlyOwner returns (uint256 interestAccrued, uint256 currentInterestIndex) {
_updateInterestIndex();
currentInterestIndex = interestIndex;

interestAccrued = (amount * (currentInterestIndex - startInterestIndex)) / 10 ** 29;
return (interestAccrued, currentInterestIndex);

interestAccrued = (amount * (currentInterestIndex - startInterestIndex)) / (10 ** ERC20(depositToken).decimals());
}

// Disable transfers
function transfer(address, uint256) public pure override returns (bool) {
revert("Disabled");
Expand Down
86 changes: 86 additions & 0 deletions contracts/TokenSwap.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma abicoder v2;

import './lib/TransferHelper.sol';
import './interfaces/ISwapRouter.sol';

contract TokenSwap {
address public constant routerAddr = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
ISwapRouter public immutable swapRouter = ISwapRouter(routerAddr);

// address public tokenA = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
// address public tokenB = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address public tokenA;
address public tokenB;

// For this example, we will set the pool fee to 0.3%.
uint24 public constant poolFee = 3000;

/// @notice swapExactInputSingle swaps a fixed amount of DAI for a maximum possible amount of WETH9
/// using the 0.3% pool by calling `exactInputSingle` in the swap router.
/// @dev The calling address must approve this contract to spend at least `amountIn` worth of its tokenA for this function to succeed.
/// @param amountIn The exact amount of tokenA that will be swapped for tokenB.
/// @return amountOut The amount of tokenB received.
function swapExactInputSingle(uint256 amountIn, address _tokenA, address _tokenB) external returns (uint256 amountOut) {
tokenA = _tokenA;
tokenB = _tokenB;

// Approve the router to spend DAI.
TransferHelper.safeApprove(tokenA, address(swapRouter), amountIn);

// Naively set amountOutMinimum to 0. In production, use an oracle or other data source to choose a safer value for amountOutMinimum.
// We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount.
ISwapRouter.ExactInputSingleParams memory params =
ISwapRouter.ExactInputSingleParams({
tokenIn: tokenA,
tokenOut: tokenB,
fee: poolFee,
recipient: msg.sender,
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});

// The call to `exactInputSingle` executes the swap.
amountOut = swapRouter.exactInputSingle(params);
}

/// @notice swapExactOutputSingle swaps a minimum possible amount of tokenA for a fixed amount of tokenB.
/// @dev The calling address must approve this contract to spend its tokenA for this function to succeed. As the amount of input tokenA is variable,
/// the calling address will need to approve for a slightly higher amount, anticipating some variance.
/// @param amountOut The exact amount of tokenB to receive from the swap.
/// @param amountInMaximum The amount of tokenA we are willing to spend to receive the specified amount of tokenB.
/// @return amountIn The amount of tokenA actually spent in the swap.
function swapExactOutputSingle(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
// Transfer the specified amount of tokenA to this contract.
// TransferHelper.safeTransferFrom(tokenA, msg.sender, address(this), amountInMaximum);

// Approve the router to spend the specifed `amountInMaximum` of tokenA.
// In production, you should choose the maximum amount to spend based on oracles or other data sources to acheive a better swap.
TransferHelper.safeApprove(tokenA, address(swapRouter), amountInMaximum);

ISwapRouter.ExactOutputSingleParams memory params =
ISwapRouter.ExactOutputSingleParams({
tokenIn: tokenA,
tokenOut: tokenB,
fee: poolFee,
recipient: msg.sender,
deadline: block.timestamp,
amountOut: amountOut,
amountInMaximum: amountInMaximum,
sqrtPriceLimitX96: 1
});

// Executes the swap returning the amountIn needed to spend to receive the desired amountOut.
amountIn = swapRouter.exactOutputSingle(params);

// For exact output swaps, the amountInMaximum may not have all been spent.
// If the actual amount spent (amountIn) is less than the specified maximum amount, we must refund the msg.sender and approve the swapRouter to spend 0.
if (amountIn < amountInMaximum) {
TransferHelper.safeApprove(tokenA, address(swapRouter), 0);
TransferHelper.safeTransfer(tokenA, msg.sender, amountInMaximum - amountIn);
}
}
}
1 change: 1 addition & 0 deletions contracts/gelato/Ops.sol
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { ModuleData } from "./Types.sol";
Expand Down
82 changes: 82 additions & 0 deletions contracts/interfaces/IERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.19;

/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);

/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);

/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);

/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);

/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);

/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);

/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);

/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}

interface IWETH is IERC20 {
function deposit() external payable;
function withdraw(uint256 amount) external;
}
Loading