UNPKG

@dolomite-exchange/dolomite-margin

Version:

Ethereum Smart Contracts and TypeScript library used for the DolomiteMargin trading protocol

389 lines 19 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SignedOperations = void 0; const web3_1 = __importDefault(require("web3")); const Signer_1 = require("./Signer"); const Constants_1 = require("../lib/Constants"); const Helpers_1 = require("../lib/Helpers"); const BytesHelper_1 = require("../lib/BytesHelper"); const SignatureHelper_1 = require("../lib/SignatureHelper"); const types_1 = require("../types"); const EIP712_OPERATION_STRUCT = [ { type: 'Action[]', name: 'actions' }, { type: 'uint256', name: 'expiration' }, { type: 'uint256', name: 'salt' }, { type: 'address', name: 'sender' }, { type: 'address', name: 'signer' }, ]; const EIP712_ACTION_STRUCT = [ { type: 'uint8', name: 'actionType' }, { type: 'address', name: 'accountOwner' }, { type: 'uint256', name: 'accountNumber' }, { type: 'AssetAmount', name: 'assetAmount' }, { type: 'uint256', name: 'primaryMarketId' }, { type: 'uint256', name: 'secondaryMarketId' }, { type: 'address', name: 'otherAddress' }, { type: 'address', name: 'otherAccountOwner' }, { type: 'uint256', name: 'otherAccountNumber' }, { type: 'bytes', name: 'data' }, ]; const EIP712_ASSET_AMOUNT_STRUCT = [ { type: 'bool', name: 'sign' }, { type: 'uint8', name: 'denomination' }, { type: 'uint8', name: 'ref' }, { type: 'uint256', name: 'value' }, ]; const EIP712_ASSET_AMOUNT_STRING = 'AssetAmount(' + 'bool sign,' + 'uint8 denomination,' + 'uint8 ref,' + 'uint256 value' + ')'; const EIP712_ACTION_STRING = 'Action(' + // tslint:disable-line 'uint8 actionType,' + 'address accountOwner,' + 'uint256 accountNumber,' + 'AssetAmount assetAmount,' + 'uint256 primaryMarketId,' + 'uint256 secondaryMarketId,' + 'address otherAddress,' + 'address otherAccountOwner,' + 'uint256 otherAccountNumber,' + 'bytes data' + ')' + EIP712_ASSET_AMOUNT_STRING; const EIP712_OPERATION_STRING = 'Operation(' + // tslint:disable-line 'Action[] actions,' + 'uint256 expiration,' + 'uint256 salt,' + 'address sender,' + 'address signer' + ')' + EIP712_ACTION_STRING; const EIP712_CANCEL_OPERATION_STRUCT = [ { type: 'string', name: 'action' }, { type: 'bytes32[]', name: 'operationHashes' }, ]; const EIP712_CANCEL_OPERATION_STRUCT_STRING = 'CancelOperation(' + 'string action,' + 'bytes32[] operationHashes' + ')'; class SignedOperations extends Signer_1.Signer { // ============ Constructor ============ constructor(contracts, web3, networkId) { super(web3); this.contracts = contracts; this.networkId = networkId; } // ============ On-Chain Cancel ============ /** * Sends an transaction to cancel an operation on-chain. */ async cancelOperation(operation, options) { const accounts = []; const actions = []; const getAccountId = (accountOwner, accountNumber) => { if (accountOwner === Constants_1.ADDRESSES.ZERO) { return 0; } const accountInfo = { owner: accountOwner, number: accountNumber.toFixed(0), }; const index = accounts.indexOf(accountInfo); if (index >= 0) { return index; } accounts.push(accountInfo); return accounts.length - 1; }; for (let i = 0; i < operation.actions.length; i += 1) { const action = operation.actions[i]; actions.push({ accountId: getAccountId(action.primaryAccountOwner, action.primaryAccountNumber), actionType: action.actionType, primaryMarketId: Helpers_1.toString(action.primaryMarketId), secondaryMarketId: Helpers_1.toString(action.secondaryMarketId), otherAddress: action.otherAddress, otherAccountId: getAccountId(action.secondaryAccountOwner, action.secondaryAccountNumber), data: BytesHelper_1.hexStringToBytes(action.data), amount: { sign: action.amount.sign, ref: Helpers_1.toString(action.amount.ref), denomination: Helpers_1.toString(action.amount.denomination), value: Helpers_1.toString(action.amount.value), }, }); } return this.contracts.callContractFunction(this.contracts.signedOperationProxy.methods.cancel(accounts, actions, { numActions: operation.actions.length.toString(), header: { expiration: operation.expiration.toFixed(0), salt: operation.salt.toFixed(0), sender: operation.sender, signer: operation.signer, }, signature: [], }), options || { from: operation.signer }); } // ============ Getter Contract Methods ============ /** * Returns true if the contract can process operations. */ async isOperational(options) { return this.contracts.callConstantContractFunction(this.contracts.signedOperationProxy.methods.g_isOperational(), options); } /** * Gets the status and the current filled amount (in makerAmount) of all given orders. */ async getOperationsAreInvalid(operations, options) { const hashes = operations.map(operation => this.getOperationHash(operation)); return this.contracts.callConstantContractFunction(this.contracts.signedOperationProxy.methods.getOperationsAreInvalid(hashes), options); } // ============ Signing Methods ============ /** * Sends operation to current provider for signing. Can sign locally if the signing account is * loaded into web3 and SigningMethod.Hash is used. */ async signOperation(operation, signingMethod) { switch (signingMethod) { case types_1.SigningMethod.Hash: case types_1.SigningMethod.UnsafeHash: case types_1.SigningMethod.Compatibility: const hash = this.getOperationHash(operation); const rawSignature = await this.web3.eth.sign(hash, operation.signer); const hashSig = SignatureHelper_1.createTypedSignature(rawSignature, SignatureHelper_1.SIGNATURE_TYPES.DECIMAL); if (signingMethod === types_1.SigningMethod.Hash) { return hashSig; } const unsafeHashSig = SignatureHelper_1.createTypedSignature(rawSignature, SignatureHelper_1.SIGNATURE_TYPES.NO_PREPEND); if (signingMethod === types_1.SigningMethod.UnsafeHash) { return unsafeHashSig; } if (this.operationByHashHasValidSignature(hash, unsafeHashSig, operation.signer)) { return unsafeHashSig; } return hashSig; case types_1.SigningMethod.TypedData: case types_1.SigningMethod.MetaMask: case types_1.SigningMethod.MetaMaskLatest: case types_1.SigningMethod.CoinbaseWallet: return this.ethSignTypedOperationInternal(operation, signingMethod); default: throw new Error(`Invalid signing method ${signingMethod}`); } } /** * Sends operation to current provider for signing of a cancel message. Can sign locally if the * signing account is loaded into web3 and SigningMethod.Hash is used. */ async signCancelOperation(operation, signingMethod) { return this.signCancelOperationByHash(this.getOperationHash(operation), operation.signer, signingMethod); } /** * Sends operationHash to current provider for signing of a cancel message. Can sign locally if * the signing account is loaded into web3 and SigningMethod.Hash is used. */ async signCancelOperationByHash(operationHash, signer, signingMethod) { switch (signingMethod) { case types_1.SigningMethod.Hash: case types_1.SigningMethod.UnsafeHash: case types_1.SigningMethod.Compatibility: const cancelHash = this.operationHashToCancelOperationHash(operationHash); const rawSignature = await this.web3.eth.sign(cancelHash, signer); const hashSig = SignatureHelper_1.createTypedSignature(rawSignature, SignatureHelper_1.SIGNATURE_TYPES.DECIMAL); if (signingMethod === types_1.SigningMethod.Hash) { return hashSig; } const unsafeHashSig = SignatureHelper_1.createTypedSignature(rawSignature, SignatureHelper_1.SIGNATURE_TYPES.NO_PREPEND); if (signingMethod === types_1.SigningMethod.UnsafeHash) { return unsafeHashSig; } if (this.cancelOperationByHashHasValidSignature(operationHash, unsafeHashSig, signer)) { return unsafeHashSig; } return hashSig; case types_1.SigningMethod.TypedData: case types_1.SigningMethod.MetaMask: case types_1.SigningMethod.MetaMaskLatest: case types_1.SigningMethod.CoinbaseWallet: return this.ethSignTypedCancelOperationInternal(operationHash, signer, signingMethod); default: throw new Error(`Invalid signing method ${signingMethod}`); } } // ============ Signing Cancel Operation Methods ============ // noinspection JSUnusedGlobalSymbols /** * Uses web3.eth.sign to sign a cancel message for an operation. This signature is not used * on-chain,but allows backend services to verify that the cancel operation api call is from * the original maker of the operation. */ async ethSignCancelOperation(operation) { return this.ethSignCancelOperationByHash(this.getOperationHash(operation), operation.signer); } /** * Uses web3.eth.sign to sign a cancel message for an operation hash. This signature is not used * on-chain, but allows dYdX backend services to verify that the cancel operation api call is from * the original maker of the operation. */ async ethSignCancelOperationByHash(operationHash, signer) { const cancelHash = this.operationHashToCancelOperationHash(operationHash); const signature = await this.web3.eth.sign(cancelHash, signer); return SignatureHelper_1.createTypedSignature(signature, SignatureHelper_1.SIGNATURE_TYPES.DECIMAL); } // ============ Signature Verification ============ /** * Returns true if the operation object has a non-null valid signature from the maker of the * operation. */ operationHasValidSignature(signedOperation) { return this.operationByHashHasValidSignature(this.getOperationHash(signedOperation), signedOperation.typedSignature, signedOperation.signer); } /** * Returns true if the operation hash has a non-null valid signature from a particular signer. */ operationByHashHasValidSignature(operationHash, typedSignature, expectedSigner) { const signer = SignatureHelper_1.ecRecoverTypedSignature(operationHash, typedSignature); return BytesHelper_1.addressesAreEqual(signer, expectedSigner); } /** * Returns true if the cancel operation message has a valid signature. */ cancelOperationHasValidSignature(operation, typedSignature) { return this.cancelOperationByHashHasValidSignature(this.getOperationHash(operation), typedSignature, operation.signer); } /** * Returns true if the cancel operation message has a valid signature. */ cancelOperationByHashHasValidSignature(operationHash, typedSignature, expectedSigner) { const cancelHash = this.operationHashToCancelOperationHash(operationHash); const signer = SignatureHelper_1.ecRecoverTypedSignature(cancelHash, typedSignature); return BytesHelper_1.addressesAreEqual(signer, expectedSigner); } // ============ Hashing Functions ============ /** * Returns the final signable EIP712 hash for approving an operation. */ getOperationHash(operation) { const structHash = web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(EIP712_OPERATION_STRING) }, { t: 'bytes32', v: this.getActionsHash(operation.actions) }, { t: 'uint256', v: Helpers_1.toString(operation.expiration) }, { t: 'uint256', v: Helpers_1.toString(operation.salt) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(operation.sender) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(operation.signer) }); return this.getEIP712Hash(structHash); } /** * Returns the EIP712 hash of the actions array. */ getActionsHash(actions) { const actionsAsHashes = actions.length ? actions .map(action => BytesHelper_1.stripHexPrefix(this.getActionHash(action))) .join('') : ''; return BytesHelper_1.hashBytes(actionsAsHashes); } /** * Returns the EIP712 hash of a single Action struct. */ getActionHash(action) { return web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(EIP712_ACTION_STRING) }, { t: 'uint256', v: Helpers_1.toString(action.actionType) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(action.primaryAccountOwner) }, { t: 'uint256', v: Helpers_1.toString(action.primaryAccountNumber) }, { t: 'bytes32', v: this.getAssetAmountHash(action.amount) }, { t: 'uint256', v: Helpers_1.toString(action.primaryMarketId) }, { t: 'uint256', v: Helpers_1.toString(action.secondaryMarketId) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(action.otherAddress) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(action.secondaryAccountOwner) }, { t: 'uint256', v: Helpers_1.toString(action.secondaryAccountNumber) }, { t: 'bytes32', v: BytesHelper_1.hashBytes(action.data) }); } /** * Returns the EIP712 hash of an AssetAmount struct. */ getAssetAmountHash(amount) { return web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(EIP712_ASSET_AMOUNT_STRING) }, { t: 'uint256', v: Helpers_1.toString(amount.sign ? 1 : 0) }, { t: 'uint256', v: Helpers_1.toString(amount.denomination) }, { t: 'uint256', v: Helpers_1.toString(amount.ref) }, { t: 'uint256', v: Helpers_1.toString(amount.value) }); } /** * Given some operation hash, returns the hash of a cancel-operation message. */ operationHashToCancelOperationHash(operationHash) { const structHash = web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(EIP712_CANCEL_OPERATION_STRUCT_STRING) }, { t: 'bytes32', v: BytesHelper_1.hashString('Cancel Operations') }, { t: 'bytes32', v: web3_1.default.utils.soliditySha3({ t: 'bytes32', v: operationHash }), }); return this.getEIP712Hash(structHash); } /** * Returns the EIP712 domain separator hash. */ getDomainHash() { return web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(SignatureHelper_1.EIP712_DOMAIN_STRING) }, { t: 'bytes32', v: BytesHelper_1.hashString('SignedOperationProxy') }, { t: 'bytes32', v: BytesHelper_1.hashString('1.1') }, { t: 'uint256', v: Helpers_1.toString(this.networkId) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(this.contracts.signedOperationProxy.options.address), }); } getNetworkDomainHash() { return this.contracts.signedOperationProxy.methods.EIP712_DOMAIN_HASH().call(); } /** * Returns a signable EIP712 Hash of a struct */ getEIP712Hash(structHash) { return web3_1.default.utils.soliditySha3({ t: 'bytes2', v: '0x1901' }, { t: 'bytes32', v: this.getDomainHash() }, { t: 'bytes32', v: structHash }); } // ============ Private Helper Functions ============ getDomainData() { return { name: 'SignedOperationProxy', version: '1.1', chainId: this.networkId, verifyingContract: this.contracts.signedOperationProxy.options.address, }; } async ethSignTypedOperationInternal(operation, signingMethod) { const actionsData = operation.actions.map((action) => { return { actionType: Helpers_1.toString(action.actionType), accountOwner: action.primaryAccountOwner, accountNumber: Helpers_1.toString(action.primaryAccountNumber), assetAmount: { sign: action.amount.sign, denomination: Helpers_1.toString(action.amount.denomination), ref: Helpers_1.toString(action.amount.ref), value: Helpers_1.toString(action.amount.value), }, primaryMarketId: Helpers_1.toString(action.primaryMarketId), secondaryMarketId: Helpers_1.toString(action.secondaryMarketId), otherAddress: action.otherAddress, otherAccountOwner: action.secondaryAccountOwner, otherAccountNumber: Helpers_1.toString(action.secondaryAccountNumber), data: action.data, }; }); const operationData = { actions: actionsData, expiration: operation.expiration.toFixed(0), salt: operation.salt.toFixed(0), sender: operation.sender, signer: operation.signer, }; const data = { types: { EIP712Domain: SignatureHelper_1.EIP712_DOMAIN_STRUCT, Operation: EIP712_OPERATION_STRUCT, Action: EIP712_ACTION_STRUCT, AssetAmount: EIP712_ASSET_AMOUNT_STRUCT, }, domain: this.getDomainData(), primaryType: 'Operation', message: operationData, }; return this.ethSignTypedDataInternal(operation.signer, data, signingMethod); } async ethSignTypedCancelOperationInternal(operationHash, signer, signingMethod) { const data = { types: { EIP712Domain: SignatureHelper_1.EIP712_DOMAIN_STRUCT, CancelOperation: EIP712_CANCEL_OPERATION_STRUCT, }, domain: this.getDomainData(), primaryType: 'CancelOperation', message: { action: 'Cancel Operations', operationHashes: [operationHash], }, }; return this.ethSignTypedDataInternal(signer, data, signingMethod); } } exports.SignedOperations = SignedOperations; //# sourceMappingURL=SignedOperations.js.map