Skip to content

Commit 13ad58d

Browse files
committed
add FXN Convex Escrow and test
1 parent e478d5b commit 13ad58d

File tree

2 files changed

+448
-0
lines changed

2 files changed

+448
-0
lines changed

src/escrows/FXNConvexEscrow.sol

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
5+
6+
interface IBooster {
7+
function createVault(
8+
uint256 _pid
9+
) external returns (address);
10+
}
11+
12+
interface IVault {
13+
function deposit(uint256 _amount) external;
14+
function withdraw(uint256 _shares) external;
15+
function stakingToken() external view returns (address);
16+
function gaugeAddress() external view returns (address);
17+
function getReward() external;
18+
}
19+
20+
interface IGauge {
21+
function getActiveRewardTokens() external view returns (address[] memory);
22+
}
23+
/**
24+
* @notice Escrow contract implementation to stake Curve LP tokens in FXN Convex and claim FXN and other rewards
25+
*/
26+
27+
contract FXNConvexEscrow {
28+
using SafeERC20 for IERC20;
29+
30+
error AlreadyInitialized();
31+
error OnlyMarket();
32+
error OnlyBeneficiary();
33+
error OnlyBeneficiaryOrAllowlist();
34+
35+
uint256 public immutable pid;
36+
37+
IBooster public immutable booster;
38+
IERC20 public immutable fxn;
39+
40+
IERC20 public gauge;
41+
IVault public vault;
42+
address public market;
43+
IERC20 public token;
44+
address public beneficiary;
45+
46+
mapping(address => bool) public allowlist;
47+
48+
modifier onlyBeneficiary() {
49+
if (msg.sender != beneficiary) revert OnlyBeneficiary();
50+
_;
51+
}
52+
53+
modifier onlyBeneficiaryOrAllowlist() {
54+
if (msg.sender != beneficiary && !allowlist[msg.sender])
55+
revert OnlyBeneficiaryOrAllowlist();
56+
_;
57+
}
58+
59+
event AllowClaim(address indexed allowedAddress, bool allowed);
60+
61+
constructor(
62+
address _booster,
63+
address _fxn,
64+
uint256 _pid
65+
) {
66+
booster = IBooster(_booster);
67+
fxn = IERC20(_fxn);
68+
pid = _pid;
69+
}
70+
71+
/**
72+
@notice Initialize escrow with a token
73+
@dev Must be called right after proxy is created.
74+
@param _token The IERC20 token representing LP token to be staked
75+
@param _beneficiary The beneficiary who the token is staked on behalf
76+
*/
77+
function initialize(IERC20 _token, address _beneficiary) public {
78+
if (market != address(0)) revert AlreadyInitialized();
79+
market = msg.sender;
80+
token = _token;
81+
beneficiary = _beneficiary;
82+
vault = IVault(booster.createVault(pid));
83+
token.approve(address(vault), type(uint).max);
84+
require(token == IERC20(vault.stakingToken()), "Wrong token");
85+
gauge = IERC20(vault.gaugeAddress()); // receipt token that goes into the vault
86+
}
87+
88+
/**
89+
@notice Withdraws the receipt token from the vault and transfers the associated ERC20 token to a recipient.
90+
@dev Will first try to pay from the escrow balance, if not enough or any, will try to pay the missing amount withdrawing from the vault.
91+
@param recipient The address to receive payment from the escrow
92+
@param amount The amount of ERC20 token to be transferred.
93+
*/
94+
function pay(address recipient, uint amount) public {
95+
if (msg.sender != market) revert OnlyMarket();
96+
uint256 tokenBal = token.balanceOf(address(this));
97+
98+
if (tokenBal >= amount) {
99+
token.safeTransfer(recipient, amount);
100+
return;
101+
}
102+
103+
104+
uint256 gaugeBalance = gauge.balanceOf(address(vault));
105+
if (gaugeBalance > 0) {
106+
uint256 missingAmount = amount - tokenBal;
107+
uint256 withdrawAmount = gaugeBalance > missingAmount
108+
? missingAmount
109+
: gaugeBalance;
110+
111+
vault.withdraw(withdrawAmount);
112+
}
113+
114+
token.safeTransfer(recipient, amount);
115+
}
116+
117+
/**
118+
@notice Get the token balance of the escrow
119+
@return Uint representing the token balance of the escrow
120+
*/
121+
function balance() public view returns (uint256) {
122+
return
123+
gauge.balanceOf(address(vault)) +
124+
token.balanceOf(address(this));
125+
}
126+
127+
/**
128+
@notice Function called by market on deposit. Stakes deposited collateral into FXN Convex gauge via vault
129+
@dev This function should remain callable by anyone to handle direct inbound transfers.
130+
*/
131+
function onDeposit() public {
132+
uint256 tokenBal = token.balanceOf(address(this));
133+
if (tokenBal == 0) return;
134+
vault.deposit(tokenBal);
135+
}
136+
137+
/**
138+
@notice Claims reward tokens to the specified address. Only callable by beneficiary and allowlisted addresses
139+
@param to Address to send claimed rewards to
140+
*/
141+
function claimTo(address to) public onlyBeneficiaryOrAllowlist {
142+
//Claim rewards
143+
vault.getReward();
144+
//Send fxn balance
145+
uint256 fxnBal = fxn.balanceOf(address(this));
146+
if (fxnBal != 0) fxn.safeTransfer(to, fxnBal);
147+
148+
//Send contract balance of extra rewards (including CRV and CVX)
149+
IGauge fxnGauge = IGauge(address(gauge));
150+
address[] memory extraRewards = fxnGauge.getActiveRewardTokens();
151+
if (extraRewards.length == 0) return;
152+
for (uint256 i; i < extraRewards.length; ++i) {
153+
// Avoid sending collateral token if it is added as a reward
154+
if (extraRewards[i] == address(token)) continue;
155+
uint256 rewardBal = IERC20(extraRewards[i]).balanceOf(address(this));
156+
if (rewardBal > 0) {
157+
//Use safe transfer in case bad reward token is added
158+
IERC20(extraRewards[i]).safeTransfer(to, rewardBal);
159+
}
160+
}
161+
}
162+
163+
164+
/**
165+
@notice Claims reward tokens to the message sender. Only callable by beneficiary
166+
*/
167+
function claim() external onlyBeneficiary {
168+
claimTo(msg.sender);
169+
}
170+
171+
/**
172+
@notice Allow address to claim on behalf of the beneficiary to any address
173+
@param allowee Address that are allowed to claim on behalf of the beneficiary
174+
@dev Can only be called by the beneficiary
175+
*/
176+
function allowClaimOnBehalf(address allowee) external onlyBeneficiary {
177+
allowlist[allowee] = true;
178+
emit AllowClaim(allowee, true);
179+
}
180+
181+
/**
182+
@notice Disallow address to claim on behalf of the beneficiary to any address
183+
@param allowee Address that are disallowed to claim on behalf of the beneficiary
184+
*/
185+
function disallowClaimOnBehalf(address allowee) external onlyBeneficiary {
186+
allowlist[allowee] = false;
187+
emit AllowClaim(allowee, false);
188+
}
189+
}

0 commit comments

Comments
 (0)