Skip to content

Commit

Permalink
Aave poc (#62)
Browse files Browse the repository at this point in the history
* v1 of Merkl contract

* feat: poc for contract

* remove feeManager

* feat: cooldown contract

* feat: handle fee recipient
  • Loading branch information
sogipec authored Apr 17, 2024
1 parent 2ecd6e2 commit 5fb3d94
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 3 deletions.
130 changes: 130 additions & 0 deletions contracts/coupons/AaveTokenWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.17;

import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "../DistributionCreator.sol";

import "../utils/UUPSHelper.sol";

contract AaveTokenWrapper is UUPSHelper, ERC20Upgradeable {
using SafeERC20 for IERC20;

// ================================= VARIABLES =================================

/// @notice `Core` contract handling access control
ICore public core;

// could be put as immutable in non upgradeable contract
address public token;
address public distributor;
address public distributionCreator;

mapping(address => uint256) public isMasterClaimer;
mapping(address => address) public delegateReceiver;
mapping(address => uint256) public permissionlessClaim;

error InvalidClaim();

// =================================== EVENTS ==================================

event Recovered(address indexed token, address indexed to, uint256 amount);

// ================================= MODIFIERS =================================

/// @notice Checks whether the `msg.sender` has the governor role or the guardian role
modifier onlyGovernor() {
if (!core.isGovernor(msg.sender)) revert NotGovernor();
_;
}

// ================================= FUNCTIONS =================================

function initialize(
address underlyingToken,
address _distributor,
address _core,
address _distributionCreator
) public initializer {
// TODO could fetch name and symbol based on real token
__ERC20_init("AaveTokenWrapper", "ATW");
__UUPSUpgradeable_init();
if (underlyingToken == address(0) || _distributor == address(0) || _distributionCreator == address(0))
revert ZeroAddress();
ICore(_core).isGovernor(msg.sender);
token = underlyingToken;
distributor = _distributor;
distributionCreator = _distributionCreator;
core = ICore(_core);
}

function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
// Needs an approval before hand, this is how mints are done
if (to == distributor) {
IERC20(token).safeTransferFrom(from, address(this), amount);
_mint(from, amount); // These are then transfered to the distributor
} else {
if (to == _getFeeRecipient()) {
IERC20(token).safeTransferFrom(from, to, amount);
_mint(from, amount);
}
}
}

function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
if (from == address(distributor)) {
if (tx.origin == to || permissionlessClaim[to] == 1 || isMasterClaimer[tx.origin] == 1) {
_handleClaim(to, amount);
} else if (allowance(to, tx.origin) > amount) {
_spendAllowance(to, tx.origin, amount);
_handleClaim(to, amount);
} else {
revert InvalidClaim();
}
} else if (to == _getFeeRecipient()) {
// To avoid having any token aside from the distributor
_burn(to, amount);
}
}

function _handleClaim(address to, uint256 amount) internal {
address delegate = delegateReceiver[to];
_burn(to, amount);
if (delegate == address(0) || delegate == to) {
IERC20(token).safeTransfer(to, amount);
} else {
IERC20(token).safeTransfer(delegate, amount);
}
}

function _getFeeRecipient() internal view returns (address feeRecipient) {
address _distributionCreator = distributionCreator;
feeRecipient = DistributionCreator(_distributionCreator).feeRecipient();
feeRecipient = feeRecipient == address(0) ? _distributionCreator : feeRecipient;
}

/// @notice Recovers any ERC20 token
function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernor {
IERC20(tokenAddress).safeTransfer(to, amountToRecover);
emit Recovered(tokenAddress, to, amountToRecover);
}

function toggleMasterClaimer(address claimer) external onlyGovernor {
uint256 claimStatus = 1 - isMasterClaimer[claimer];
isMasterClaimer[claimer] = claimStatus;
}

function togglePermissionlessClaim() external {
uint256 permission = 1 - permissionlessClaim[msg.sender];
permissionlessClaim[msg.sender] = permission;
}

function updateDelegateReceiver(address receiver) external {
delegateReceiver[msg.sender] = receiver;
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {}
}
7 changes: 4 additions & 3 deletions contracts/coupons/RadiantCoupon.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: BUSL-1.1

