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
/
RedeemableERC20ClaimEscrow.sol
476 lines (444 loc) · 22 KB
/
RedeemableERC20ClaimEscrow.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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
// SPDX-License-Identifier: CAL
pragma solidity =0.8.10;
import {RedeemableERC20} from "../redeemableERC20/RedeemableERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./TrustEscrow.sol";
/// Escrow contract for ERC20 tokens to be deposited and withdrawn against
/// redeemableERC20 tokens from a specific `Trust`.
///
/// When some token is deposited the running total of that token against the
/// trust is incremented by the deposited amount. When some `redeemableERC20`
/// token holder calls `withdraw` they are sent the full balance they have not
/// previously claimed, multiplied by their fraction of the redeemable token
/// supply that they currently hold. As redeemable tokens are frozen after
/// distribution there are no issues with holders manipulating withdrawals by
/// transferring tokens to claim multiple times.
///
/// As redeemable tokens can be burned it is possible for the total supply to
/// decrease over time, which naively would result in claims being larger
/// retroactively (prorata increases beyond what can be paid).
///
/// For example:
/// - Alice and Bob hold 50 rTKN each, 100 total supply
/// - 100 TKN is deposited
/// - Alice withdraws 50% of 100 TKN => alice holds 50 TKN escrow holds 50 TKN
/// - Alice burns her 50 rTKN
/// - Bob attempts to withdraw his 50 rTKN which is now 100% of supply
/// - Escrow tries to pay 100% of 100 TKN deposited and fails as the escrow
/// only holds 50 TKN (alice + bob = 150%).
///
/// To avoid the escrow allowing more withdrawals than deposits we include the
/// total rTKN supply in the key of each deposit mapping, and include it in the
/// emmitted event. Alice and Bob must read the events offchain and make a
/// withdrawal relative to the rTKN supply as it was at deposit time. Many
/// deposits can be made under a single rTKN supply and will all combine to a
/// single withdrawal but deposits made across different supplies will require
/// multiple withdrawals.
///
/// Alice or Bob could burn their tokens before withdrawing and would simply
/// withdraw zero or only some of the deposited TKN. This hurts them
/// individually, so they SHOULD check their indexer for claimable assets in
/// the escrow before considering a burn. But neither of them can cause the
/// other to be able to withdraw more or less relative to the supply as it was
/// at the time of TKN being deposited, or to trick the escrow into overpaying
/// more TKN than was deposited under a given `Trust`.
///
/// A griefer could attempt to flood the escrow with many dust deposits under
/// many different supplies in an attempt to confuse alice/bob. They are free
/// to filter out events in their indexer that come from an unknown depositor
/// or fall below some dust value threshold.
///
/// Tokens may also exit the escrow as an `undeposit` call where the depositor
/// receives back the tokens they deposited. As above the depositor must
/// provide the rTKN supply from `deposit` time in order to `undeposit`.
///
/// As `withdraw` and `undeposit` both represent claims on the same tokens they
/// are mutually exclusive outcomes, hence the need for an escrow. The escrow
/// will process `withdraw` only if the `Trust` is reporting a complete and
/// successful raise. Similarly `undeposit` will only return tokens after the
/// `Trust` completes and reports failure. While the `Trust` is in active
/// distribution neither `withdraw` or `undeposit` will move tokens. This is
/// necessary in part because it is only safe to calculate entitlements once
/// the redeemable tokens are fully distributed and frozen.
///
/// Because much of the redeemable token supply will never be sold, and then
/// burned, `depositPending` MUST be called rather than `deposit` while the
/// raise is active. When the raise completes anon can call `sweepPending`
/// which will calculate and emit a `Deposit` event for a useful `supply`.
///
/// Any supported ERC20 token can be deposited at any time BUT ONLY under a
/// `Trust` contract that is the child of the `TrustFactory` that the escrow
/// is deployed for. `TrustEscrow` is used to prevent a `Trust` from changing
/// the pass/fail outcome once it is known due to a bug/attempt to double
/// spend escrow funds.
///
/// This mechanism is very similar to the native burn mechanism on
/// `redeemableERC20` itself under `redeem` but without requiring any tokens to
/// be burned in the process. Users can claim the same token many times safely,
/// simply receiving 0 tokens if there is nothing left to claim.
///
/// This does NOT support rebase/elastic token _balance_ mechanisms on the
/// escrowed token as the escrow has no way to track deposits/withdrawals other
/// than 1:1 conservation of input/output. For example, if 100 tokens are
/// deposited under two different trusts and then that token rebases all
/// balances to half, there will be 50 tokens in the escrow but the escrow will
/// attempt transfers up to 100 tokens between the two trusts. Essentially the
/// first 50 tokens will send and the next 50 tokens will fail because the
/// trust literally doesn't have 100 tokens at that point.
///
/// Elastic _supply_ tokens are supported as every token to be withdrawn must
/// be first deposited, with the caveat that if some mechanism can
/// mint/burn/transfer tokens out from under the escrow contract directly, this
/// will break internal accounting much like the rebase situation.
///
/// Using a real-world example, stETH from LIDO would be NOT be supported as
/// the balance changes every day to reflect incoming ETH from validators, but
/// wstETH IS supported as balances remain static while the underlying assets
/// per unit of wstETH increase each day. This is of course exactly why wstETH
/// was created in the first place.
///
/// Every escrowed token has a separate space in the deposited/withdrawn
/// mappings so that some broken/malicious/hacked token that leads to incorrect
/// token movement in/out of the escrow cannot impact other tokens, even for
/// the same trust and redeemable.
contract RedeemableERC20ClaimEscrow is TrustEscrow {
using Math for uint256;
using SafeERC20 for IERC20;
/// Emitted for every successful pending deposit.
event PendingDeposit(
/// Anon `msg.sender` depositing the token.
address sender,
/// `Trust` contract deposit is under.
address trust,
/// Redeemable token that can claim this deposit.
/// Implicitly snapshots the redeemable so malicious `Trust` cannot
/// redirect funds later.
address redeemable,
/// `IERC20` token being deposited.
address token,
/// Amount of token deposited.
uint256 amount
);
/// Emitted every time a pending deposit is swept to a full deposit.
event Sweep(
/// Anon `msg.sender` sweeping the deposit.
address sender,
/// Anon `msg.sender` who originally deposited the token.
address depositor,
/// `Trust` contract deposit is under.
address trust,
/// Redeemable token first reported by the trust.
address redeemable,
/// `IERC20` token being swept into a deposit.
address token,
/// Amount of token being swept into a deposit.
uint256 amount
);
/// Emitted for every successful deposit.
event Deposit(
/// Anon `msg.sender` triggering the deposit.
/// MAY NOT be the `depositor` in the case of a pending sweep.
address sender,
/// Anon `msg.sender` who originally deposited the token.
/// MAY NOT be the current `msg.sender` in the case of a pending sweep.
address depositor,
/// `Trust` contract deposit is under.
address trust,
/// Redeemable token that can claim this deposit.
address redeemable,
/// `IERC20` token being deposited.
address token,
/// rTKN supply at moment of deposit.
uint256 supply,
/// Amount of token deposited.
uint256 amount
);
/// Emitted for every successful undeposit.
event Undeposit(
/// Anon `msg.sender` undepositing the token.
address sender,
/// `Trust` contract undeposit is from.
address trust,
/// Redeemable token that is being undeposited against.
address redeemable,
/// `IERC20` token being undeposited.
address token,
/// rTKN supply at moment of deposit.
uint256 supply,
/// Amount of token undeposited.
uint256 amount
);
/// Emitted for every successful withdrawal.
event Withdraw(
/// Anon `msg.sender` withdrawing the token.
address withdrawer,
/// `Trust` contract withdrawal is from.
address trust,
/// Redeemable token used to withdraw.
address redeemable,
/// `IERC20` token being withdrawn.
address token,
/// rTKN supply at moment of deposit.
uint256 supply,
/// Amount of token withdrawn.
uint256 amount
);
/// Every time an address calls `withdraw` their withdrawals increases to
/// match the current `totalDeposits` for that trust/token combination.
/// The token amount they actually receive is only their prorata share of
/// that deposited balance. The prorata scaling calculation happens inline
/// within the `withdraw` function.
/// trust => withdrawn token => rTKN supply => withdrawer => amount
// solhint-disable-next-line max-line-length
mapping(address => mapping(address => mapping(uint256 => mapping(address => uint256))))
internal withdrawals;
/// Deposits during an active raise are desirable to trustlessly prove to
/// raise participants that they will in fact be able to access the TKN
/// after the raise succeeds. Deposits during the pending stage are set
/// aside with no rTKN supply mapping, to be swept into a real deposit by
/// anon once the raise completes.
mapping(address => mapping(address => mapping(address => uint256)))
internal pendingDeposits;
/// Every time an address calls `deposit` their deposited trust/token
/// combination is increased. If they call `undeposit` when the raise has
/// failed they will receive the full amount they deposited back. Every
/// depositor must call `undeposit` for themselves.
/// trust => deposited token => depositor => rTKN supply => amount
// solhint-disable-next-line max-line-length
mapping(address => mapping(address => mapping(address => mapping(uint256 => uint256))))
internal deposits;
/// Every time an address calls `deposit` the amount is added to that
/// trust/token/supply combination. This increase becomes the
/// "high water mark" that withdrawals move up to with each `withdraw`
/// call.
/// trust => deposited token => rTKN supply => amount
mapping(address => mapping(address => mapping(uint256 => uint256)))
internal totalDeposits;
/// Redundant tracking of deposits withdrawn.
/// Counts aggregate deposits down as users withdraw, while their own
/// individual withdrawal counters count up.
/// Guards against buggy/malicious redeemable tokens that don't correctly
/// freeze their balances, hence opening up double spends.
/// trust => deposited token => rTKN supply => amount
mapping(address => mapping(address => mapping(uint256 => uint256)))
internal remainingDeposits;
/// Depositor can set aside tokens during pending raise status to be swept
/// into a real deposit later.
/// The problem with doing a normal deposit while the raise is still active
/// is that the `Trust` will burn all unsold tokens when the raise ends. If
/// we captured the token supply mid-raise then many deposited TKN would
/// be allocated to unsold rTKN. Instead we set aside TKN so that raise
/// participants can be sure that they will be claimable upon raise success
/// but they remain unbound to any rTKN supply until `sweepPending` is
/// called.
/// `depositPending` is a one-way function, there is no way to `undeposit`
/// until after the raise fails. Strongly recommended that depositors do
/// NOT call `depositPending` until raise starts, so they know it will also
/// end.
/// @param trust_ The `Trust` to assign this deposit to.
/// @param token_ The `IERC20` token to deposit to the escrow.
/// @param amount_ The amount of token to despoit. Requires depositor has
/// approved at least this amount to succeed.
function depositPending(
address trust_,
address token_,
uint256 amount_
) external {
require(amount_ > 0, "ZERO_DEPOSIT");
require(escrowStatus(trust_) == EscrowStatus.Pending, "NOT_PENDING");
pendingDeposits[trust_][token_][msg.sender] += amount_;
// Important to snapshot the token from the trust here so it can't be
// changed later by the trust.
address redeemable_ = token(trust_);
emit PendingDeposit(msg.sender, trust_, redeemable_, token_, amount_);
IERC20(token_).safeTransferFrom(msg.sender, address(this), amount_);
}
/// Internal accounting for a deposit.
/// Identical for both a direct deposit and sweeping a pending deposit.
function registerDeposit(
address trust_,
address token_,
address depositor_,
uint256 amount_
) private {
require(escrowStatus(trust_) > EscrowStatus.Pending, "PENDING");
require(amount_ > 0, "ZERO_DEPOSIT");
address redeemable_ = token(trust_);
uint256 supply_ = IERC20(redeemable_).totalSupply();
deposits[trust_][token_][depositor_][supply_] += amount_;
totalDeposits[trust_][token_][supply_] += amount_;
remainingDeposits[trust_][token_][supply_] += amount_;
emit Deposit(
msg.sender,
depositor_,
trust_,
redeemable_,
token_,
supply_,
amount_
);
}
/// Anon can convert any existing pending deposit to a deposit with known
/// rTKN supply once the escrow has moved out of pending status.
/// As `sweepPending` is anon callable, raise participants know that the
/// depositor cannot later prevent a sweep, and depositor knows that raise
/// participants cannot prevent a sweep. As per normal deposits, the output
/// of swept tokens depends on success/fail state allowing `undeposit` or
/// `withdraw` to be called subsequently.
/// Partial sweeps are NOT supported, to avoid griefers splitting a deposit
/// across many different `supply_` values.
function sweepPending(
address trust_,
address token_,
address depositor_
) external {
uint256 amount_ = pendingDeposits[trust_][token_][depositor_];
delete pendingDeposits[trust_][token_][depositor_];
emit Sweep(
msg.sender,
depositor_,
trust_,
token(trust_),
token_,
amount_
);
registerDeposit(trust_, token_, depositor_, amount_);
}
/// Any address can deposit any amount of its own `IERC20` under a `Trust`.
/// The `Trust` MUST be a child of the trusted factory.
/// The deposit will be accounted for under both the depositor individually
/// and the trust in aggregate. The aggregate value is used by `withdraw`
/// and the individual value by `undeposit`.
/// The depositor is responsible for approving the token for this contract.
/// `deposit` is still enabled after the distribution ends; `undeposit` is
/// always allowed in case of a fail and disabled on success. Multiple
/// `deposit` calls before and after a success result are supported. If a
/// depositor deposits when a raise has failed they will need to undeposit
/// it again manually.
/// Delegated `deposit` is not supported. Every depositor is directly
/// responsible for every `deposit`.
/// WARNING: As `undeposit` can only be called when the `Trust` reports
/// failure, `deposit` should only be called when the caller is sure the
/// `Trust` will reach a clear success/fail status. For example, when a
/// `Trust` has not yet been seeded it may never even start the raise so
/// depositing at this point is dangerous. If the `Trust` never starts the
/// raise it will never fail the raise either.
/// @param trust_ The `Trust` to assign this deposit to.
/// @param token_ The `IERC20` token to deposit to the escrow.
/// @param amount_ The amount of token to deposit. Requires depositor has
/// approved at least this amount to succeed.
function deposit(
address trust_,
address token_,
uint256 amount_
) external {
registerDeposit(trust_, token_, msg.sender, amount_);
IERC20(token_).safeTransferFrom(msg.sender, address(this), amount_);
}
/// The inverse of `deposit`.
/// In the case of a failed distribution the depositors can claim back any
/// tokens they deposited in the escrow.
/// Ideally the distribution is a success and this does not need to be
/// called but it is important that we can walk back deposits and try again
/// for some future raise if needed.
/// Delegated `undeposit` is not supported, only the depositor can wind
/// back their original deposit.
/// `amount_` must be non-zero.
/// If several tokens have been deposited against a given trust for the
/// depositor then each token must be individually undeposited. There is
/// no onchain tracking or bulk processing for the depositor, they are
/// expected to know what they have previously deposited and if/when to
/// process an `undeposit`.
/// @param trust_ The `Trust` to undeposit from.
/// @param token_ The token to undeposit.
function undeposit(
address trust_,
address token_,
uint256 supply_,
uint256 amount_
) external {
// Can only undeposit when the `Trust` reports failure.
require(escrowStatus(trust_) == EscrowStatus.Fail, "NOT_FAIL");
require(amount_ > 0, "ZERO_AMOUNT");
deposits[trust_][token_][msg.sender][supply_] -= amount_;
// Guard against outputs exceeding inputs.
// Last undeposit gets a gas refund.
totalDeposits[trust_][token_][supply_] -= amount_;
remainingDeposits[trust_][token_][supply_] -= amount_;
emit Undeposit(
msg.sender,
trust_,
// Include this in the event so that indexer consumers see a
// consistent world view even if the trust_ changes its answer
// about the redeemable.
token(trust_),
token_,
supply_,
amount_
);
IERC20(token_).safeTransfer(msg.sender, amount_);
}
/// The successful handover of a `deposit` to a recipient.
/// When a redeemable token distribution is successful the redeemable token
/// holders are automatically and immediately eligible to `withdraw` any
/// and all tokens previously deposited against the relevant `Trust`.
/// The `withdraw` can only happen if/when the relevant `Trust` reaches the
/// success distribution status.
/// Delegated `withdraw` is NOT supported. Every redeemable token holder is
/// directly responsible for being aware of and calling `withdraw`.
/// If a redeemable token holder calls `redeem` they also burn their claim
/// on any tokens held in escrow so they MUST first call `withdraw` THEN
/// `redeem`.
/// It is expected that the redeemable token holder knows about the tokens
/// that they will be withdrawing. This information is NOT tracked onchain
/// or exposed for bulk processing.
/// Partial `withdraw` is not supported, all tokens allocated to the caller
/// are withdrawn`. 0 amount withdrawal is an error, if the prorata share
/// of the token being claimed is small enough to round down to 0 then the
/// withdraw will revert.
/// Multiple withdrawals across multiple deposits is supported and is
/// equivalent to a single withdraw after all relevant deposits.
/// @param trust_ The trust to `withdraw` against.
/// @param token_ The token to `withdraw`.
function withdraw(
address trust_,
address token_,
uint256 supply_
) external {
// Can only withdraw when the `Trust` reports success.
require(escrowStatus(trust_) == EscrowStatus.Success, "NOT_SUCCESS");
uint256 totalDeposited_ = totalDeposits[trust_][token_][supply_];
uint256 withdrawn_ = withdrawals[trust_][token_][supply_][msg.sender];
RedeemableERC20 redeemable_ = RedeemableERC20(token(trust_));
withdrawals[trust_][token_][supply_][msg.sender] = totalDeposited_;
//solhint-disable-next-line max-line-length
uint256 amount_ = (// Underflow MUST error here (should not be possible).
(totalDeposited_ - withdrawn_) *
// prorata share of `msg.sender`'s current balance vs. supply
// as at the time deposit was made. If nobody burns they will
// all get a share rounded down by integer division. 100 split
// 3 ways will be 33 tokens each, leaving 1 TKN as escrow dust,
// for example. If someone burns before withdrawing they will
// receive less, so 0/33/33 from 100 with 34 TKN as escrow
// dust, for example.
redeemable_.balanceOf(msg.sender)) / supply_;
// Guard against outputs exceeding inputs.
// For example a malicious `Trust` could report a `redeemable_` token
// that does NOT freeze balances. In this case token holders can double
// spend their withdrawals by simply shuffling the same token around
// between accounts.
remainingDeposits[trust_][token_][supply_] -= amount_;
require(amount_ > 0, "ZERO_WITHDRAW");
emit Withdraw(
msg.sender,
trust_,
address(redeemable_),
token_,
supply_,
amount_
);
IERC20(token_).safeTransfer(msg.sender, amount_);
}
}