Skip to content

Commit 64b8a41

Browse files
Brechtpdwangdongdong77
authored
[protocol 3.6] Alternative fast withdrawal implementation (#1715)
* [protocol 3.6] Alternative fast withdrawal implementation * [protocol 3.6] Feedback * fix warning and rename file * fix warning and rename file * fix warning and rename file * more * more * more * more * more Co-authored-by: wangdong <[email protected]> Co-authored-by: Daniel Wang <[email protected]>
1 parent 0627946 commit 64b8a41

File tree

9 files changed

+313
-173
lines changed

9 files changed

+313
-173
lines changed

packages/loopring_v3/circuit/Circuits/TransferCircuit.h

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,7 @@ class TransferCircuit : public BaseTransactionCircuit
179179
validUntil.packed,
180180
NUM_BITS_TIMESTAMP,
181181
FMT(prefix, ".requireValidUntil")),
182-
requireValidFee(
183-
pb,
184-
fee.packed,
185-
maxFee.packed,
186-
NUM_BITS_AMOUNT,
187-
FMT(prefix, ".requireValidFee")),
182+
requireValidFee(pb, fee.packed, maxFee.packed, NUM_BITS_AMOUNT, FMT(prefix, ".requireValidFee")),
188183
requireZeroWeightB(
189184
pb,
190185
isTransferTx.result(),

packages/loopring_v3/contracts/aux/agents/FastWithdrawalAgent.sol

Lines changed: 58 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,18 @@ import "../../lib/MathUint.sol";
1111
import "../../lib/ReentrancyGuard.sol";
1212
import "../../lib/SignatureUtil.sol";
1313

14-
15-
/// @title Fast withdrawal agent implementation. The fast withdrawal request reduces to
16-
/// a normal onchain withdrawal request after a specified time limit has exceeded.
14+
/// @title Fast withdrawal agent implementation. With the help of liquidity providers (LPs),
15+
/// exchange operators can convert any normal withdrawals into fast withdrawals.
1716
///
1817
/// Fast withdrawals are a way for the owner to provide instant withdrawals for
1918
/// users with the help of a liquidity provider and conditional transfers.
2019
///
2120
/// A fast withdrawal requires the non-trustless cooperation of 2 parties:
2221
/// - A liquidity provider which provides funds to users immediately onchain
2322
/// - The operator which will make sure the user has sufficient funds offchain
24-
/// so that the liquidity provider can be paid back offchain using a conditional transfer.
25-
/// The operator also needs to process those conditional transfers so that the
26-
/// liquidity provider receives its funds back in its own account where it
27-
/// again has full custody over it.
28-
///
29-
/// However, there is a special case when the fast withdrawal reduces to a standard
30-
/// withdrawal and the fee is paid onchain. In this case the withdrawal can be
31-
/// done completely trustless, no cooperation with the owner is needed.
23+
/// so that the liquidity provider can be paid back.
24+
/// The operator also needs to process those withdrawals so that the
25+
/// liquidity provider receives its funds back.
3226
///
3327
/// We require the fast withdrawals to be executed by the liquidity provider (as msg.sender)
3428
/// so that the liquidity provider can impose its own rules on how its funds are spent. This will
@@ -44,49 +38,44 @@ import "../../lib/SignatureUtil.sol";
4438
/// authorize this contract as their agent.
4539
///
4640
/// @author Brecht Devos - <[email protected]>
41+
/// @author Kongliang Zhong - <[email protected]>
42+
/// @author Daniel Wang - <[email protected]>
4743
contract FastWithdrawalAgent is ReentrancyGuard
4844
{
4945
using AddressUtil for address;
5046
using AddressUtil for address payable;
5147
using ERC20SafeTransfer for address;
5248
using MathUint for uint;
53-
using SignatureUtil for bytes32;
5449

55-
struct FastWithdrawal
50+
event Processed(
51+
address exchange,
52+
address from,
53+
address to,
54+
address token,
55+
uint96 amount,
56+
address provider,
57+
bool success
58+
);
59+
60+
struct Withdrawal
5661
{
5762
address exchange;
5863
address from; // The owner of the account
59-
address to; // The address that will receive the tokens withdrawn
64+
address to; // The `to` address of the withdrawal
6065
address token;
6166
uint96 amount;
62-
address feeToken;
63-
uint96 fee;
64-
uint32 nonce;
65-
uint32 validUntil;
66-
67-
bytes signature;
68-
}
69-
70-
// EIP712
71-
bytes32 constant public FASTWITHDRAWAL_TYPEHASH = keccak256(
72-
"FastWithdrawal(address exchange,address from,address to,address token,uint96 amount,address feeToken,uint96 fee,uint32 nonce,uint32 validUntil)"
73-
);
74-
bytes32 public DOMAIN_SEPARATOR;
75-
76-
constructor()
77-
{
78-
DOMAIN_SEPARATOR = EIP712.hash(EIP712.Domain("FastWithdrawalAgent", "1.0", address(this)));
67+
uint32 storageID;
7968
}
8069

8170
// This method needs to be called by any liquidity provider
82-
function executeFastWithdrawals(FastWithdrawal[] memory fastWithdrawals)
71+
function executeFastWithdrawals(Withdrawal[] calldata withdrawals)
8372
public
8473
nonReentrant
8574
payable
8675
{
8776
// Do all fast withdrawals
88-
for (uint i = 0; i < fastWithdrawals.length; i++) {
89-
executeFastWithdrawal(fastWithdrawals[i]);
77+
for (uint i = 0; i < withdrawals.length; i++) {
78+
executeInternal(withdrawals[i]);
9079
}
9180
// Return any ETH left into this contract
9281
// (can happen when more ETH is sent than needed for the fast withdrawals)
@@ -95,68 +84,51 @@ contract FastWithdrawalAgent is ReentrancyGuard
9584

9685
// -- Internal --
9786

98-
function executeFastWithdrawal(FastWithdrawal memory fastWithdrawal)
87+
function executeInternal(Withdrawal calldata withdrawal)
9988
internal
10089
{
90+
require(
91+
withdrawal.exchange != address(0) &&
92+
withdrawal.from != address(0) &&
93+
withdrawal.to != address(0) &&
94+
withdrawal.amount != 0,
95+
"INVALID_WITHDRAWAL"
96+
);
97+
10198
// The liquidity provider always authorizes the fast withdrawal by being the direct caller
10299
address payable liquidityProvider = msg.sender;
103100

104-
if (fastWithdrawal.signature.length > 0) {
105-
// Compute the hash
106-
bytes32 hash = EIP712.hashPacked(
107-
DOMAIN_SEPARATOR,
108-
keccak256(
109-
abi.encodePacked(
110-
FASTWITHDRAWAL_TYPEHASH,
111-
fastWithdrawal.exchange,
112-
fastWithdrawal.from,
113-
fastWithdrawal.to,
114-
fastWithdrawal.token,
115-
fastWithdrawal.amount,
116-
fastWithdrawal.feeToken,
117-
fastWithdrawal.fee,
118-
fastWithdrawal.nonce,
119-
fastWithdrawal.validUntil
120-
)
121-
)
122-
);
123-
124-
// Check the signature
125-
require(hash.verifySignature(fastWithdrawal.from, fastWithdrawal.signature), "INVALID_SIGNATURE");
126-
127-
// Check the time limit
128-
require(block.timestamp <= fastWithdrawal.validUntil, "TX_EXPIRED");
129-
130-
// Approve the offchain transfer from the account that's withdrawing back to the liquidity provider
131-
IExchangeV3(fastWithdrawal.exchange).approveOffchainTransfer(
132-
fastWithdrawal.from,
101+
bool success;
102+
// Override the destination address of a withdrawal to the address of the liquidity provider
103+
try IExchangeV3(withdrawal.exchange).setWithdrawalRecipient(
104+
withdrawal.from,
105+
withdrawal.to,
106+
withdrawal.token,
107+
withdrawal.amount,
108+
withdrawal.storageID,
109+
liquidityProvider
110+
) {
111+
// Transfer the tokens immediately to the requested address
112+
// using funds from the liquidity provider (`msg.sender`).
113+
transfer(
133114
liquidityProvider,
134-
fastWithdrawal.token,
135-
fastWithdrawal.amount,
136-
fastWithdrawal.feeToken,
137-
fastWithdrawal.fee,
138-
fastWithdrawal.validUntil,
139-
fastWithdrawal.nonce
140-
);
141-
} else {
142-
// Override the destination address of a withdrawal to the address of the liquidity provider
143-
IExchangeV3(fastWithdrawal.exchange).setWithdrawalRecipient(
144-
fastWithdrawal.from,
145-
fastWithdrawal.to,
146-
fastWithdrawal.token,
147-
fastWithdrawal.amount,
148-
fastWithdrawal.nonce,
149-
liquidityProvider
115+
withdrawal.to,
116+
withdrawal.token,
117+
withdrawal.amount
150118
);
119+
success = true;
120+
} catch {
121+
success = false;
151122
}
152123

153-
// Transfer the tokens immediately to the requested address
154-
// using funds from the liquidity provider (`msg.sender`).
155-
transfer(
124+
emit Processed(
125+
withdrawal.exchange,
126+
withdrawal.from,
127+
withdrawal.to,
128+
withdrawal.token,
129+
withdrawal.amount,
156130
liquidityProvider,
157-
fastWithdrawal.to,
158-
fastWithdrawal.token,
159-
fastWithdrawal.amount
131+
success
160132
);
161133
}
162134

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright 2017 Loopring Technology Limited.
3+
pragma solidity ^0.7.0;
4+
pragma experimental ABIEncoderV2;
5+
6+
import "../agents/FastWithdrawalAgent.sol";
7+
import "../../lib/OwnerManagable.sol";
8+
import "../../lib/EIP712.sol";
9+
import "../../lib/ERC20.sol";
10+
import "../../lib/ERC20SafeTransfer.sol";
11+
import "../../lib/MathUint.sol";
12+
import "../../lib/ReentrancyGuard.sol";
13+
import "../../lib/SignatureUtil.sol";
14+
15+
16+
/// @title Basic contract storing funds for a liquidity provider.
17+
/// @author Brecht Devos - <[email protected]>
18+
contract FastWithdrawalLiquidityProvider is ReentrancyGuard, OwnerManagable
19+
{
20+
using AddressUtil for address;
21+
using AddressUtil for address payable;
22+
using ERC20SafeTransfer for address;
23+
using MathUint for uint;
24+
using SignatureUtil for bytes32;
25+
26+
struct FastWithdrawalApproval
27+
{
28+
address exchange;
29+
address from;
30+
address to;
31+
address token;
32+
uint96 amount;
33+
uint32 storageID;
34+
uint64 validUntil; // most significant 32 bits as block height, least significant 32 bits as block time
35+
address signer;
36+
bytes signature; // signer's signature
37+
}
38+
39+
bytes32 constant public FASTWITHDRAWAL_APPROVAL_TYPEHASH = keccak256(
40+
"FastWithdrawalApproval(address exchange,address from,address to,address token,uint96 amount,uint32 storageID,uint64 validUntil)"
41+
);
42+
43+
bytes32 public DOMAIN_SEPARATOR;
44+
45+
FastWithdrawalAgent public agent;
46+
47+
constructor(FastWithdrawalAgent _agent)
48+
{
49+
agent = _agent;
50+
DOMAIN_SEPARATOR = EIP712.hash(EIP712.Domain("FastWithdrawalLiquidityProvider", "1.0", address(this)));
51+
}
52+
53+
// Execute the fast withdrawals.
54+
// Full approvals are posted onchain so they can be used by anyone to speed up the
55+
// withdrawal by calling , but the approvals are not validated when done by the
56+
// owner or one of the managers.
57+
function execute(FastWithdrawalApproval[] calldata approvals)
58+
external
59+
nonReentrant
60+
{
61+
// Prepare the data and validate the approvals when necessary
62+
FastWithdrawalAgent.Withdrawal[] memory withdrawals =
63+
new FastWithdrawalAgent.Withdrawal[](approvals.length);
64+
65+
bool skipApprovalCheck = isManager(msg.sender);
66+
for (uint i = 0; i < approvals.length; i++) {
67+
require(skipApprovalCheck || isApprovalValid(approvals[i]), "PROHIBITED");
68+
withdrawals[i] = translate(approvals[i]);
69+
}
70+
71+
// Calculate how much ETH we need to send to the agent contract.
72+
// We could also send the full ETH balance each time, but that'll need
73+
// an additional transfer to send funds back, which may actually be more efficient.
74+
uint value = 0;
75+
for (uint i = 0; i < withdrawals.length; i++) {
76+
if (withdrawals[i].token == address(0)) {
77+
value = value.add(withdrawals[i].amount);
78+
}
79+
}
80+
// Execute all the fast withdrawals
81+
agent.executeFastWithdrawals{value: value}(withdrawals);
82+
}
83+
84+
// Allows the LP to transfer funds back out of this contract.
85+
function drain(
86+
address to,
87+
address token,
88+
uint amount
89+
)
90+
external
91+
nonReentrant
92+
onlyOwner
93+
{
94+
if (token == address(0)) {
95+
to.sendETHAndVerify(amount, gasleft()); // ETH
96+
} else {
97+
token.safeTransferAndVerify(to, amount); // ERC20 token
98+
}
99+
}
100+
101+
// Allows the LP to enable the necessary ERC20 approvals
102+
function approve(
103+
address token,
104+
address spender,
105+
uint amount
106+
)
107+
external
108+
nonReentrant
109+
onlyOwner
110+
{
111+
require(ERC20(token).approve(spender, amount), "APPROVAL_FAILED");
112+
}
113+
114+
function isApprovalValid(
115+
FastWithdrawalApproval calldata approval
116+
)
117+
internal
118+
view
119+
returns (bool)
120+
{
121+
// Compute the hash
122+
bytes32 hash = EIP712.hashPacked(
123+
DOMAIN_SEPARATOR,
124+
keccak256(
125+
abi.encodePacked(
126+
FASTWITHDRAWAL_APPROVAL_TYPEHASH,
127+
approval.exchange,
128+
approval.from,
129+
approval.to,
130+
approval.token,
131+
approval.amount,
132+
approval.storageID,
133+
approval.validUntil
134+
)
135+
)
136+
);
137+
138+
return hash.verifySignature(approval.signer, approval.signature) &&
139+
checkValidUntil(approval.validUntil) &&
140+
isManager(approval.signer);
141+
}
142+
143+
receive() payable external {}
144+
145+
// -- Internal --
146+
147+
function checkValidUntil(uint64 validUntil)
148+
internal
149+
view
150+
returns (bool)
151+
{
152+
return (validUntil & 0xFFFFFFFF) >= block.timestamp ||
153+
(validUntil >> 32) >= block.number;
154+
}
155+
156+
function translate(FastWithdrawalApproval calldata approval)
157+
internal
158+
pure
159+
returns (FastWithdrawalAgent.Withdrawal memory)
160+
{
161+
return FastWithdrawalAgent.Withdrawal({
162+
exchange: approval.exchange,
163+
from: approval.from,
164+
to: approval.to,
165+
token: approval.token,
166+
amount: approval.amount,
167+
storageID: approval.storageID
168+
});
169+
}
170+
}

packages/loopring_v3/contracts/core/impl/DefaultDepositContract.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ contract DefaultDepositContract is IDepositContract, Claimable
3838
modifier ifNotZero(uint amount)
3939
{
4040
if (amount == 0) return;
41-
else { _; }
41+
else _;
4242
}
4343

4444
event CheckBalance(

0 commit comments

Comments
 (0)