Skip to content

Latest commit

 

History

History
507 lines (370 loc) · 14 KB

0001-asset-trait.md

File metadata and controls

507 lines (370 loc) · 14 KB

Introduce the Asset trait

Introduction

Smart contracts can carry out sensitive operations, such as transferring currency to another account. We introduce the Asset trait which represents items of value (for example, currency such as Wei). Asset types support a restricted set of operations and have their own semantics.

Assets can be transferred from/to other Assets of the same type (for example, transferring Wei from one variable to another). By default, it is not possible to create an Asset from a raw type (such as an Integer), and they cannot be implicitly destroyed.

In the context of currency, smart contracts often use state properties to record information about the balance they possess. So far, making such properties accurately reflect the balance the contract actually possesses had to be done manually by the programmer. Oversights, such as forgetting to update a state property, might lead to inconsistencies between a smart contract's actual balance and its state properties' view. Asset types provide a safe way of handling currency in Flint.

Making Wei and other currency types implement Asset allow the contract's state to always accurately represent the actual contract's balance (by default). The type system enforces Wei transfers to be recorded in the contract's state. Adding Wei to a contract can be done safely through an @payable function.

// Wei implements Asset

contract Bank {
  var balances: [Address: Wei]
}

Bank :: account <- (balances.keys) {
  @payable
  mutating func deposit(implicit value: inout Wei) {
    // Omitting this line causes a compiler warning: the value received should be recorded.
    balances[address].transfer(&value)
  }
  
  mutating func withdraw() {
    // balances[account] is automatically set to 0 before transferring.
    send(account, &balances[account])
  }
}

Motivation

Numerous attacks targeting smart contracts, such as ones relating to reentrancy calls, allow hackers to steal a current's Ether balance.

The following Bank contract records the balances of its customers, and implicitly assumes that the sum of all the balances reflects exactly the total amount of Wei the bank received. When supporting withdraw and deposit operations, the programmer needs to manually update the balances dictionary to reflect the changes.

In the following example, if any of the lines ⍺ or β were omitted, the contract's state wouldn't be accurately representing the total amount it has. Omitting line β is more dangerous: we would be sending Wei without recording it in our state. A customer could withdraw the same amount until the bank's balance is completely exhausted.

contract Bank {
  var balances: [Address: Wei]
}

Bank :: account <- (balances.keys) {
  @payable
  mutating func deposit(implicit value: inout Wei) {
    balances[account] += value // ⍺
  }
  
  mutating func withdraw() {
    send(account, balances[account])
    balances[account] = 0 // β
  }
}

The following Solidity contracts show how call reentrancy can result in contracts sending more Wei than they intended to. The withdraw function retrieves the balance of the given account, transfers it back, then sets it to 0. On line 13, an external call is performed using the low-level call function, attaching a Wei value. No function signature is specified, so the target’s fallback function is called. The vulnerability is exploited if the target’s fallback function calls back into withdraw(address). Lines 11–13 will be executed again, without having set the recipient’s balance to 0. Vulnerable thus sends balance again, and the process repeats itself until the transaction’s gas is exhausted.

contract Vulnerable {
  mapping(address => uint256) public balances;

  ...

  function withdraw(address recipient) public { 
    uint256 balance = balances[recipient]; 
    recipient.call.value(balance)(); 
    balances[recipient] = 0; // Fix: place this line before the call.
  } 
}

contract Attacker {
  uint256 public total; function () public payable {
    msg.sender.call(bytes4(keccak256("withdraw(address)")), this);
    total += msg.value;
  }
}

The vulnerability can be avoided by swapping the last two lines of the withdraw function. A type system could help ensure a contract can't send more Wei than it intended to.

Proposed solution

We aim for the Flint equivalent of the above contract to simply be:

contract Bank {
  var balances: [Address: Wei]
}

Bank :: account <- (balances.keys) {
  @payable
  mutating func deposit(implicit value: inout Wei) {
    // Omitting this line causes a compiler warning: the value received should be recorded.
    balances[address].transfer(&value)
  }
  
  mutating func withdraw() {
    // balances[account] is automatically set to 0 before transferring.
    send(account, &balances[account])
  }
}

