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

fix: radiant PoC #61

Merged
merged 7 commits into from
Apr 29, 2024
Merged
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
130 changes: 130 additions & 0 deletions contracts/tokenWrappers/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) {}
}
71 changes: 71 additions & 0 deletions contracts/tokenWrappers/BaseTokenWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

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

interface IDistributionCreator {
function distributor() external view returns (address);
function feeRecipient() external view returns (address);

Check failure on line 13 in contracts/tokenWrappers/BaseTokenWrapper.sol

View workflow job for this annotation

GitHub Actions / lint

Insert ⏎
}

abstract contract BaseMerklTokenWrapper is UUPSHelper, ERC20Upgradeable {
using SafeERC20 for IERC20;

// ================================= CONSTANTS =================================

IDistributionCreator public constant DISTRIBUTOR_CREATOR =
IDistributionCreator(0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd);

address public immutable DISTRIBUTOR = DISTRIBUTOR_CREATOR.distributor();
address public immutable FEE_RECIPIENT = DISTRIBUTOR_CREATOR.feeRecipient();

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

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

// =================================== 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 token() public view virtual returns (address);

function isTokenWrapper() external pure returns (bool) {
return true;
}

function initialize(ICore _core) public initializer onlyProxy {
__ERC20_init(
string.concat("Merkl Token Wrapper - ", IERC20Metadata(token()).name()),
string.concat("mtw", IERC20Metadata(token()).symbol())
);
__UUPSUpgradeable_init();
if (address(_core) == address(0)) revert ZeroAddress();
core = _core;
}

/// @notice Recovers any ERC20 token
/// @dev Governance only, to trigger only if something went wrong
function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernor {
IERC20(tokenAddress).safeTransfer(to, amountToRecover);
emit Recovered(tokenAddress, to, amountToRecover);
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal view override onlyGovernorUpgrader(core) {}
}
63 changes: 63 additions & 0 deletions contracts/tokenWrappers/RadiantTokenWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

import { BaseMerklTokenWrapper } from "./BaseTokenWrapper.sol";

interface IVesting {
function rdntToken() external view returns (address);
function vestTokens(address, uint256, bool) external returns (address);

Check failure on line 13 in contracts/tokenWrappers/RadiantTokenWrapper.sol

View workflow job for this annotation

GitHub Actions / lint

Insert ⏎
}

/// @title Radiant MTW
/// @dev This token can only be held by merkl distributor
/// @dev Transferring to the distributor will require transferring the underlying token to this contract
/// @dev Transferring from the distributor will trigger vesting action
/// @dev Transferring token to the distributor is permissionless so anyone could mint this wrapper - the only
/// impact would be to forfeit these tokens
contract RadiantMerklTokenWrapper is BaseMerklTokenWrapper {
using SafeERC20 for IERC20;

// ================================= CONSTANTS =================================

IVesting public constant VESTING = IVesting(0x76ba3eC5f5adBf1C58c91e86502232317EeA72dE);
address internal immutable _UNDERLYING = VESTING.rdntToken();

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

function token() public view override returns (address) {
return _UNDERLYING;
}

function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
// Needs an RDNT approval beforehand, this is how mints of coupons are done
if (to == DISTRIBUTOR) {
IERC20(_UNDERLYING).safeTransferFrom(from, address(this), amount);
_mint(from, amount); // These are then transferred to the distributor
}

// Will be burn right after, to avoid having any token aside from on the distributor
if (to == FEE_RECIPIENT) {
IERC20(_UNDERLYING).safeTransferFrom(from, FEE_RECIPIENT, amount);
_mint(from, amount); // These are then transferred to the fee manager
}
}

function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
if (to == FEE_RECIPIENT) {
_burn(to, amount); // To avoid having any token aside from on the distributor
}

if (from == DISTRIBUTOR) {
_burn(to, amount);

// Vesting logic
IERC20(_UNDERLYING).transfer(address(VESTING), amount);
VESTING.vestTokens(to, amount, true);
}
}
}
83 changes: 83 additions & 0 deletions contracts/tokenWrappers/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);
}
}
44 changes: 44 additions & 0 deletions deploy/coupon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { DistributionCreator__factory } from '@angleprotocol/sdk';
import { DeployFunction } from 'hardhat-deploy/types';
import yargs from 'yargs';
import { Distributor__factory } from '../typechain';

const argv = yargs.env('').boolean('ci').parseSync();

const func: DeployFunction = async ({ deployments, ethers, network }) => {
const { deploy } = deployments;
const { deployer } = await ethers.getNamedSigners();

const couponName = 'RadiantMerklTokenWrapper';
const distributionCreator = DistributionCreator__factory.connect('0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd', deployer)
const core = await distributionCreator.core()

console.log(`Deploying Coupon`);
console.log('Starting with the implementation');

await deploy(`${couponName}_Implementation`, {
contract: couponName,
from: deployer.address,
args: [],
log: !argv.ci,
});

const implementationAddress = (await ethers.getContract(`${couponName}_Implementation`)).address;

console.log('Starting with the implementation');

const distributorInterface = Distributor__factory.createInterface();

await deploy(`${couponName}_Proxy`, {
contract: 'ERC1967Proxy',
from: deployer.address,
args: [implementationAddress, distributorInterface.encodeFunctionData('initialize', [core])],
log: !argv.ci,
});

console.log(`Successfully deployed the contract ${couponName} at ${implementationAddress}`);
console.log('');
};

func.tags = ['mtw'];
export default func;
Loading
Loading