From 5fb3d945e39f2c5db1137066a5671a7184e5cb4f Mon Sep 17 00:00:00 2001 From: Pablo Veyrat <50438397+sogipec@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:28:54 +0200 Subject: [PATCH] Aave poc (#62) * v1 of Merkl contract * feat: poc for contract * remove feeManager * feat: cooldown contract * feat: handle fee recipient --- contracts/coupons/AaveTokenWrapper.sol | 130 +++++++++++++++++++++++++ contracts/coupons/RadiantCoupon.sol | 7 +- contracts/coupons/StakedToken.sol | 83 ++++++++++++++++ 3 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 contracts/coupons/AaveTokenWrapper.sol create mode 100644 contracts/coupons/StakedToken.sol diff --git a/contracts/coupons/AaveTokenWrapper.sol b/contracts/coupons/AaveTokenWrapper.sol new file mode 100644 index 0000000..2d5d895 --- /dev/null +++ b/contracts/coupons/AaveTokenWrapper.sol @@ -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) {} +} diff --git a/contracts/coupons/RadiantCoupon.sol b/contracts/coupons/RadiantCoupon.sol index 8e7ee21..8874d1c 100644 --- a/contracts/coupons/RadiantCoupon.sol +++ b/contracts/coupons/RadiantCoupon.sol @@ -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"; @@ -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 @@ -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 } @@ -66,3 +66,4 @@ contract RadiantCoupon is UUPSHelper, ERC20Upgradeable { /// @inheritdoc UUPSUpgradeable function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {} } +*/ \ No newline at end of file diff --git a/contracts/coupons/StakedToken.sol b/contracts/coupons/StakedToken.sol new file mode 100644 index 0000000..5fe5312 --- /dev/null +++ b/contracts/coupons/StakedToken.sol @@ -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); + } +}