We introduce the Asset trait and make Wei an instance of it. A Flint Asset represents an item of value (for example, currency). Asset types support a restricted set of operations and have their own semantics.

The asset trait is defined as follows:

trait Asset {
  associatedtype RawType

  // Create the asset by transferring a given amount of asset's contents.
  init(from other: inout Self, amount: RawType)
 
  // Unsafely create the Asset using the given raw value.
  init(unsafeValue: RawType)

  // Return the raw value held by the receiver.
  func getRawValue() -> RawType

  // Transfer a given amount from source into the receiver.
  mutating func transfer(from source: inout Self, amount: RawType)

  // Clears the receiver.
  mutating func destroy()

  // Unsafely set the raw value of the receiver.
  mutating func unsafelySetRawValue(_ value: RawType)
}

We implement convenience functions:

extension Asset {
  // Create the asset by transferring another asset's contents.
  init(from other: inout Self) {
    self.init(from: &other, amount: other.getRawValue())
  }

  // Transfer the value held by another Asset of the same concrete type.
  mutating func transfer(from source: inout Self) {
    transfer(from: &source, amount: source.getRawValue())
  }
}

And the global send function:

func send<T: Asset & EthereumCurrency>(destination: Address, asset: T)

Assets backed by numeric raw values

For types which are backed by a numeric value, such as Wei or Ether, we define the Numeric and Comparable traits and implement a trait extension.

trait Numeric {
  infix func +(_ other: Self)
  infix func -(_ other: Self)
}

trait Comparable {
  infix func <(_ other: Self)
  infix func <=(_ other: Self)
  infix func >(_ other: Self)
  infix func >=(_ other: Self)
}
extension Asset where RawType: Numeric & Comparable {
  // Self.RawType: Numeric

  mutating func transfer(from source: inout Self, amount: RawType) {
    if amount > source.getRawValue() { fatalError() }

    source.unsafelySetRawValue(source.getRawValue() - amount)
    unsafelySetRawValue(getRawValue() + amount)
  }

  mutating func destroy() {
    unsafelySetRawValue(0)
  }
}

Wei is then defined as:

struct Wei: Asset, Currency {
  var rawValue: Int

  init(unsafeValue: Int) {
    rawValue = unsafeValue
  }

  init(from other: Wei, amount: Int) {
    rawValue = 0
    transfer(from: &other, amount: amount)
  }

  func getRawValue() -> Int {
    return rawValue
  }

  mutating func unsafelySetRawValue(_ value: Int) {
    rawValue = value
  }
}

Semantics

Compiler warnings are triggered when asset local variables or parameters are not consumed exactly once in the scope of the function.


SUSAN: WHY IS THIS ONLY A WARNING RATHER THAN AN ERROR?


Transferring an asset

The contents of an asset can be transferred to another asset like so:

// Transfer the contents of b into a, clearing b.
a.transfer(from: &b)

Transferring a subset of an asset

// Transfer 50 from b into a.
a.transfer(from: &b, amount: 50)

Warnings

Assignment between assets trigger warnings

var a = Wei(from: &b)
a = b // Warning: The contents of a are implicitly destroyed. Use transfer(from:) instead.

Use of assets after transfer

let a = Wei(from: &b)
let c = Wei(from: &b) // Warning: The contents of b have already been transferred in this scope.

Local variables which haven't been transferred exactly once

{
  let a = Wei(from: &b)
  // Warning: The contents will be lost as a has not been transferred in this scope.
}

Unsafe operations

Creation from a raw value

let a = Wei(unsafeCreate: 50)

Setting an asset's raw value

a.unsafelySetRawValue(50)

Destroying an asset

{
  let a = Wei(from: &b)
  a.destroy()
  // No warning.
}

Unsupported operations

Assets as parameters

Functions cannot take asset parameters by value, as implicit copying of assets should be avoided.

func foo(a: Wei) {} // Error: Asset of type 'Wei' needs to be passed inout.

Functions returning assets

Functions cannot return assets.

func foo() -> Wei {} // Error: Cannot return type 'Wei' which conforms to 'Asset'.

Branching

When branching is involved:

