deployment: vault contract updates (#199)

Updates to the API of the Vault contract as worked on in codex-storage/codex-contracts-eth#220

Most important changes:
- token transfers are much more restricted in the vault, improving its safety
- when a storage request fails, only the providers at fault are punished, the rest are not
-  when a storage request fails, the client is only reimbursed for the remaining time in the contract
-  no longer possible for the marketplace to request one final storage proof before allowing withdrawal
-  repair reward is temporarily stored with the client's funds while slot is free, and not yet filled; this could lead to client withdrawing repair rewards when slots are free when the request ends
This commit is contained in:
markspanbroek 2025-03-10 10:26:15 +01:00 committed by GitHub
parent e99af7d03e
commit 4a5ddf0e52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -29,19 +29,19 @@ Requests on the new smart contract, but it will still fulfill the duties of the
contract.
```
┌─────────────────────────────────────────
│ Contract v2
┌───────────────────────────────┼────────────────────┬──────────────────────►
┌─────────────────────────────────────────
│ Contract v2
┌───────────────────────────────┼────────────────────┬──────────────────────►
│ Contract v1 │┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼│ time
└───────────────────────────────┼────────────────────┤
│ Grace period │
│ │
│ │
│ │
│ │
│ │
Codex release with Codex release that
new smart contract removes old contract
└───────────────────────────────┼────────────────────┤
│ Grace period │
│ │
│ │
│ │
│ │
│ │
Codex release with Codex release that
new smart contract removes old contract
```
An important consideration for this upgrade mechanism is that we plan to limit the maximum duration of Storage Requests.
@ -102,7 +102,7 @@ initial ideals of a permissionless approach.
### Upgradable contract
OpenZeppelin has tooling for writing [Upgradable contracts](https://docs.openzeppelin.com/upgrades-plugins/1.x/). While
this approach has some technical limitations, it should be sufficient for realizing bug fixes as part of emergency upgrades.
this approach has some technical limitations, it should be sufficient for realizing bug fixes as part of emergency upgrades.
This would require an admin role that would have this capability.
While this solution provides the most capable and flexible way to ensure security, it also gives "too much power" for
@ -191,71 +191,128 @@ contracts — a business logic contract and a funds vault contract.
We will reference the business logic contract as **Marketplace contract** as it will contain the Marketplace logic.
It will be possible to perform emergency upgrades using concepts described in the [Upgradable contract](#upgradable-contract) section.
In this way, if there is a bug/exploit that would affect the funds, it is possible to patch it quickly.
The original "feature upgrade" path still holds with this approach, where this business logic contract would get upgraded
The original "feature upgrade" path still holds with this approach, where this business logic contract would get upgraded
as discussed in the [Feature upgrades](#feature-upgrades) section.
The admin role would belong to multisig and be maintained inside the organization. This contract would not hold any funds as
it would delegate them to the Vault contract.
Vault contract will have the responsibility to keep users' funds safe. It should have minimal logic incorporated to minimize
the attack surface. Ideally, the Vault contract will be deployed once and never altered as it will have one simple task
the attack surface. Ideally, the Vault contract will be deployed once and never altered as it will have one simple task
to do, and there should not be a need to extend it. This contract will not incorporate the upgradability capabilities,
but as a safety measure, it will be possible to freeze it as described in the [Freezing contract](#freezing-contract) section.
but as a safety measure, it will be possible to freeze it as described in the [Freezing contract](#freezing-contract) section.
Freezing the Vault would be done in case of severe exploitable bugs.
Thanks to this splitting of the contract, we will limit the liabilities over funds control as described
in the [Upgradable contract](#upgradable-contract) section, but at the same it gives us the flexibility to react to
Thanks to this splitting of the contract, we will limit the liabilities over funds control as described
in the [Upgradable contract](#upgradable-contract) section, but at the same it gives us the flexibility to react to
unforeseen situations.
The Vault contract should have logic that prevents the simultaneous draining of all the funds. We came up with two designs for
this - time-based locking and recipient-based locking. The time-based locking Vault is described in depth below.
The recipient-base Vault works with a locking schema where the funds have a predefined set of recipients to which the funds
can be transferred. In this way, the hacker can't redirect the funds to their controlled accounts.
Unfortunately, this concept is not applicable to Marketplace because of slot repairs, when one slot's host is replaced
with another, which would require reallocating funds and hence open an opportunity for hackers to redirect the funds to
their accounts.
The Vault contract should have logic that prevents the simultaneous draining of all the funds. We came up with two ideas for
this - time-based locking and recipient-based locking. The Vault described below utilizes both ideas.
### Time-based Vault
### Vault
This Vault works on locking the funds until a certain time threshold is reached when it allows them to be spent. In this way
there is only a tiny fraction of the funds possible to be spent at a given time by the "business logic" contract as
we assume nodes will proactively and quickly collect their funds when able. If there is an exploit on the business logic
This Vault works on locking the funds until a certain time threshold is reached when it allows them to be withdrawn. In this way
there is only a tiny fraction of the funds possible to be redirected at a given time by the "business logic" contract as
the vault only allows manipulation of funds while they are time-locked. If there is an exploit on the business logic
contract, the attacker could withdraw only a small amount of funds.
We envision the following API of the Vault contract:
```solidity
contract TimeVault {
/// Creates new deposit with the given amount transferred from account "fromAccount" and locks it till spendable_from_timestamp
function deposit(uint256 amount, addr fromAccount, uint256 spendable_from_timestamp) returns (DepositId)
/// Deposits more funds to the already existing deposit
function deposit(uint256 amount, addr fromAccount, uint256 spendable_from_timestamp, DepositId id) returns (DepositId)
/// Extends the timelock of the specified deposit
function extend(DepositId id, uint256 spendable_from_timestamp)
/// Lower deposit amount of funds from the specified deposit
function burn(DepositId id, uint256 amount)
/// Transfer the given amount to the recipient, provided the block's timestamp is after the deposit's time lock
function spend(DepositId id, addr recipient, uint256 amount)
contract Vault {
/// Locks the fund until the expiry timestamp. The lock expiry can be extended
/// later, but no more than the maximum timestamp.
function lock(FundId fundId, Timestamp expiry, Timestamp maximum) {}
/// Deposits an amount of tokens into the vault, and adds them to the balance
/// of the account. ERC20 tokens are transfered from the caller to the vault
/// contract.
function deposit(FundId fundId, AccountId accountId, uint128 amount) {}
/// Takes an amount of tokens from the account balance and designates them
/// for the account holder. These tokens are no longer available to be
/// transfered to other accounts.
function designate(FundId fundId, AccountId accountId, uint128 amount) {}
/// Transfers an amount of tokens from one acount to the other.
function transfer(FundId fundId, AccountId from, AccountId to, uint128 amount) {}
/// Transfers tokens from one account to the other over time.
function flow(FundId fundId, AccountId from, AccountId to, TokensPerSecond rate) {}
/// Delays unlocking of a locked fund.
function extendLock(FundId fundId, Timestamp expiry) {}
/// Burns an amount of designated tokens from the account.
function burnDesignated(FundId fundId, AccountId accountId, uint128 amount) {}
/// Burns all tokens from the account.
function burnAccount(FundId fundId, AccountId accountId) {}
/// Freezes a fund. Stops all tokens flows and disallows any operations on the
/// fund until it unlocks.
function freezeFund(FundId fundId) {}
/// Transfers all ERC20 tokens in the account out of the vault to the account
/// holder.
function withdraw(FundId fundId, AccountId accountId) {}
/// Allows an account holder to withdraw its tokens from a fund directly,
/// bypassing the need to ask the controller of the fund to initiate the
/// withdrawal.
function withdrawByRecipient(Controller controller, FundId fundId, AccountId accountId) {}
}
```
_Important note is that this is a standalone contract, and it needs to keep track of the "owner of the deposit".
Hence, only the address that performs initial deposit can manipulate the funds later on._
_Important note is that this is a standalone contract, and it needs to keep track of the "controller of the fund".
Hence, the funds for each controller are independent, and each controller can only manipulate its own funds._
Integration into the Marketplace contract would be in the following way:
- `deposit()` reward funds upon `requestStorage()` call with timelock until the Request's `expiry`
- If the Request starts, the timelock is extended till the Request's end
- If the Request expires, then funds (partial payouts for hosts and remaining funds for a client) can be withdrawn when requested
- `deposit(depositId)` collateral funds to existing Request's deposit upon `fillSlot()`
- If the Request expires, the collateral can be withdrawn together with partial payouts
- Upon slot's being freed because of the host being kicked out, then `burn()` host's collateral lowered by the amount dedicated repair reward
- Upon Request's end, collateral and rewards can be `spend()`
- Upon Request's failure, all host's collateral is `burn()`. The original reward can be `spend()` back to the client, but only after the Request's end
- `RequestId`s are used as `FundId` identifiers in the vault.
- When storage is requested by a client, it leads to the following calls on the
vault:
- `lock(requestId, request.expiry, request.end)`
- `deposit(requestId, clientAccount, request.price)`
- When a slot is filled by a provider, the associated collateral is deposited
and designated, and some of the client tokens flow to the provider:
- `deposit(requestId, providerAccount, collateral)`
- `designate(requestId, providerAccount, collateral - repairReward)`
- `flow(requestId, clientAccount, providerAccount, pricePerSlotPerSecond)`
- When a request starts, the time lock is extended:
- `extendLock(requestId, request.end)`
- When a request ends, then the vault will allow hosts and client to withdraw
their tokens. This consists of collateral and partial payouts for hosts and
any remaining funds for the client:
- either: `withdraw(requestId, account)`
- or: `withdrawByRecipient(marketplace, requestId, account)`
- When a provider misses a storage proof, then a part of its collateral is
burned:
- `burnDesignated(requestId, providerAccount, slashAmount)`
- When a slot is freed because a provider missed too many proofs, then the
repair reward is set aside, the flow of tokens to the provider is reversed,
and the rest of the provider tokens are burned:
- `transfer(requestId, providerAccount, clientAccount, repairReward)`
- `flow(requestId, providerAccount, clientAccount, pricePerSlotPerSecond)`
- `burnAccount(requestId, providerAccount)`
- When a slot is repaired then the repair reward is transfered to the new
provider:
- `transfer(requestId, clientAccount, providerAccount, repairReward)`
- When a request fails, the fund is frozen:
- `freezeFund(requestId)`
The main disadvantage of this approach is that when the Request fails, the client can collect its original funds only after the Request's end.
The main downsides of this approach are:
- it is no longer possible to burn the funds of all providers when a storage
request fails; only the funds of providers that failed to provide storage
proofs are burned
- it is no longer possible to return all funds to the client when a storage
request fails; a client is only refunded for the remaining time
- when a request ends, then everyone can withdraw directly from the vault,
so it's not possible for the marketplace to e.g. request one final storage
proof before allowing withdrawal
We believe that the added safety of using a time vault is important enough to
accept these downsides.
### Contract's architecture
@ -265,11 +322,11 @@ Codex node will not have to be bothered about Vault's existence as every fund's
through the Marketplace contract. The Codex node will have a hardcoded Marketplace contract address with which it will interact.
```
┌────────────┐ ┌──────────────────────┐ ┌────────────────┐
│ │ │ │ │ │
│ Codex Node ├────►│ Marketplace contract ├────►│ Vault contract │
│ │ │ │ │ │
└────────────┘ └──────────────────────┘ └────────────────┘
┌────────────┐ ┌──────────────────────┐ ┌────────────────┐
│ │ │ │ │ │
│ Codex Node ├────►│ Marketplace contract ├────►│ Vault contract │
│ │ │ │ │ │
└────────────┘ └──────────────────────┘ └────────────────┘
```
### Scenarios handling
@ -305,10 +362,9 @@ in the [Freezing contract](#freezing-contract) section.
> [!CAUTION]
> The multisig account will not have direct control over the funds as they will be in the safekeeping of the Vault.
> But with the current Vault design, only the depositor can manipulate the funds, which in our case
> is the Marketplace contract that the multisig has control over. Maybe we should consider how users could
> withdraw funds from the Vault directly without interacting through the Marketplace contract. That will have to be
> thoroughly considered, as allowing such a thing could hinder the security guarantees of the Vault.
> But with the current Vault design, only the controller can manipulate the funds, which in our case
> is the Marketplace contract that the multisig has control over. This is why users can withdraw funds from the
> Vault directly without interacting through the Marketplace contract.
#### Vault emergency