- Proposal: FIP-0001
- Author: Franklin Schrans
- Review Manager: TBD
- Status: Split FIP-0004 & FIP-0005
- Issue label: 0001-asset-trait
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])
}
}
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.
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)
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
}
}
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?
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)
// Transfer 50 from b into a.
a.transfer(from: &b, amount: 50)
var a = Wei(from: &b)
a = b // Warning: The contents of a are implicitly destroyed. Use transfer(from:) instead.
let a = Wei(from: &b)
let c = Wei(from: &b) // Warning: The contents of b have already been transferred in this scope.
{
let a = Wei(from: &b)
// Warning: The contents will be lost as a has not been transferred in this scope.
}
let a = Wei(unsafeCreate: 50)
a.unsafelySetRawValue(50)
{
let a = Wei(from: &b)
a.destroy()
// No warning.
}
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 cannot return assets.
func foo() -> Wei {} // Error: Cannot return type 'Wei' which conforms to 'Asset'.
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.
}
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.
}
It is not possible to declare a function taking inout
parameters (required for Asset types)
with public
visibility.
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.
}
}
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
}
}
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)
}
}
}
In the future, we should consider using syntactic sugar for the Asset
operations.
SUSAN: WHAT SHOULD THIS LOOK LIKE?
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.
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.
Many alternatives were considered.
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)
}
}
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.