Skip to content

Commit

Permalink
Add support for new multi send (#512)
Browse files Browse the repository at this point in the history
  • Loading branch information
rmeissner authored and elgatovital committed Dec 11, 2019
1 parent 06916a9 commit f4fae6d
Show file tree
Hide file tree
Showing 18 changed files with 341 additions and 169 deletions.
15 changes: 10 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ android {
buildConfigField javaTypes.BOOLEAN, "VERBOSE_EXCEPTIONS", getKey("VERBOSE_EXCEPTIONS", "false")
buildConfigField javaTypes.BOOLEAN, "ALLOW_RESTRICTED_TX", getKey("ALLOW_RESTRICTED_TX", "false")
// Contracts
buildConfigField javaTypes.STRING, "SAFE_MASTER_COPY_0_0_2", asString(getKey("SAFE_MASTER_COPY_0_0_2", "0xAC6072986E985aaBE7804695EC2d8970Cf7541A2")) // Version 0.0.2-alpha (Mainnet)
buildConfigField javaTypes.STRING, "SAFE_MASTER_COPY_0_1_0", asString(getKey("SAFE_MASTER_COPY_0_1_0", "0x8942595A2dC5181Df0465AF0D7be08c8f23C93af")) // Version 0.1.0 (All networks)
buildConfigField javaTypes.STRING, "SAFE_MASTER_COPY_1_0_0", asString(getKey("SAFE_MASTER_COPY_1_0_0", "0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A")) // Version 1.0.0 (All networks)
buildConfigField javaTypes.STRING, "SAFE_MASTER_COPY_0_0_2", asString(getKey("SAFE_MASTER_COPY_0_0_2", "0xAC6072986E985aaBE7804695EC2d8970Cf7541A2"))
// Version 0.0.2-alpha (Mainnet)
buildConfigField javaTypes.STRING, "SAFE_MASTER_COPY_0_1_0", asString(getKey("SAFE_MASTER_COPY_0_1_0", "0x8942595A2dC5181Df0465AF0D7be08c8f23C93af"))
// Version 0.1.0 (All networks)
buildConfigField javaTypes.STRING, "SAFE_MASTER_COPY_1_0_0", asString(getKey("SAFE_MASTER_COPY_1_0_0", "0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A"))
// Version 1.0.0 (All networks)
buildConfigField javaTypes.STRING, "PROXY_FACTORY_ADDRESS", asString(getKey("PROXY_FACTORY_ADDRESS", "0x12302fE9c02ff50939BaAaaf415fc226C078613C"))
buildConfigField javaTypes.STRING, "MULTI_SEND_ADDRESS", asString(getKey("MULTI_SEND_ADDRESS", "0xe74d6af1670fb6560dd61ee29eb57c7bc027ce4e"))
buildConfigField javaTypes.STRING, "MULTI_SEND_OLD_ADDRESS", asString(getKey("MULTI_SEND_ADDRESS", "0xe74d6af1670fb6560dd61ee29eb57c7bc027ce4e"))
buildConfigField javaTypes.STRING, "MULTI_SEND_ADDRESS", asString(getKey("MULTI_SEND_ADDRESS", "0x8D29bE29923b68abfDD21e541b9374737B49cdAD"))
buildConfigField javaTypes.STRING, "ENS_REGISTRY", asString(getKey("ENS_REGISTRY", "0xe7410170f87102df0055eb195163a03b7f2bff4a"))
// Safe creation params
buildConfigField javaTypes.INT, "PROXY_INIT_DATA_LENGTH", getKey("PROXY_INIT_DATA_LENGTH", "3006")
Expand Down Expand Up @@ -117,7 +121,7 @@ android {
buildConfigField javaTypes.STRING, "BLOCKCHAIN_NET_URL", asString(getKey("BLOCKCHAIN_NET_URL", "https://mainnet.infura.io/v3/"))
buildConfigField javaTypes.STRING, "RELAY_SERVICE_URL", asString(getKey("RELAY_SERVICE_URL", "https://safe-relay.gnosis.pm/api/"))
buildConfigField javaTypes.STRING, "SAFE_CREATION_FUNDER", asString(getKey("SAFE_CREATION_FUNDER", "0x07F455F30e862E13E3E3D960762cB11c4F744d52"))
buildConfigField javaTypes.STRING, "ENS_REGISTRY", asString(getKey("MULTI_SEND_ADDRESS", "0x314159265dd8dbb310642f98f50c066173c1259b"))
buildConfigField javaTypes.STRING, "ENS_REGISTRY", asString(getKey("ENS_REGISTRY", "0x314159265dd8dbb310642f98f50c066173c1259b"))
}
}

Expand All @@ -129,6 +133,7 @@ android {

packagingOptions {
exclude 'META-INF/rxjava.properties'
exclude 'META-INF/extensions.kotlin_module'
exclude 'META-INF/LICENSE'
}

Expand Down
2 changes: 2 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
# Okio
####################################################################################################
-dontwarn okio.**
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*

####################################################################################################
# Okhttp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ sealed class TransactionData : Parcelable {

@Parcelize
@TypeParceler<Solidity.Address, SolidityAddressParceler>
data class MultiSend(val transactions: List<SafeTransaction>) : TransactionData()
data class MultiSend(val transactions: List<SafeTransaction>, val contract: Solidity.Address) : TransactionData()

fun addToBundle(bundle: Bundle) =
bundle.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import pm.gnosis.heimdall.GnosisSafe
import pm.gnosis.heimdall.MultiSend
import pm.gnosis.heimdall.data.db.ApplicationDb
import pm.gnosis.heimdall.data.db.models.TransactionDescriptionDb
import pm.gnosis.heimdall.data.repositories.*
import pm.gnosis.heimdall.data.repositories.RestrictedTransactionException
import pm.gnosis.heimdall.data.repositories.TransactionData
import pm.gnosis.heimdall.data.repositories.TransactionExecutionRepository.Operation
import pm.gnosis.heimdall.data.repositories.TransactionInfo
import pm.gnosis.heimdall.data.repositories.TransactionInfoRepository
import pm.gnosis.heimdall.data.repositories.models.ERC20Token
import pm.gnosis.heimdall.data.repositories.models.SafeTransaction
import pm.gnosis.model.Solidity
Expand Down Expand Up @@ -63,17 +66,19 @@ class DefaultTransactionInfoRepository @Inject constructor(

override fun parseTransactionData(transaction: SafeTransaction): Single<TransactionData> =
Single.fromCallable {
val tx = transaction.wrapped
when {
transaction.isCall() -> tx.parseCall()
isMultiSend(transaction) && isReplaceRecoveryPhrase(transaction) -> TransactionData.ReplaceRecoveryPhrase(transaction)
transaction.isCall() -> transaction.wrapped.parseCall()
isMultiSend(transaction) -> parseMultiSend(transaction)
else ->
TransactionData.Generic(tx.address, tx.value?.value ?: BigInteger.ZERO, tx.data, transaction.operation)
else -> transaction.toGenericTransactionData()
}
}
.subscribeOn(Schedulers.io())

private fun SafeTransaction.toGenericTransactionData() =
wrapped.let { tx ->
TransactionData.Generic(tx.address, tx.value?.value ?: BigInteger.ZERO, tx.data, operation)
}

private fun SafeTransaction.isCall() = this.operation == Operation.CALL

private fun Transaction.parseCall() =
Expand All @@ -88,18 +93,41 @@ class DefaultTransactionInfoRepository @Inject constructor(
TransactionData.Generic(address, value?.value ?: BigInteger.ZERO, data, Operation.CALL)
}

// TODO: This need to be adjusted for the new MultiSend
private fun isMultiSend(safeTransaction: SafeTransaction) =
safeTransaction.operation == Operation.DELEGATE_CALL &&
safeTransaction.wrapped.address == MULTI_SEND_LIB &&
safeTransaction.wrapped.data != null &&
safeTransaction.wrapped.data!!.isSolidityMethod(MultiSend.MultiSend.METHOD_ID)

// TODO: This need to be adjusted for the new MultiSend
private fun parseMultiSend(transaction: SafeTransaction): TransactionData.MultiSend {
private fun parseMultiSend(transaction: SafeTransaction): TransactionData {
val payload =
transaction.wrapped.data?.removeSolidityMethodPrefix(MultiSend.MultiSend.METHOD_ID) ?: return TransactionData.MultiSend(emptyList())
transaction.wrapped.data?.removeSolidityMethodPrefix(MultiSend.MultiSend.METHOD_ID)
?: return TransactionData.MultiSend(emptyList(), transaction.wrapped.address)

val multiSend = when (transaction.wrapped.address) {
MULTI_SEND_LIB -> nullOnThrow { parseMultiSendNew(payload) }
MULTI_SEND_OLD_LIB -> nullOnThrow { parseMultiSendOld(payload) }
else -> null
} ?: return transaction.toGenericTransactionData()
return processMultiSend(transaction, multiSend)
}

private fun parseMultiSendNew(payload: String): TransactionData.MultiSend {
val transactions = mutableListOf<SafeTransaction>()
val data = MultiSend.MultiSend.decodeArguments(payload).transactions.items.toHexString()
val reader = PayloadReader(data)
while (reader.hasAdditional(85)) {
val operation = Operation.fromInt(reader.readAsHexInt(1))
val to = nullOnThrow { reader.read(20).asEthereumAddress() } ?: throw IllegalArgumentException("Illegal to")
val value = nullOnThrow { Wei(reader.readAsHexBigInteger(32)) } ?: throw IllegalArgumentException("Illegal value")
val dataSize = nullOnThrow { reader.readAsHexBigInteger(32) } ?: throw IllegalArgumentException("Missing data size")
val data = nullOnThrow { reader.read(dataSize.toInt()).hexToByteArray() }
transactions.add(SafeTransaction(Transaction(to, value = value, data = data?.toHex()?.addHexPrefix()), operation))
}

return TransactionData.MultiSend(transactions, MULTI_SEND_LIB)
}

private fun parseMultiSendOld(payload: String): TransactionData.MultiSend {
val transactions = mutableListOf<SafeTransaction>()
val partitions = SolidityBase.PartitionData.of(payload)
nullOnThrow { partitions.consume() } ?: throw IllegalArgumentException("Missing multisend data position")
Expand All @@ -115,21 +143,29 @@ class DefaultTransactionInfoRepository @Inject constructor(
current = nullOnThrow { partitions.consume() }
}

return TransactionData.MultiSend(transactions)
return TransactionData.MultiSend(transactions, MULTI_SEND_OLD_LIB)
}

private fun isReplaceRecoveryPhrase(transaction: SafeTransaction): Boolean {
val payload = transaction.wrapped.data?.removeSolidityMethodPrefix(MultiSend.MultiSend.METHOD_ID) ?: return false
private fun processMultiSend(transaction: SafeTransaction, multiSend: TransactionData.MultiSend) =
parseReplaceRecoveryPhrase(transaction, multiSend)
?: multiSend

val noPrefix = payload.removeHexPrefix()
if (noPrefix.length.rem(PADDED_HEX_LENGTH) != 0) throw IllegalArgumentException("Data is not a multiple of $PADDED_HEX_LENGTH")
val partitions = noPrefix.chunked(PADDED_HEX_LENGTH)
if (partitions.size != 20) return false
if (partitions[3] != partitions[12]) return false
private fun parseReplaceRecoveryPhrase(transaction: SafeTransaction, multiSend: TransactionData.MultiSend): TransactionData? {
if (multiSend.transactions.size != 2) return null

if (!partitions[7].startsWith(GnosisSafe.SwapOwner.METHOD_ID)) return false
if (!partitions[16].startsWith(GnosisSafe.SwapOwner.METHOD_ID)) return false
return true
// Needs to be a valid owner swap tx
val firstOwnerSwap = multiSend.transactions[0]
if (firstOwnerSwap.operation != Operation.CALL || firstOwnerSwap.wrapped.data?.isSolidityMethod(GnosisSafe.SwapOwner.METHOD_ID) != true)
return null

// Needs to be a valid owner swap tx
val secondOwnerSwap = multiSend.transactions[1]
if (secondOwnerSwap.operation != Operation.CALL || secondOwnerSwap.wrapped.data?.isSolidityMethod(GnosisSafe.SwapOwner.METHOD_ID) != true)
return null

// We need to swap owners at the same Safe
if (firstOwnerSwap.wrapped.address != secondOwnerSwap.wrapped.address) return null
return TransactionData.ReplaceRecoveryPhrase(transaction)
}

private fun parseTokenTransfer(transaction: Transaction): TransactionData.AssetTransfer {
Expand Down Expand Up @@ -168,8 +204,23 @@ class DefaultTransactionInfoRepository @Inject constructor(
Operation.values()[operation.toInt()]
)

private class PayloadReader(private val payload: String) {
private var index = 0

fun read(bytes: Int) = payload.substring(index, index + bytes * 2).apply {
index += bytes * 2
}

fun readAsHexBigInteger(bytes: Int) = read(bytes).hexAsBigInteger()

fun readAsHexInt(bytes: Int) = read(bytes).toInt(16)

fun hasAdditional(bytes: Int) = (index + bytes * 2) <= payload.length
}

companion object {
private val MULTI_SEND_LIB = BuildConfig.MULTI_SEND_ADDRESS.asEthereumAddress()!!
private val MULTI_SEND_OLD_LIB = BuildConfig.MULTI_SEND_OLD_ADDRESS.asEthereumAddress()!!
// These additional costs are hardcoded in the smart contract
private val SAFE_TX_BASE_COSTS = BigInteger.valueOf(32000)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ class WalletConnectBridgeRepository @Inject constructor(
.toList()
.subscribeBy(
onSuccess = {
showSendTransactionNotification(peerMeta, safe, TransactionData.MultiSend(txs), call.id, sessionId)
showSendTransactionNotification(peerMeta, safe, TransactionData.MultiSend(txs, MULTI_SEND_LIB), call.id, sessionId)
},
onError = { t -> rejectRequest(call.id, RejectionReason.AppError(t, MULTI_SEND_RPC)).subscribe() })
}
Expand Down Expand Up @@ -453,6 +453,7 @@ class WalletConnectBridgeRepository @Inject constructor(

companion object {
private const val CHANNEL_WALLET_CONNECT_REQUESTS = "channel_wallet_connect_requests"
private val MULTI_SEND_LIB = BuildConfig.MULTI_SEND_ADDRESS.asEthereumAddress()!!
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class AddressHelper @Inject constructor(
}
}
.flatMap {
if (address == MULTI_SEND_LIB)
if (address == MULTI_SEND_LIB || address == MULTI_SEND_OLD_LIB)
Single.just(addressView.context.getString(R.string.multi_send_contract))
else
addressBookRepository.loadAddressBookEntry(address).map { it.name }
Expand All @@ -64,5 +64,6 @@ class AddressHelper @Inject constructor(

companion object {
private val MULTI_SEND_LIB = BuildConfig.MULTI_SEND_ADDRESS.asEthereumAddress()!!
private val MULTI_SEND_OLD_LIB = BuildConfig.MULTI_SEND_OLD_ADDRESS.asEthereumAddress()!!
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ data class SimpleLocalizedException(override val message: String) : Exception(me
(throwable as HttpException).let {
@Suppress("ConstantConditionIf")
if (BuildConfig.VERBOSE_EXCEPTIONS) {
return@add "${throwable.code()} (${throwable.message()}): ${throwable.response().errorBody()?.string()}"
return@add "${throwable.code()} (${throwable.message()}): ${throwable.response()?.errorBody()?.string()}"
}
when (throwable.code()) {
HttpCodes.FORBIDDEN, HttpCodes.UNAUTHORIZED -> c.getString(R.string.error_not_authorized_for_action)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ abstract class BaseTransactionViewHolder<T>(
}
is TransactionData.MultiSend -> {
updateViews(
address = BuildConfig.MULTI_SEND_ADDRESS.asEthereumAddress()!!,
address = data.contract,
infoText = "${data.transactions.size} batched transaction",
valueText = null,
valueColor = R.color.blue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import pm.gnosis.heimdall.data.repositories.models.SafeTransaction
import pm.gnosis.heimdall.di.ApplicationContext
import pm.gnosis.heimdall.ui.exceptions.SimpleLocalizedException
import pm.gnosis.heimdall.ui.safe.mnemonic.InputRecoveryPhraseContract
import pm.gnosis.heimdall.ui.transactions.builder.MultiSendTransactionBuilder
import pm.gnosis.mnemonic.Bip39ValidationResult
import pm.gnosis.model.Solidity
import pm.gnosis.model.SolidityBase
Expand Down Expand Up @@ -223,21 +224,8 @@ class DefaultRecoverSafeOwnersHelper @Inject constructor(
TransactionExecutionRepository.Operation.CALL
)
else ->
SafeTransaction(
Transaction(
BuildConfig.MULTI_SEND_ADDRESS.asEthereumAddress()!!, data = MultiSend.MultiSend.encode(
Solidity.Bytes(
payloads.joinToString(separator = "") {
SolidityBase.encodeFunctionArguments(
Solidity.UInt8(BigInteger.ZERO),
safeInfo.address,
Solidity.UInt256(BigInteger.ZERO),
Solidity.Bytes(it.hexStringToByteArray())
)
}.hexStringToByteArray()
)
)
), TransactionExecutionRepository.Operation.DELEGATE_CALL
MultiSendTransactionBuilder.build(
payloads.map { SafeTransaction(Transaction(safeInfo.address, data=it), TransactionExecutionRepository.Operation.CALL) }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import pm.gnosis.models.Transaction
import pm.gnosis.models.Wei
import pm.gnosis.utils.asEthereumAddress
import pm.gnosis.utils.hexStringToByteArray
import pm.gnosis.utils.toHex
import java.math.BigInteger


Expand Down Expand Up @@ -64,17 +65,20 @@ object MultiSendTransactionBuilder {
private val MULTI_SEND_LIB = BuildConfig.MULTI_SEND_ADDRESS.asEthereumAddress()!!

fun build(data: TransactionData.MultiSend): SafeTransaction =
build(data.transactions)

fun build(transactions: List<SafeTransaction>) =
SafeTransaction(
Transaction(
MULTI_SEND_LIB, data = MultiSend.MultiSend.encode(
Solidity.Bytes(
data.transactions.joinToString(separator = "") {
SolidityBase.encodeFunctionArguments(
Solidity.UInt8(it.operation.toInt().toBigInteger()), // Operation
it.wrapped.address, // To
Solidity.UInt256(it.wrapped.value?.value ?: BigInteger.ZERO), // Value
Solidity.Bytes(it.wrapped.data?.hexStringToByteArray() ?: byteArrayOf()) // Data
)
transactions.joinToString(separator = "") {
val data = (it.wrapped.data?.hexStringToByteArray() ?: byteArrayOf())
Solidity.UInt8(it.operation.toInt().toBigInteger()).encodePacked() + // Operation
it.wrapped.address.encodePacked() + // To
Solidity.UInt256(it.wrapped.value?.value ?: BigInteger.ZERO).encodePacked() + // Value
Solidity.UInt256(data.size.toBigInteger()).encodePacked() + // Data length
Solidity.Bytes(data).encodePacked() // Data
}.hexStringToByteArray()
)
)
Expand Down
Loading

0 comments on commit f4fae6d

Please sign in to comment.