This repository has been archived by the owner on Dec 7, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
/
BPoolFeeEscrow.sol
313 lines (292 loc) · 14.3 KB
/
BPoolFeeEscrow.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
// SPDX-License-Identifier: CAL
pragma solidity =0.8.10;
import {IBPool} from "../pool/IBPool.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IConfigurableRightsPool} from "../pool/IConfigurableRightsPool.sol";
import "./TrustEscrow.sol";
/// Represents fees as they are claimed by a recipient on a per-trust basis.
/// Used to work around a limitation in the EVM i.e. return values must be
/// structured this way in a dynamic length array when bulk-claiming.
struct ClaimedFees {
// The trust that fees were claimed for.
address trust;
// The amount of fees that were claimed.
// This is denominated in the token claimed.
uint256 claimedFees;
}
/// Escrow contract for fees IN ADDITION TO BPool fees.
/// The goal is to set aside some revenue for curators, infrastructure, and
/// anyone else who can convince an end-user to part with some extra tokens and
/// gas for escrow internal accounting on top of the basic balancer swap. This
/// is Rain's "pay it forward" revenue model, rather than trying to capture and
/// pull funds back to the protocol somehow. The goal is to incentivise many
/// ecosystems that are nourished by Rain but are not themselves Rain.
///
/// Technically this might look like a website/front-end prefilling an address
/// the maintainers own and some nominal fee like 1% of each trade. The fee is
/// in absolute numbers on this contract but a GUI is free to calculate this
/// value in any way it deems appropriate. The assumption is that end-users of
/// a GUI will not manually alter the fee, because if they would do that it
/// makes more sense that they would simply call the balancer swap function
/// directly and avoid even paying the gas required by the escrow contract.
///
/// Balancer pool fees natively set aside prorata for LPs ONLY. Our `Trust`
/// requires that 100% of the LP tokens and token supply are held by the
/// managing pool contract that the `Trust` deploys. Naively we could set a
/// fee on the balancer pool and have the contract that owns the LP tokens
/// attempt to divvy the volume fees out to FEs from some registry. The issue
/// is that the Balancer contracts are all outside our control so we have no
/// way to prevent a malicious end-user or FE lying about how they interact
/// with the Balancer pool. The only way to ensure that every trade accurately
/// sets aside fees is to put a contract in between the buyer and the pool
/// that can execute the trade sans fees on the buyers's behalf.
///
/// Some important things to note about fee handling:
/// - Fees are NOT forwarded if the raise fails according to the Trust. Instead
/// they are forwarded to the redeemable token so buyers can redeem a refund.
/// - Fees are ONLY collected when tokens are purchased, thus contributing to
/// the success of a raise. When tokens are sold there are no additional fees
/// set aside by this escrow. Repeatedly buying/selling does NOT allow for
/// wash trading to claim additional fees as the user must pay the fee in
/// full in addition to the token spot price for every round-trip.
/// - ANYONE can process a claim for a recipient and/or a refund for a trust.
/// - The information about which trusts to claim/refund is available offchain
/// via the `Fee` event.
///
/// We cannot prevent FEs implementing their own smart contracts to take fees
/// outside the scope of the escrow, but we aren't encouraging or implementing
/// it for them either.
contract BPoolFeeEscrow is TrustEscrow {
using SafeERC20 for IERC20;
/// A claim has been processed for a recipient.
/// ONLY emitted if non-zero fees were claimed.
event ClaimFees(
/// Anon who processed the claim.
address sender,
/// Recipient of the fees.
address recipient,
/// Trust the fees were collected for.
address trust,
/// Reserve token first reported by the `Trust`.
address reserve,
/// Amount of fees claimed.
uint256 claimedFees
);
/// A refund has been processed for a `Trust`.
/// ONLY emitted if non-zero fees were refunded.
event RefundFees(
/// Anon who processed the refund.
address sender,
/// `Trust` the fees were refunded to.
/// Fees go to the redeemable token, not the `Trust` itself.
address trust,
/// Reserve token first reported by the `Trust`.
address reserve,
/// Redeemable token first reported by the `Trust`.
address redeemable,
/// Amount of fees refunded.
uint256 refundedFees
);
/// A fee has been set aside for a recipient.
event Fee(
/// Anon who sent fees.
address sender,
/// Recipient of the fee.
address recipient,
/// `Trust` the fee was set aside for.
address trust,
/// Reserve token first reported by the `Trust`.
address reserve,
/// Redeemable token first reported by the `Trust`.
address redeemable,
/// Amount of fee denominated in the reserve asset of the `trust`.
uint256 fee
);
/// Fees set aside under a trust for a specific recipient.
/// Denominated in the reserve asset of the trust.
/// There can be many recipients for a single trust.
/// Fees are forwarded to each recipient when they claim. The recipient
/// receives all the fees collected under a trust in a single claim.
/// Fee claims are mutually exclusive with refund claims.
/// trust => recipient => amount
mapping(address => mapping(address => uint256)) internal fees;
/// Refunds for a trust are the same as the sum of all its fees.
/// Denominated in the reserve asset of the trust.
/// Refunds are forwarded to the raise token created by the trust.
/// Refunds are mutually exclusive with any fee claims.
/// All fees are forwarded to the same token address which is singular per
/// trust.
/// trust => amount
mapping(address => uint256) internal totalFees;
/// Anon can pay the gas to send all claimable fees to any recipient.
/// Caller is expected to infer profitable trusts for the recipient by
/// parsing the event log for `Fee` events. Caller pays gas and there is no
/// benefit to not claiming fees, so anon can claim for any recipient.
/// Claims are processed on a per-trust basis.
/// Processing a claim before the trust distribution has reached either a
/// success/fail state is an error.
/// Processing a claim for a failed distribution simply deletes the record
/// of claimable fees for the recipient without sending tokens.
/// Processing a claim for a successful distribution transfers the accrued
/// fees to the recipient (and deletes the record for gas refund).
/// Partial claims are NOT supported, to avoid anon griefing claims by e.g.
/// claiming 95% of a recipient's value, leaving dust behind that isn't
/// worth the gas to claim, but meaningfully haircut's the recipients fees.
/// A 0 value claim is a noop rather than error, to make it possible to
/// write a batch claim wrapper that cannot be griefed. E.g. anon claims N
/// trusts and then another anon claims 1 of these trusts with higher gas,
/// causing the batch transaction to revert.
/// @param recipient_ The recipient of the fees.
/// @param trust_ The trust to process claims for. SHOULD be a child of the
/// a trusted `TrustFactory`.
/// @return The fees claimed.
function claimFees(address recipient_, address trust_)
public
returns (uint256)
{
EscrowStatus escrowStatus_ = escrowStatus(trust_);
require(escrowStatus_ == EscrowStatus.Success, "NOT_SUCCESS");
uint256 amount_ = fees[trust_][recipient_];
// Zero `amount_` is noop not error.
// Allows batch wrappers to be written that cannot be front run
// and reverted.
if (amount_ > 0) {
// Guard against outputs exceeding inputs.
// Last `receipient_` gets gas refund.
totalFees[trust_] -= amount_;
// Gas refund.
delete fees[trust_][recipient_];
address reserve_ = reserve(trust_);
emit ClaimFees(msg.sender, recipient_, trust_, reserve_, amount_);
IERC20(reserve_).safeTransfer(recipient_, amount_);
}
return amount_;
}
/// Anon can pay the gas to refund fees for a `Trust`.
/// Refunding forwards the fees as `Trust` reserve to its redeemable token.
/// Refunding does NOT directly return fees to the sender nor directly to
/// the `Trust`.
/// The refund will forward all fees collected if and only if the raise
/// failed, according to the `Trust`.
/// This can be called many times but a failed raise will only have fees to
/// refund once. Subsequent calls will be a noop if there is `0` refundable
/// value remaining.
///
/// @param trust_ The `Trust` to refund for. This MSHOULDUST be a child of
/// a trusted `TrustFactory`.
/// @return The total refund.
function refundFees(address trust_) external returns (uint256) {
EscrowStatus escrowStatus_ = escrowStatus(trust_);
require(escrowStatus_ == EscrowStatus.Fail, "NOT_FAIL");
uint256 amount_ = totalFees[trust_];
// Zero `amount_` is noop not error.
// Allows batch wrappers to be written that cannot be front run
// and reverted.
if (amount_ > 0) {
// Gas refund.
delete totalFees[trust_];
address reserve_ = reserve(trust_);
address token_ = token(trust_);
emit RefundFees(msg.sender, trust_, reserve_, token_, amount_);
IERC20(reserve_).safeTransfer(token_, amount_);
}
return amount_;
}
/// Unidirectional wrapper around `swapExactAmountIn` for 'buying tokens'.
/// In this context, buying tokens means swapping the reserve token IN to
/// the underlying balancer pool and withdrawing the minted token OUT.
///
/// The main goal is to establish a convention for front ends that drive
/// traffic to a raise to collect some fee from each token purchase. As
/// there could be many front ends for a single raise, and the fees are
/// based on volume, the safest thing to do is to set aside the fees at the
/// source in an escrow and allow each receipient to claim their fees when
/// ready. This avoids issues like wash trading to siphon fees etc.
///
/// The end-user 'chooses' (read: The FE sets the parameters for them) a
/// recipient (the FE) and fee to be _added_ to their trade.
///
/// Of course, the end-user can 'simply' bypass the `buyToken` function
/// call and interact with the pool themselves, but if a client front-end
/// presents this to a user it's most likely they will just use it.
///
/// This function does a lot of heavy lifting:
/// - Ensure the `Trust` is a child of the factory this escrow is bound to
/// - Internal accounting to track fees for the fee recipient
/// - Ensure the fee meets the minimum requirements of the receiver
/// - Taking enough reserve tokens to cover the trade and the fee
/// - Poking the weights on the underlying pool to ensure the best price
/// - Performing the trade and forwading the token back to the caller
///
/// Despite the additional "hop" with the escrow sitting between the user
/// and the pool this function is similar or even cheaper gas than the
/// user poking, trading and setting aside a fee as separate actions.
///
/// @param feeRecipient_ The recipient of the fee as `Trust` reserve.
/// @param trust_ The `Trust` to buy tokens from. This `Trust` SHOULD be
/// known as a child of a trusted `TrustFactory`.
/// @param fee_ The amount of the fee.
/// @param reserveAmountIn_ As per balancer.
/// @param minTokenAmountOut_ As per balancer.
/// @param maxPrice_ As per balancer.
function buyToken(
address feeRecipient_,
address trust_,
uint256 fee_,
uint256 reserveAmountIn_,
uint256 minTokenAmountOut_,
uint256 maxPrice_
) external returns (uint256 tokenAmountOut, uint256 spotPriceAfter) {
// Zero fee makes no sense, simply call `swapExactAmountIn` directly
// rather than using the escrow.
require(fee_ > 0, "ZERO_FEE");
require(escrowStatus(trust_) == EscrowStatus.Pending, "ENDED");
fees[trust_][feeRecipient_] += fee_;
totalFees[trust_] += fee_;
// A bad reserve could set itself up to be drained from the escrow, but
// cannot interfere with other reserve balances.
// e.g. rebasing reserves are NOT supported.
// A bad token could fail to send itself to `msg.sender` which doesn't
// hurt the escrow.
// A bad crp or pool is not approved to touch escrow fees, only the
// `msg.sender` funds.
address reserve_ = reserve(trust_);
address token_ = token(trust_);
IConfigurableRightsPool crp_ = IConfigurableRightsPool(crp(trust_));
address pool_ = crp_.bPool();
emit Fee(
msg.sender,
feeRecipient_,
trust_,
reserve_,
token_,
fee_
);
crp_.pokeWeights();
// These two calls are to the reserve, which we do NOT know or have any
// control over. Even a well known `Trust` can set a badly behaved
// reserve.
IERC20(reserve_).safeTransferFrom(
msg.sender,
address(this),
fee_ + reserveAmountIn_
);
// The pool is never approved for anything other than this swap so we
// can set the allowance directly rather than increment it.
IERC20(reserve_).safeApprove(pool_, reserveAmountIn_);
// Perform the swap sans fee.
(uint256 tokenAmountOut_, uint256 spotPriceAfter_) = IBPool(pool_)
.swapExactAmountIn(
reserve_,
reserveAmountIn_,
token_,
minTokenAmountOut_,
maxPrice_
);
// Return the result of the swap to `msg.sender`.
IERC20(token_).safeTransfer(msg.sender, tokenAmountOut_);
// Mimic return signature of `swapExactAmountIn`.
return ((tokenAmountOut_, spotPriceAfter_));
}
}