@ocap/tx-protocols 1.6.3 → 1.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/lib/execute.js +97 -30
- package/lib/index.js +56 -14
- package/lib/protocols/account/declare.js +36 -30
- package/lib/protocols/account/delegate.js +78 -40
- package/lib/protocols/account/migrate.js +65 -49
- package/lib/protocols/account/revoke-delegate.js +39 -23
- package/lib/protocols/asset/acquire-v2.js +159 -0
- package/lib/protocols/asset/acquire-v3.js +242 -0
- package/lib/protocols/asset/calls/README.md +5 -0
- package/lib/protocols/asset/calls/transfer-token.js +37 -0
- package/lib/protocols/asset/calls/transfer.js +29 -0
- package/lib/protocols/asset/create.js +85 -36
- package/lib/protocols/asset/mint.js +133 -0
- package/lib/protocols/asset/pipes/exec-mint-hook.js +59 -0
- package/lib/protocols/asset/pipes/extract-factory-tokens.js +18 -0
- package/lib/protocols/asset/pipes/verify-acquire-params.js +30 -0
- package/lib/protocols/asset/pipes/verify-itx-address.js +41 -0
- package/lib/protocols/asset/pipes/verify-itx-assets.js +49 -0
- package/lib/protocols/asset/pipes/verify-itx-variables.js +26 -0
- package/lib/protocols/asset/pipes/verify-mint-limit.js +13 -0
- package/lib/protocols/asset/update.js +76 -44
- package/lib/protocols/factory/create.js +146 -0
- package/lib/protocols/governance/claim-stake.js +219 -0
- package/lib/protocols/governance/revoke-stake.js +136 -0
- package/lib/protocols/governance/stake.js +176 -0
- package/lib/protocols/rollup/claim-reward.js +283 -0
- package/lib/protocols/rollup/create-block.js +333 -0
- package/lib/protocols/rollup/create.js +169 -0
- package/lib/protocols/rollup/join.js +156 -0
- package/lib/protocols/rollup/leave.js +127 -0
- package/lib/protocols/rollup/migrate-contract.js +53 -0
- package/lib/protocols/rollup/migrate-token.js +66 -0
- package/lib/protocols/rollup/pause.js +52 -0
- package/lib/protocols/rollup/pipes/ensure-service-fee.js +37 -0
- package/lib/protocols/rollup/pipes/ensure-validator.js +10 -0
- package/lib/protocols/rollup/pipes/verify-evidence.js +37 -0
- package/lib/protocols/rollup/pipes/verify-paused.js +10 -0
- package/lib/protocols/rollup/pipes/verify-signers.js +88 -0
- package/lib/protocols/rollup/resume.js +52 -0
- package/lib/protocols/rollup/update.js +98 -0
- package/lib/protocols/token/create.js +150 -0
- package/lib/protocols/token/deposit-v2.js +241 -0
- package/lib/protocols/token/withdraw-v2.js +255 -0
- package/lib/protocols/trade/exchange-v2.js +179 -0
- package/lib/protocols/trade/transfer-v2.js +136 -0
- package/lib/protocols/trade/transfer-v3.js +241 -0
- package/lib/util.js +325 -2
- package/package.json +23 -16
- package/lib/protocols/misc/poke.js +0 -106
- package/lib/protocols/trade/exchange.js +0 -139
- package/lib/protocols/trade/transfer.js +0 -101
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const isEmpty = require('empty-value');
|
|
2
|
+
const cloneDeep = require('lodash/cloneDeep');
|
|
3
|
+
const Joi = require('@arcblock/validator');
|
|
4
|
+
const Error = require('@ocap/util/lib/error');
|
|
5
|
+
const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
6
|
+
const { account, token } = require('@ocap/state');
|
|
7
|
+
const { toTokenAddress } = require('@arcblock/did-util');
|
|
8
|
+
const { fromTokenToUnit } = require('@ocap/util');
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line global-require
|
|
11
|
+
const debug = require('debug')(`${require('../../../package.json').name}:create-token`);
|
|
12
|
+
const { decodeAnySafe } = require('../../util');
|
|
13
|
+
const ensureServiceFee = require('../rollup/pipes/ensure-service-fee');
|
|
14
|
+
|
|
15
|
+
const MAX_TOTAL_SUPPLY = fromTokenToUnit(10000 * 100000000, 18); // 32
|
|
16
|
+
|
|
17
|
+
const runner = new Runner();
|
|
18
|
+
|
|
19
|
+
runner.use(pipes.VerifyMultiSig(0));
|
|
20
|
+
|
|
21
|
+
const schema = Joi.object({
|
|
22
|
+
address: Joi.DID().role('ROLE_TOKEN').required(),
|
|
23
|
+
name: Joi.string().min(1).max(32).required(),
|
|
24
|
+
description: Joi.string().min(1).max(128).required(),
|
|
25
|
+
symbol: Joi.string().min(2).max(6).uppercase().required(),
|
|
26
|
+
unit: Joi.string().min(1).max(6).lowercase().required(),
|
|
27
|
+
decimal: Joi.number().min(6).max(18).required(),
|
|
28
|
+
icon: Joi.string().optional().valid(''),
|
|
29
|
+
totalSupply: Joi.BN().greater(0).max(MAX_TOTAL_SUPPLY).required(),
|
|
30
|
+
initialSupply: Joi.BN().greater(0).max(Joi.ref('totalSupply')).required(),
|
|
31
|
+
foreignToken: Joi.schemas.foreignToken.optional().default(null),
|
|
32
|
+
data: Joi.any().optional(),
|
|
33
|
+
}).options({ stripUnknown: true, noDefaults: false });
|
|
34
|
+
|
|
35
|
+
// 1. verify itx
|
|
36
|
+
runner.use(({ itx }, next) => {
|
|
37
|
+
const { error } = schema.validate(itx);
|
|
38
|
+
if (error) {
|
|
39
|
+
return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
|
|
40
|
+
}
|
|
41
|
+
return next();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
runner.use(
|
|
45
|
+
pipes.VerifyInfo([
|
|
46
|
+
{
|
|
47
|
+
error: 'INVALID_TOKEN',
|
|
48
|
+
message: 'Token address is not valid',
|
|
49
|
+
fn: (context) => {
|
|
50
|
+
const itx = cloneDeep(context.itx);
|
|
51
|
+
itx.data = decodeAnySafe(itx.data);
|
|
52
|
+
itx.address = '';
|
|
53
|
+
|
|
54
|
+
return toTokenAddress(itx) === context.itx.address;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
])
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Ensure sender exist
|
|
61
|
+
runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
|
|
62
|
+
runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
|
|
63
|
+
|
|
64
|
+
// Ensure delegation
|
|
65
|
+
runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK', table: 'account' }));
|
|
66
|
+
runner.use(pipes.VerifyDelegation({ type: 'signature', signerKey: 'senderState', delegatorKey: 'delegatorState' }));
|
|
67
|
+
|
|
68
|
+
// Ensure token not exist
|
|
69
|
+
runner.use(pipes.ExtractState({ from: 'itx.address', to: 'tokenState', table: 'token', status: 'OK' }));
|
|
70
|
+
runner.use(
|
|
71
|
+
pipes.VerifyInfo([
|
|
72
|
+
{
|
|
73
|
+
error: 'DUPLICATE_TOKEN',
|
|
74
|
+
message: 'Token address already exists on chain',
|
|
75
|
+
fn: (context) => isEmpty(context.tokenState),
|
|
76
|
+
},
|
|
77
|
+
])
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Ensure uniq ness of token symbol
|
|
81
|
+
runner.use(async (context, next) => {
|
|
82
|
+
const { symbol } = context.config.token;
|
|
83
|
+
if (symbol.toLowerCase() === context.itx.symbol.toLowerCase()) {
|
|
84
|
+
return next(new Error('DUPLICATE_SYMBOL', `Token symbol can not be ${symbol}`));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const exist = await context.statedb.token.existBySymbol(context.itx.symbol, context);
|
|
88
|
+
if (exist) {
|
|
89
|
+
return next(new Error('DUPLICATE_SYMBOL', 'Token symbol already exists'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return next();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
runner.use(ensureServiceFee);
|
|
96
|
+
|
|
97
|
+
// Update sender state, token state
|
|
98
|
+
runner.use(
|
|
99
|
+
async (context, next) => {
|
|
100
|
+
const { tx, itx, statedb, senderState, delegatorState, senderUpdates, vaultState, vaultUpdates } = context;
|
|
101
|
+
const data = decodeAnySafe(itx.data);
|
|
102
|
+
const owner = delegatorState ? delegatorState.address : senderState.address;
|
|
103
|
+
|
|
104
|
+
const delegatorUpdates = {};
|
|
105
|
+
senderUpdates.tokens = senderUpdates.tokens || {};
|
|
106
|
+
|
|
107
|
+
// We are definitely creating a different token, so it is safe to set tokens to initial supply
|
|
108
|
+
// For delegated create-token, the delegator is the actual token-holder
|
|
109
|
+
if (delegatorState) {
|
|
110
|
+
delegatorUpdates.tokens = delegatorState.tokens || {};
|
|
111
|
+
delegatorUpdates.tokens[itx.address] = itx.initialSupply;
|
|
112
|
+
} else {
|
|
113
|
+
senderUpdates.tokens[itx.address] = itx.initialSupply;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const [newSenderState, tokenState, newVaultState, newDelegatorState] = await Promise.all([
|
|
117
|
+
statedb.account.update(
|
|
118
|
+
senderState.address,
|
|
119
|
+
account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
|
|
120
|
+
context
|
|
121
|
+
),
|
|
122
|
+
|
|
123
|
+
statedb.token.create(itx.address, token.create({ ...cloneDeep(itx), data, issuer: owner }, context), context),
|
|
124
|
+
|
|
125
|
+
isEmpty(vaultUpdates)
|
|
126
|
+
? vaultState
|
|
127
|
+
: statedb.account.update(vaultState.address, account.update(vaultState, vaultUpdates, context), context),
|
|
128
|
+
|
|
129
|
+
delegatorState
|
|
130
|
+
? statedb.account.update(
|
|
131
|
+
delegatorState.address,
|
|
132
|
+
account.update(delegatorState, delegatorUpdates, context),
|
|
133
|
+
context
|
|
134
|
+
)
|
|
135
|
+
: null,
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
context.senderState = newSenderState;
|
|
139
|
+
context.tokenState = tokenState;
|
|
140
|
+
context.vaultState = newVaultState;
|
|
141
|
+
context.delegatorState = newDelegatorState;
|
|
142
|
+
|
|
143
|
+
debug('create token v2', tokenState);
|
|
144
|
+
|
|
145
|
+
next();
|
|
146
|
+
},
|
|
147
|
+
{ persistError: true }
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
module.exports = runner;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/* eslint-disable indent */
|
|
2
|
+
const Error = require('@ocap/util/lib/error');
|
|
3
|
+
const Joi = require('@arcblock/validator');
|
|
4
|
+
const getListField = require('@ocap/util/lib/get-list-field');
|
|
5
|
+
const { BN, fromUnitToToken } = require('@ocap/util');
|
|
6
|
+
const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
7
|
+
const { account, stake, evidence } = require('@ocap/state');
|
|
8
|
+
const { toStakeAddress } = require('@arcblock/did-util');
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line global-require
|
|
11
|
+
const debug = require('debug')(`${require('../../../package.json').name}:deposit-token`);
|
|
12
|
+
|
|
13
|
+
const VerifySigners = require('../rollup/pipes/verify-signers');
|
|
14
|
+
const VerifyPaused = require('../rollup/pipes/verify-paused');
|
|
15
|
+
const { applyTokenUpdates, getTxFee, getBNSum, getRewardLocker } = require('../../util');
|
|
16
|
+
|
|
17
|
+
const schema = Joi.object({
|
|
18
|
+
token: Joi.schemas.tokenInput.required(),
|
|
19
|
+
to: Joi.DID().wallet('ethereum').required(),
|
|
20
|
+
proposer: Joi.DID().wallet('ethereum').required(),
|
|
21
|
+
evidence: Joi.object({
|
|
22
|
+
hash: Joi.string().regex(Joi.patterns.txHash).required(),
|
|
23
|
+
}).required(),
|
|
24
|
+
rollup: Joi.DID().role('ROLE_ROLLUP').required(),
|
|
25
|
+
actualFee: Joi.BN().min(0).required(),
|
|
26
|
+
data: Joi.any().optional(),
|
|
27
|
+
}).options({ stripUnknown: true, noDefaults: false });
|
|
28
|
+
|
|
29
|
+
const runner = new Runner();
|
|
30
|
+
|
|
31
|
+
// 1. verify itx
|
|
32
|
+
runner.use(({ tx, itx }, next) => {
|
|
33
|
+
const { error } = schema.validate(itx);
|
|
34
|
+
if (error) {
|
|
35
|
+
return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (itx.to !== tx.from) {
|
|
39
|
+
return next(new Error('INVALID_TX', 'You can only deposit to tx sender account'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ensure proposer in signer list
|
|
43
|
+
const signatures = getListField(tx, 'signatures');
|
|
44
|
+
if (signatures.some((x) => x.signer === itx.proposer) === false) {
|
|
45
|
+
return next(new Error('INVALID_TX', 'itx.proposer must exist in signatures'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ensure tx.from not in validator list
|
|
49
|
+
if (signatures.some((x) => x.signer === tx.from)) {
|
|
50
|
+
return next(new Error('INVALID_TX', 'tx.from should not exist in signatures'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return next();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 2. verify rollup against itx
|
|
57
|
+
runner.use(pipes.ExtractState({ from: 'itx.rollup', to: 'rollupState', status: 'INVALID_ROLLUP', table: 'rollup' }));
|
|
58
|
+
runner.use(VerifyPaused());
|
|
59
|
+
runner.use((context, next) => {
|
|
60
|
+
const { itx, rollupState } = context;
|
|
61
|
+
if (rollupState.tokenAddress !== itx.token.address) {
|
|
62
|
+
return next(new Error('INVALID_TX', 'Deposit token address does not match with rollup state'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (rollupState.validators.some((x) => x.address === itx.proposer) === false) {
|
|
66
|
+
return next(new Error('INVALID_TX', 'itx.proposer does not exist in validators'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return next();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 3. verify evidence
|
|
73
|
+
runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'evidenceState', status: 'OK', table: 'evidence' }));
|
|
74
|
+
runner.use(({ evidenceState }, next) => {
|
|
75
|
+
if (evidenceState) {
|
|
76
|
+
return next(new Error('INVALID_TX', 'Deposit evidence already seen on this chain'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return next();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// 4. verify signers & signatures
|
|
83
|
+
runner.use(VerifySigners({ signersKey: 'tx.signaturesList', allowSender: true }));
|
|
84
|
+
runner.use(pipes.VerifyMultiSigV2({ signersKey: 'signers' }));
|
|
85
|
+
|
|
86
|
+
// 5. verify token state
|
|
87
|
+
runner.use(pipes.ExtractState({ from: 'itx.token.address', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })); // prettier-ignore
|
|
88
|
+
|
|
89
|
+
// 6. verify staking: get address, extract state, verify amount
|
|
90
|
+
runner.use((context, next) => {
|
|
91
|
+
const { itx } = context;
|
|
92
|
+
context.stakeAddress = toStakeAddress(itx.proposer, itx.rollup);
|
|
93
|
+
context.lockerAddress = getRewardLocker(itx.rollup);
|
|
94
|
+
return next();
|
|
95
|
+
});
|
|
96
|
+
runner.use(pipes.ExtractState({ from: 'lockerAddress', to: 'lockerState', status: 'OK', table: 'stake' }));
|
|
97
|
+
runner.use(pipes.ExtractState({ from: 'stakeAddress', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
|
|
98
|
+
runner.use((context, next) => {
|
|
99
|
+
const { itx, stakeState, tokenState, rollupState } = context;
|
|
100
|
+
if (stakeState.revocable === true) {
|
|
101
|
+
return next(new Error('INVALID_STAKE_STATE', `Staking not locked for deposit: ${stakeState.address}`));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const depositAmount = new BN(itx.token.value).add(new BN(itx.actualFee));
|
|
105
|
+
const minDepositAmount = new BN(rollupState.minDepositAmount);
|
|
106
|
+
const maxDepositAmount = new BN(rollupState.maxDepositAmount);
|
|
107
|
+
if (depositAmount.lt(minDepositAmount)) {
|
|
108
|
+
return next(
|
|
109
|
+
new Error(
|
|
110
|
+
'INVALID_TX',
|
|
111
|
+
`Deposit amount must be greater than minDepositAmount: ${fromUnitToToken(minDepositAmount, tokenState.decimal)}`
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (depositAmount.gt(maxDepositAmount)) {
|
|
116
|
+
return next(
|
|
117
|
+
new Error(
|
|
118
|
+
'INVALID_TX',
|
|
119
|
+
`Deposit amount must be less than maxDepositAmount: ${fromUnitToToken(maxDepositAmount, tokenState.decimal)}`
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const stakedAmount = new BN(stakeState.tokens[rollupState.tokenAddress] || 0);
|
|
125
|
+
if (depositAmount.gt(stakedAmount)) {
|
|
126
|
+
return next(
|
|
127
|
+
new Error(
|
|
128
|
+
'INSUFFICIENT_STAKE',
|
|
129
|
+
`Deposit amount must be less than available stake: ${fromUnitToToken(stakedAmount, tokenState.decimal)}`
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return next();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 7. verify actualFee
|
|
138
|
+
runner.use((context, next) => {
|
|
139
|
+
const { itx, rollupState, tokenState } = context;
|
|
140
|
+
const { depositFeeRate, maxDepositFee, minDepositFee } = rollupState;
|
|
141
|
+
|
|
142
|
+
const { reward } = getTxFee({
|
|
143
|
+
amount: itx.token.value,
|
|
144
|
+
feeRate: depositFeeRate,
|
|
145
|
+
maxFee: maxDepositFee,
|
|
146
|
+
minFee: minDepositFee,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (new BN(itx.actualFee).lt(new BN(reward))) {
|
|
150
|
+
const expected = fromUnitToToken(reward, tokenState.decimal);
|
|
151
|
+
const actual = fromUnitToToken(itx.actualFee, tokenState.decimal);
|
|
152
|
+
return next(new Error('INVALID_TX', `itx.actualFee too low, expect at least ${expected}, got ${actual}`));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return next();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 7. verify sender and signer states
|
|
159
|
+
runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'OK', table: 'account' }));
|
|
160
|
+
runner.use(pipes.ExtractState({ from: 'signers', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
|
|
161
|
+
runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
|
|
162
|
+
|
|
163
|
+
// 8. update state: the token minting is done when deposit finalized in rollup-block
|
|
164
|
+
runner.use(
|
|
165
|
+
async (context, next) => {
|
|
166
|
+
const { tx, itx, statedb, senderState, stakeState, stakeAddress, lockerState, lockerAddress } = context;
|
|
167
|
+
|
|
168
|
+
const user = itx.token.value;
|
|
169
|
+
const fee = itx.actualFee;
|
|
170
|
+
const total = getBNSum(user, fee);
|
|
171
|
+
|
|
172
|
+
const stakeUpdates = applyTokenUpdates([{ address: itx.token.address, value: total }], stakeState, 'sub');
|
|
173
|
+
const senderUpdates = applyTokenUpdates([{ address: itx.token.address, value: user }], senderState || {}, 'add');
|
|
174
|
+
const lockerUpdates = applyTokenUpdates(
|
|
175
|
+
[{ address: itx.token.address, value: fee }],
|
|
176
|
+
lockerState || { tokens: {} },
|
|
177
|
+
'add'
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const sender = senderState ? senderState.address : tx.from;
|
|
181
|
+
const [newSenderState, newStakeState, newLockerState, evidenceState] = await Promise.all([
|
|
182
|
+
// updateOrCreate user account
|
|
183
|
+
statedb.account.updateOrCreate(
|
|
184
|
+
senderState,
|
|
185
|
+
account.updateOrCreate(senderState, { address: sender, nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
|
|
186
|
+
context
|
|
187
|
+
),
|
|
188
|
+
|
|
189
|
+
// update stake amount from the proposer
|
|
190
|
+
statedb.stake.update(stakeAddress, stake.update(stakeState, stakeUpdates, context), context),
|
|
191
|
+
|
|
192
|
+
// Update pending fee locker state
|
|
193
|
+
lockerState
|
|
194
|
+
? statedb.stake.update(lockerState.address, stake.update(lockerState, lockerUpdates, context), context)
|
|
195
|
+
: statedb.stake.create(
|
|
196
|
+
lockerAddress,
|
|
197
|
+
stake.create(
|
|
198
|
+
{
|
|
199
|
+
address: lockerAddress,
|
|
200
|
+
sender: itx.rollup,
|
|
201
|
+
receiver: itx.rollup,
|
|
202
|
+
revocable: false,
|
|
203
|
+
message: 'pending-block-reward',
|
|
204
|
+
revokeWaitingPeriod: 0,
|
|
205
|
+
...lockerUpdates,
|
|
206
|
+
},
|
|
207
|
+
context
|
|
208
|
+
),
|
|
209
|
+
context
|
|
210
|
+
),
|
|
211
|
+
|
|
212
|
+
// Create evidence state
|
|
213
|
+
statedb.evidence.create(
|
|
214
|
+
itx.evidence.hash,
|
|
215
|
+
evidence.create({ hash: itx.evidence.hash, data: 'rollup-deposit' }, context),
|
|
216
|
+
context
|
|
217
|
+
),
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
context.senderState = newSenderState;
|
|
221
|
+
context.stakeState = newStakeState;
|
|
222
|
+
context.evidenceState = evidenceState;
|
|
223
|
+
context.stakeStates = [newStakeState, newLockerState];
|
|
224
|
+
|
|
225
|
+
context.updatedAccounts = [
|
|
226
|
+
// stake for tx proposer is decreased
|
|
227
|
+
{ address: stakeAddress, token: itx.token.address, delta: `-${total}`, action: 'unlock' },
|
|
228
|
+
// mint to depositor from stake
|
|
229
|
+
{ address: sender, token: itx.token.address, delta: user, action: 'unlock' },
|
|
230
|
+
// tx fee is locked for later claiming
|
|
231
|
+
{ address: lockerAddress, token: itx.token.address, delta: fee, action: 'pending' },
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
debug('deposit-token-v2', itx);
|
|
235
|
+
|
|
236
|
+
next();
|
|
237
|
+
},
|
|
238
|
+
{ persistError: true }
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
module.exports = runner;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/* eslint-disable indent */
|
|
2
|
+
const Error = require('@ocap/util/lib/error');
|
|
3
|
+
const Joi = require('@arcblock/validator');
|
|
4
|
+
const getListField = require('@ocap/util/lib/get-list-field');
|
|
5
|
+
const { BN, fromUnitToToken } = require('@ocap/util');
|
|
6
|
+
const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
7
|
+
const { account, stake } = require('@ocap/state');
|
|
8
|
+
const { toStakeAddress } = require('@arcblock/did-util');
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line global-require
|
|
11
|
+
const debug = require('debug')(`${require('../../../package.json').name}:withdraw-token`);
|
|
12
|
+
|
|
13
|
+
const VerifyPaused = require('../rollup/pipes/verify-paused');
|
|
14
|
+
const { applyTokenUpdates, getTxFee, getBNSum, getRewardLocker } = require('../../util');
|
|
15
|
+
|
|
16
|
+
const verifyMultiSigV2 = pipes.VerifyMultiSigV2({ signersKey: 'signers' });
|
|
17
|
+
|
|
18
|
+
const schema = Joi.object({
|
|
19
|
+
token: Joi.schemas.tokenInput.required(),
|
|
20
|
+
to: Joi.DID().wallet('ethereum').required(),
|
|
21
|
+
rollup: Joi.DID().role('ROLE_ROLLUP').required(),
|
|
22
|
+
proposer: Joi.DID().wallet('ethereum').optional().allow('').default(''),
|
|
23
|
+
actualFee: Joi.BN().min(0).required(),
|
|
24
|
+
maxFee: Joi.BN().min(0).required(),
|
|
25
|
+
data: Joi.any().optional(),
|
|
26
|
+
}).options({ stripUnknown: true, noDefaults: false });
|
|
27
|
+
|
|
28
|
+
const runner = new Runner();
|
|
29
|
+
|
|
30
|
+
// 1. verify itx
|
|
31
|
+
runner.use(({ tx, itx }, next) => {
|
|
32
|
+
const { error } = schema.validate(itx);
|
|
33
|
+
if (error) {
|
|
34
|
+
return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (itx.to !== tx.from) {
|
|
38
|
+
return next(new Error('INVALID_TX', 'You can only withdraw to tx sender account'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return next();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 2. verify rollup against itx
|
|
45
|
+
runner.use(pipes.ExtractState({ from: 'itx.rollup', to: 'rollupState', status: 'INVALID_ROLLUP', table: 'rollup' }));
|
|
46
|
+
runner.use(VerifyPaused());
|
|
47
|
+
runner.use((context, next) => {
|
|
48
|
+
const { itx, rollupState } = context;
|
|
49
|
+
if (rollupState.tokenAddress !== itx.token.address) {
|
|
50
|
+
return next(new Error('INVALID_TX', 'Withdraw token address does not match with rollup'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return next();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 3. verify token state
|
|
57
|
+
runner.use(pipes.ExtractState({ from: 'itx.token.address', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })); // prettier-ignore
|
|
58
|
+
|
|
59
|
+
// 4. verify sender and signer states
|
|
60
|
+
runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
|
|
61
|
+
runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
|
|
62
|
+
|
|
63
|
+
// 5. verify amount
|
|
64
|
+
runner.use((context, next) => {
|
|
65
|
+
const { tx, itx, tokenState, rollupState } = context;
|
|
66
|
+
|
|
67
|
+
// withdraw amount
|
|
68
|
+
const withdrawAmount = new BN(itx.token.value);
|
|
69
|
+
const minWithdrawAmount = new BN(rollupState.minWithdrawAmount);
|
|
70
|
+
const maxWithdrawAmount = new BN(rollupState.maxWithdrawAmount);
|
|
71
|
+
if (withdrawAmount.lt(minWithdrawAmount)) {
|
|
72
|
+
return next(
|
|
73
|
+
new Error(
|
|
74
|
+
'INVALID_TX',
|
|
75
|
+
`Withdraw amount must be greater than: ${fromUnitToToken(minWithdrawAmount, tokenState.decimal)}`
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (withdrawAmount.gt(maxWithdrawAmount)) {
|
|
80
|
+
return next(
|
|
81
|
+
new Error(
|
|
82
|
+
'INVALID_TX',
|
|
83
|
+
`Withdraw amount must be less than maxWithdrawAmount: ${fromUnitToToken(maxWithdrawAmount, tokenState.decimal)}`
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// user balance
|
|
89
|
+
const maxAmount = getBNSum(itx.token.value, itx.actualFee, itx.maxFee);
|
|
90
|
+
context.tokenConditions = { owner: tx.from, tokens: [{ address: itx.token.address, value: maxAmount }] };
|
|
91
|
+
|
|
92
|
+
return next();
|
|
93
|
+
});
|
|
94
|
+
runner.use(pipes.VerifyTokenBalance({ ownerKey: 'senderState', conditionKey: 'tokenConditions' }));
|
|
95
|
+
|
|
96
|
+
// 6. verify actualFee
|
|
97
|
+
runner.use((context, next) => {
|
|
98
|
+
const { itx, rollupState, tokenState } = context;
|
|
99
|
+
const { withdrawFeeRate, maxWithdrawFee, minWithdrawFee } = rollupState;
|
|
100
|
+
|
|
101
|
+
const isFixedFee = new BN(itx.maxFee).isZero();
|
|
102
|
+
if (isFixedFee) {
|
|
103
|
+
const { reward } = getTxFee({
|
|
104
|
+
amount: itx.token.value,
|
|
105
|
+
feeRate: withdrawFeeRate,
|
|
106
|
+
maxFee: maxWithdrawFee,
|
|
107
|
+
minFee: minWithdrawFee,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (new BN(itx.actualFee).lt(new BN(reward))) {
|
|
111
|
+
const expected = fromUnitToToken(reward, tokenState.decimal);
|
|
112
|
+
const actual = fromUnitToToken(itx.actualFee, tokenState.decimal);
|
|
113
|
+
return next(new Error('INVALID_TX', `itx.actualFee too low, expect at least ${expected}, got ${actual}`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return next();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// 7. verify maxFee and conditional multi-sig
|
|
121
|
+
// eslint-disable-next-line consistent-return
|
|
122
|
+
runner.use((context, next) => {
|
|
123
|
+
const { tx, itx } = context;
|
|
124
|
+
const isFixedFee = new BN(itx.maxFee).isZero();
|
|
125
|
+
if (isFixedFee) {
|
|
126
|
+
return next();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const signatures = getListField(tx, 'signatures');
|
|
130
|
+
|
|
131
|
+
// ensure signature count
|
|
132
|
+
if (signatures.length !== 1) {
|
|
133
|
+
return next(new Error('INVALID_TX', 'Withdraw with maxFee should have one multi-sig'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ensure proposer in signer list
|
|
137
|
+
if (signatures.some((x) => x.signer === itx.proposer) === false) {
|
|
138
|
+
return next(new Error('INVALID_TX', 'itx.proposer must exist in tx.signatures'));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ensure signature is valid
|
|
142
|
+
context.signers = signatures.map((x) => x.signer);
|
|
143
|
+
verifyMultiSigV2(context, next);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 8. verify staking: user withdraw is locked in stake
|
|
147
|
+
runner.use((context, next) => {
|
|
148
|
+
const { itx, senderState } = context;
|
|
149
|
+
context.stakeAddress = toStakeAddress(senderState.address, itx.rollup);
|
|
150
|
+
context.lockerAddress = getRewardLocker(itx.rollup);
|
|
151
|
+
return next();
|
|
152
|
+
});
|
|
153
|
+
runner.use(pipes.ExtractState({ from: 'lockerAddress', to: 'lockerState', status: 'OK', table: 'stake' }));
|
|
154
|
+
runner.use(pipes.ExtractState({ from: 'stakeAddress', to: 'stakeState', status: 'OK', table: 'stake' }));
|
|
155
|
+
runner.use((context, next) => {
|
|
156
|
+
const { stakeState } = context;
|
|
157
|
+
if (stakeState && stakeState.revocable === true) {
|
|
158
|
+
return next(new Error('INVALID_STAKE_STATE', `Staking not locked for withdraw: ${stakeState.address}`));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return next();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// 8. update state: the fee splitting and token burning is done when withdraw finalized in rollup-block
|
|
165
|
+
runner.use(
|
|
166
|
+
async (context, next) => {
|
|
167
|
+
const { tx, itx, statedb, stakeState, senderState, lockerState, lockerAddress, stakeAddress } = context;
|
|
168
|
+
|
|
169
|
+
const total = getBNSum(itx.token.value, itx.actualFee, itx.maxFee);
|
|
170
|
+
const fee = getBNSum(itx.actualFee, itx.maxFee);
|
|
171
|
+
|
|
172
|
+
const senderUpdates = applyTokenUpdates([{ address: itx.token.address, value: total }], senderState, 'sub');
|
|
173
|
+
|
|
174
|
+
// Burned amount should equal to user received amount
|
|
175
|
+
const stakeUpdates = applyTokenUpdates(
|
|
176
|
+
[{ address: itx.token.address, value: itx.token.value }],
|
|
177
|
+
stakeState || {},
|
|
178
|
+
'add'
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Fees are locked to reward locker, and funded later
|
|
182
|
+
const lockerUpdates = applyTokenUpdates(
|
|
183
|
+
[{ address: itx.token.address, value: fee }],
|
|
184
|
+
lockerState || { tokens: {} },
|
|
185
|
+
'add'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const [newSenderState, newStakeState, newLockerState] = await Promise.all([
|
|
189
|
+
// update user account
|
|
190
|
+
statedb.account.update(
|
|
191
|
+
senderState.address,
|
|
192
|
+
account.update(senderState, { nonce: tx.nonce, ...senderUpdates }, context),
|
|
193
|
+
context
|
|
194
|
+
),
|
|
195
|
+
|
|
196
|
+
// lock withdraw amount to a not-revocable stake
|
|
197
|
+
stakeState
|
|
198
|
+
? statedb.stake.update(stakeAddress, stake.update(stakeState, stakeUpdates, context), context)
|
|
199
|
+
: statedb.stake.create(
|
|
200
|
+
stakeAddress,
|
|
201
|
+
stake.create(
|
|
202
|
+
{
|
|
203
|
+
address: stakeAddress,
|
|
204
|
+
sender: senderState.address,
|
|
205
|
+
receiver: itx.rollup,
|
|
206
|
+
revocable: false,
|
|
207
|
+
tokens: stakeUpdates.tokens,
|
|
208
|
+
message: 'withdraw-locker',
|
|
209
|
+
assets: [],
|
|
210
|
+
},
|
|
211
|
+
context
|
|
212
|
+
),
|
|
213
|
+
context
|
|
214
|
+
),
|
|
215
|
+
|
|
216
|
+
// Update pending fee locker state
|
|
217
|
+
lockerState
|
|
218
|
+
? statedb.stake.update(lockerState.address, stake.update(lockerState, lockerUpdates, context), context)
|
|
219
|
+
: statedb.stake.create(
|
|
220
|
+
lockerAddress,
|
|
221
|
+
stake.create(
|
|
222
|
+
{
|
|
223
|
+
address: lockerAddress,
|
|
224
|
+
sender: itx.rollup,
|
|
225
|
+
receiver: itx.rollup,
|
|
226
|
+
revocable: false,
|
|
227
|
+
message: 'pending-block-reward',
|
|
228
|
+
revokeWaitingPeriod: 0,
|
|
229
|
+
...lockerUpdates,
|
|
230
|
+
},
|
|
231
|
+
context
|
|
232
|
+
),
|
|
233
|
+
context
|
|
234
|
+
),
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
context.senderState = newSenderState;
|
|
238
|
+
context.stakeState = newStakeState;
|
|
239
|
+
context.lockerState = newLockerState;
|
|
240
|
+
context.stakeStates = [newStakeState, newLockerState];
|
|
241
|
+
|
|
242
|
+
context.updatedAccounts = [
|
|
243
|
+
{ address: senderState.address, token: itx.token.address, delta: `-${total}`, action: 'lock' },
|
|
244
|
+
{ address: stakeAddress, token: itx.token.address, delta: itx.token.value, action: 'lock' },
|
|
245
|
+
{ address: lockerAddress, token: itx.token.address, delta: fee, action: 'pending' },
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
debug('withdraw-token-v2', itx);
|
|
249
|
+
|
|
250
|
+
next();
|
|
251
|
+
},
|
|
252
|
+
{ persistError: true }
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
module.exports = runner;
|