/*
pragma solidity ^0.8.17;
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
Expand Down Expand Up @@ -41,6 +41,7 @@ contract RadiantCoupon is UUPSHelper, ERC20Upgradeable {
IERC20(RADIANT).safeTransferFrom(from, address(this), amount);
_mint(from, amount); // These are then transfered to the distributor
}
// TODO: check allowance issue
if (to == address(FEE_MANAGER)) {
IERC20(RADIANT).safeTransferFrom(from, address(FEE_MANAGER), amount);
_mint(from, amount); // These are then transferred to the fee manager
Expand All @@ -50,8 +51,7 @@ contract RadiantCoupon is UUPSHelper, ERC20Upgradeable {
function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
if (to == address(FEE_MANAGER)) {
_burn(to, amount); // To avoid having any token aside from on the distributor
}
if (from == address(DISTRIBUTOR)) {
} else if (from == address(DISTRIBUTOR)) {
_burn(to, amount);
// HERE CALL THE VESTING CONTRACT TO STAKE ON BEHALF OF THE USER
}
Expand All @@ -66,3 +66,4 @@ contract RadiantCoupon is UUPSHelper, ERC20Upgradeable {
/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {}
}
*/
83 changes: 83 additions & 0 deletions contracts/coupons/StakedToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.17;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { ERC4626, ERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

// Cooldown logic forked from: https://github.com/aave/aave-stake-v2/blob/master/contracts/stake/StakedTokenV3.sol
contract StakedToken is ERC4626 {
uint256 public immutable COOLDOWN_SECONDS;
uint256 public immutable UNSTAKE_WINDOW;

mapping(address => uint256) public stakerCooldown;

error InsufficientCooldown();
error InvalidBalanceOnCooldown();
error UnstakeWindowFinished();

event Cooldown(address indexed sender, uint256 timestamp);

// ================================= FUNCTIONS =================================

constructor(
IERC20 asset_,
string memory name_,
string memory symbol_,
uint256 cooldownSeconds,
uint256 unstakeWindow
) ERC4626(asset_) ERC20(name_, symbol_) {
COOLDOWN_SECONDS = cooldownSeconds;
UNSTAKE_WINDOW = unstakeWindow;
}

function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
if (from == address(0)) {
// For a mint: we update the cooldown of the receiver if needed
stakerCooldown[to] = getNextCooldownTimestamp(0, amount, to, balanceOf(to));
} else if (to == address(0)) {
uint256 cooldownEndTimestamp = stakerCooldown[from] + COOLDOWN_SECONDS;
if (block.timestamp > cooldownEndTimestamp) revert InsufficientCooldown();
if (block.timestamp - cooldownEndTimestamp <= UNSTAKE_WINDOW) revert UnstakeWindowFinished();
} else if (from != to) {
uint256 previousSenderCooldown = stakerCooldown[from];
stakerCooldown[to] = getNextCooldownTimestamp(previousSenderCooldown, amount, to, balanceOf(to));
// if cooldown was set and whole balance of sender was transferred - clear cooldown
if (balanceOf(from) == amount && previousSenderCooldown != 0) {
stakerCooldown[from] = 0;
}
}
}

function getNextCooldownTimestamp(
uint256 fromCooldownTimestamp,
uint256 amountToReceive,
address toAddress,
uint256 toBalance
) public view returns (uint256 toCooldownTimestamp) {
toCooldownTimestamp = stakerCooldown[toAddress];
if (toCooldownTimestamp == 0) return 0;

uint256 minimalValidCooldownTimestamp = block.timestamp - COOLDOWN_SECONDS - UNSTAKE_WINDOW;

if (minimalValidCooldownTimestamp > toCooldownTimestamp) {
toCooldownTimestamp = 0;
} else {
fromCooldownTimestamp = (minimalValidCooldownTimestamp > fromCooldownTimestamp)
? block.timestamp
: fromCooldownTimestamp;

if (fromCooldownTimestamp >= toCooldownTimestamp) {
toCooldownTimestamp =
(amountToReceive * fromCooldownTimestamp + toBalance * toCooldownTimestamp) /
(amountToReceive + toBalance);
}
}
}

function cooldown() external {
if (balanceOf(msg.sender) != 0) revert InvalidBalanceOnCooldown();
stakerCooldown[msg.sender] = block.timestamp;
emit Cooldown(msg.sender, block.timestamp);
}
}

0 comments on commit 5fb3d94

Please sign in to comment.