Skip to content

Commit

Permalink
Merge branch 'main' into feat/mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Picodes authored May 3, 2024
2 parents d21f0a7 + 4f414bf commit 2cc9aa8
Show file tree
Hide file tree
Showing 23 changed files with 5,308 additions and 2 deletions.
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);
}

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);
}

/// @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

0 comments on commit 2cc9aa8

Please sign in to comment.