Здесь есть интересный компромисс: больше логики в смарт-контрактах можно использовать для реализации большего количества функций безопасности: тайм-аутов, лимитов расходов, мультиподписи, хранилищ и т. Д. Однако чем больше логики в смарт-контракте, тем больше поверхность атаки и тем больше вероятность появления ошибок, которые могут подорвать функции безопасности.
Мы можем рассматривать спектр инструментов управления активами от самых простых до более сложных. Самый простой способ защитить ваш эфир - использовать один закрытый ключ, соответствующий адресу Ethereum, иногда называемому «Учетная запись конечного пользователя». С этим методом вообще не нужно беспокоиться о логике смарт-контрактов, поэтому мы устранили этот риск. Однако использование всего лишь одного ключа означает, что у вас есть единственная точка отказа.
На другом конце спектра вы можете создавать очень сложные контракты с кошельком для управления своими средствами. У Ethereum Foundation есть один контракт кошелька, который они активно используют, и недавно Gnosis представил сложный кошелек с несколькими подписями, поддерживающий лимиты расходов, административный контроль и использование рабочего процесса, при котором владельцы подтверждают транзакции, отправленные другими.
Простой контракт с несколькими подписями
Мы хотели изучить, как может выглядеть простейший контракт с несколькими подписями. Он должен иметь возможность для порогового количества держателей ключей собираться вместе и перемещать средства, но для обеспечения простоты нам не нужны более продвинутые функции, такие как лимиты расходов или возможность обновлять подписывающих лиц.
Вдохновением является то, как мультиподписи реализованы в Биткойне, где они поддерживаются непосредственно на языке сценариев в виде кода операции OP_CHECKMULTISIG
. На сегодняшний день мне неизвестно ни одного случая кражи или потери биткойнов из-за неисправного скрипта мультиподписи биткойнов.
В итоге мы получили контракт, который выталкивает большую часть логики за пределы цепочки, где каждый владелец мульти-подписи отвечает за создание подписи, которая разрешает транзакции, а затем используется одна функция для представления всех подписей к контракту для проверки. Мы используем предложенную спецификацию ERC191, которая является попыткой стандартизации формата подписей. Одно целое число nonce используется для предотвращения атак повторного воспроизведения.
Что касается пользовательского интерфейса, идея состоит в том, что у каждого держателя ключей с несколькими подписями будет пользовательский интерфейс, в котором они будут вводить данные транзакции, которую они хотят отправить (в идеале, на автономном компьютере). Затем на онлайн-машине «оператор» собирал все подписи от держателя ключей и отправлял фактическую транзакцию, содержащую все подписи. Оператору не нужно будет иметь какой-либо фактический контроль над средствами, держатели ключей с несколькими подписями в конечном итоге являются теми, кто имеет право выполнять транзакции.
Полный код представлен здесь:
pragma solidity 0.4.14;
contract SimpleMultiSig {
uint public nonce; // (only) mutable state
uint public threshold; // immutable state
mapping (address => bool) isOwner; // immutable state
address[] public ownersArr; // immutable state
function SimpleMultiSig(uint threshold_, address[] owners_) {
if (owners_.length > 10 || threshold_ > owners_.length || threshold_ == 0) {throw;}
for (uint i=0; i<owners_.length; i++) {
isOwner[owners_[i]] = true;
}
ownersArr = owners_;
threshold = threshold_;
}
// Note that address recovered from signatures must be strictly increasing
function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) {
if (sigR.length != threshold) {throw;}
if (sigR.length != sigS.length || sigR.length != sigV.length) {throw;}
// Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191
bytes32 txHash = sha3(byte(0x19), byte(0), this, destination, value, data, nonce);
address lastAdd = address(0); // cannot have address(0) as an owner
for (uint i = 0; i < threshold; i++) {
address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]);
if (recovered <= lastAdd || !isOwner[recovered]) throw;
lastAdd = recovered;
}
// If we make it here all signatures are accounted for
nonce = nonce + 1;
if (!destination.call.value(value)(data)) {throw;}
}
function () payable {}
}
и код также доступен в следующем репозитории github.
Преимущества
Некоторые полезные свойства этого контракта:
- Минимальная кодовая база: всего 40 строк кода
- Минимальное изменяемое состояние: единственные изменяемые данные - это одно
uint
, увеличивающееся при каждом выполнении. - Минимальный интерфейс: интерфейс состоит из одной функции
- Может отправлять произвольные транзакции, поэтому поддерживает токены
Отсутствие сложных переходов между состояниями делает невозможным завершение контракта в замороженном состоянии с недоступными средствами, как это описано здесь Эмином Гюн Сирером. Поскольку единственно возможный переход между состояниями - это простой увеличивающийся счетчик, контракт всегда будет в правильном состоянии. Маловероятно, что счетчик переполнится, поскольку используется 32-байтовое целое число. Кроме того, тестирование упрощается тем, что тестируется только одна функция (помимо конструктора).
Поскольку мы максимально упростили внутричейн-логику, сложность рабочего процесса вне сети увеличивается, что приводит к следующим недостаткам:
- Требует, чтобы пользователь подписал данные, не связанные с транзакциями, что может помешать использованию некоторых аппаратных кошельков.
- Конечным пользователям необходимо координировать действия вне сети, чтобы отправить транзакцию.
Будущая работа: формальная проверка
Простота контракта может сделать его хорошим кандидатом для создания формальной спецификации и выполнения формальной проверки с использованием этой спецификации - математического доказательства того, что код соответствует спецификации. Для этого может потребоваться переписать контракт на языке, на котором компиляция в EVM также была формально проверена. Формальная проверка EVM в последнее время привлекает все большее внимание с недавним выпуском формальной семантики для EVM.
Следующим шагом может быть написание простого контракта с несколькими подписями в LLL или даже в чистом байт-коде EVM, чтобы ограничить риски со стороны компилятора Solidity.
Резюме
В этом посте был рассмотрен более простой тип контракта Ethereum с несколькими подписями, в котором отдельные подписи агрегируются вне сети перед отправкой их всех в одной транзакции. Это упрощает код смарт-контракта в цепочке, чтобы упростить его просмотр и открывает возможность для выполнения формальной проверки в будущем.
Благодарим Нейта Раша за предложения по улучшению контракта.