mutating func foo(out: inout Wei) {
  var x = Wei(from: &self.a)
  var z = Wei(from: &self.c)

  if x.getRawValue() == 2 {
    var y = Wei(from: &self.b)
    x.destroy()
    z.destroy()
    // Warning: The contents of y will be lost as y has not been transferred in this scope.
  }

  out.transfer(from: &x)
  // Warning: The contents of z will be lost as y might not have been transferred in this scope.
}

Impact on mutating functions

As usual, functions taking state properties as inout arguments are considered to be mutating.

func foo() {
  let x = Wei(from: &self.a) // Error: use of mutating statement in non-mutating function.
}

Impact on external function call

It is not possible to declare a function taking inout parameters (required for Asset types) with public visibility.

Example: Withdrawing a specific amount

contract Wallet {
  var balances: [Address: Wei]
}

Wallet :: account <- (any) {
  mutating func withdraw(amount: inout Wei) {
    let retrieved = Wei(from: &balances[account], amount: amount) // Removes amount from balances[account].
    send(account, &retrieved) // Transfers retrieved.
  }
}

Receiving currency

Functions annotated with @payable have an implicit parameter of type Wei, which is an Asset. This makes recording a transfer's value type-safe.

contract Wallet {
  var balance: Wei
}

Wallet :: (any) {
  @payable
  mutating func receive(implicit value: inout Wei) {
    balance.transfer(&value) // Safe
  }
}

Example: Distributing money among peers

The following example distributes weighted amounts of Wei to a set of beneficiaries, attaching a split bonus as well.

contract Wallet {
  var beneficiaries: [Address: Wei]
  var weights: [Address: Int]
  var bonus: Wei

  var owner: Address
}


Wallet :: (any) {
  @payable
  mutating func receiveBonus(implicit newBonus: inout Wei) {
    bonus.transfer(&newBonus)
  }
}

Wallet :: (owner) {
  mutating func distribute(amount: Int) {
    let beneficiaryBonus = bonus.getRawValue() / beneficiaries.count
    for i in (0..<beneficiaries.count) {
      var allocation = Wei(from: &balance, amount: amount * weights[i])
      allocation.transfer(from: &bonus, amount: beneficiaryBonus)
      
      send(beneficiaries[i], &allocation)
    }
  }
}

Possible future extensions

Special syntax

In the future, we should consider using syntactic sugar for the Asset operations.


SUSAN: WHAT SHOULD THIS LOOK LIKE?


@autodestroying attribute

We should consider creating an @autodestroying function attribute, which would implicitly destroy local Asset variables at the end of every scope it defines.

@autodestroying
mutating func foo() {
  var x = Wei(from: &self.a)
  var z = Wei(from: &self.c)

  if x.getRawValue() == 2 {
    var y = Wei(from: &self.b)
    // y is implicitly destroyed
  }

  // z is implicitly destroyed
}

We need to find compelling use-cases for this feature.

Implicit type conversions between compatible Assets

We should consider implicitly converting compatible Asset types when applicable.

// a has type Ether 
let b = Wei(from: &a) // Convert a to its Wei correspondant and assign to b.

Alternatives considered

Many alternatives were considered.

Class-based approach

We could also implement assets using a class-based approach.

class Asset<T: Numeric> {
  var rawValue: T

  init(unsafeValue: T) {
    rawValue = unsafeValue
  }

  func getRawValue() -> T {
    return rawValue
  }

  mutating func transfer(from source: inout Asset<T>) {
    transfer(from: source, amount: source.getRawValue())
  }

  mutating func transfer(from source: inout Asset<T>, amount: RawType) {
    if amount > source.getRawValue() { fatalError() }

    source.unsafelySetRawValue(source.getRawValue() - amount)
    unsafelySetRawValue(getRawValue() + amount)
  }

  mutating func unsafelySetRawValue(_ value: T) {
    rawValue = value
  }
}

class Wei: Asset<Int> {
  init(from other: Wei, amount: Int) {
    rawValue = 0
    transfer(from: &other, amount: amount)
  }

  mutating func destroy() {
    unsafelySetRawValue(0)
  }
}

Linear types

We considered implement the Asset trait as linear type. Local variables would have needed to be consumed exactly once in the scope they are defined. State properties however would only be able to be consumed at most once, making them affine types. These rules are however not enforcable for assets contained in arrays or dictionaries, due to aliasing issues. Instead, the compiler produces warnings whenever it can detect such cases.