Dappsys

WARNING: This is still very much a work in progress. Do not use these contracts in production!

Dappsys is a set of minimal utility contracts targeting solidity versions above 0.8.

The contracts are written in a style that attempts wherever practical to avoid the following:

  • dependencies
  • inheritance
  • external calls
  • branching
  • looping
  • dynamic types

Alignment and aesthetics are considered to be beneficial to auditability and so are prioritised.

Wherever possible security properties have been verified formally.

Contracts

Mixins

Standalone Contracts

  • token.sol: an erc20 token with authed mint / burn and permit
  • proxy.sol: execute atomic transaction sequences from a persistent identity
  • delay.sol: a governance timelock delay
  • value.sol: an on chain beacon for off chain oracles

Interface Definitions

  • erc20.sol

Mixins

Although inheritance and imports are generally avoided within dappsys there are a few cases where the benefits of code reuse outweigh the costs to auditability imposed by splitting contract logic across many files.

These contracts can be inherited from to provide common functionality:

Auth

source code // tests

Auth implements a multi owner authorization pattern.

An owner is known as a ward of the contract. Wards can make other users wards by calling rely(address usr), or they can demote other wards by calling deny(address usr).

A contract that inherits from Auth will have the auth modifier available, which will revert if the caller is not a ward.

The constructor of Auth takes an address that will be made the initial ward.

Properties

  • authed methods can only be called by wards
  • wards can only be added and removed by other wards
  • all changes to ward status are logged

Reference

Data

  • mapping (address => bool): wards status for every ethereum address

Methods

rely(address usr):

  • makes usr a ward
  • logs a Rely event

deny(address usr):

  • removes usr as a ward
  • logs a Deny event

Events

  • Rely(address usr): usr is now a ward
  • Deny(address usr): usr is no longer a ward

Example

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.6;

import {Auth} from "dappsys-v2/auth.sol";

contract Set is Auth {
    uint public value;

    // note that we must call the constructor of Auth to specify the initial ward
    constructor(uint v) Auth(msg.sender) {
        value = v;
    }

    function set(uint v) external auth {
        value = v;
    }
}

contract UseSet {
    function go() external {
        Set set = new Set(10);

        // we are a ward here since we deployed the contract
        require(set.wards(address(this)));

        // since we are a ward we can call `set`
        set.set(11);
        require(set.value() == 11);

        // make vitalik a ward
        set.rely(0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B);
        // revoke our authority
        set.deny(address(this));

        // this fails now since we are no longer a ward
        set.set(12);
    }
}

Math

source code // tests

Math implements a few common numeric helpers:

  • fixed point division, multiplication and exponentiation
  • a babylonian integer square root routine
  • min and max methods for both int and uint types

Fixed Point Arithmetic

The fixed point multiplication and division routines are implemented for three different units:

  • wad: fixed point decimal with 18 decimals (for basic quantities, e.g. balances)
  • ray: fixed point decimal with 27 decimals (for precise quantites, e.g. ratios)
  • rad: fixed point decimal with 45 decimals (result of integer multiplication with a wad and a ray)

Generally, wad should be used additively and ray should be used multiplicatively. It usually doesn't make sense to multiply a wad by a wad (or a rad by a rad).

For convenience three useful constants are provided, each representing one in each of the above units:

  • WAD: 1 ** 18
  • RAD: 1 ** 27
  • RAY: 1 ** 45

Multiplication

Two multiplication operators are provided in Math:

  • wmul: multiply a quantity by a wad. Precision is lost.
  • rmul: multiply a quantity by a ray. Precision is lost.

Both wmul and rmul will always round down.

They can be used sensibly with the following combination of units:

  • wmul(wad, wad) -> wad
  • wmul(ray, wad) -> ray
  • wmul(rad, wad) -> rad
  • rmul(wad, ray) -> wad
  • rmul(ray, ray) -> ray
  • rmul(rad, ray) -> rad

Division

Two division operators are provided in Math:

  • wdiv: divide a quantity by a wad. Precision is lost.
  • rdiv: divide a quantity by a ray. Precision is lost.

Both wdiv and rdiv will always round down.

They can be used sensibly with the following combination of units:

  • wdiv(wad, wad) -> wad
  • wdiv(ray, wad) -> ray
  • wdiv(rad, wad) -> rad
  • rdiv(wad, ray) -> wad
  • rdiv(ray, ray) -> ray
  • rdiv(rad, ray) -> rad

Exponentiation

The fixed point exponentiation routine (rpow) is implemented using exponentiation by squaring, giving a complexity of O(log n) (instead of O(n) for naive repeated multiplication).

rpow accepts three parameters:

  • x: the base
  • n: the exponent
  • b: the fixed point numeric base (e.g. 18 for a wad)

calling rpow(x, n, b) will interpret x as a fixed point integer with b digits of precision, and raise it to the power of n.

Square Root

sqrt is an algorithm for approximating the square root of any given integer using the babylonian method. The implementation is taken from uniswap-v2-core. It can be shown that it terminates in at most 255 loop iterations.

Calling sqrt(x) will find the number y such that y * y <= x and (y + 1) * (y + 1) > x

Min / Max helpers

Four trvial min / max helpers are provided:

  • min(uint x, uint y): finds the minimum of two uints
  • max(uint x, uint y): finds the maximum of two uints
  • imin(int x, int y): finds the minimum of two ints
  • imax(int x, int y): finds the maximum of two ints

Move

source code // tests

Move proves a wrapper around the erc20 transfer operations that handles erc20 tokens with a missing return value.

It can handle erc20 tokens that are missing a return value (e.g. BNB, USDT), tokens that return false instead of reverting on failure (e.g. ZRX), as well as tokens that correctly return true if the transfer was performed successfully. It cannot handle tokens that return false for a successful transfer (e.g. Tether Gold).

Be aware that every token interaction is dangerous and should be made with great care, and that the usage of a wrapper such as this one protects you from only a very small class of potential problems. If at all possible use a contract level allow list and carefully audit every token that your system will be interacting with.

See the weird-erc20 repo for a non exhaustive list of surprising mainnet tokens.

Interface

move(address token, address dst, uint amt)

  • Calls token.transferFrom(address(this), dst, amt)
  • Reverts if the call to transferFrom reverts
  • Reverts if token returns a boolean false.

move(address token, address src, address dst, uint amt)

  • Calls token.transferFrom(src, dst, amt)
  • Reverts if the call to transferFrom reverts
  • Reverts if token returns a boolean false.

Lock

source code // tests

Lock provides a reentrancy mutex. Contracts that inherit from Lock will have the lock modifier available, any subcall resulting from a call to a locked method will revert if it attempts to call any other locked method on the same contract.

Note that reentrant calls to methods that are not protected by the lock modifier will still succeed.

Interface

Data

  • bool unlocked: lock status

Modifiers

lock():

reverts if unlocked is true, and sets it to false for the duration of the call to the wrapped method.

Example

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.6;

import {Lock} from "dappsys-v2/lock.sol";

contract Locked is Lock {
    uint public value = 0;

    function badSet(uint v) external lock {
        doSet(v);
    }

    function doBadSet(uint v) internal lock {
        value = v;
    }

    function goodSet(uint v) external lock {
        doGoodSet(v);
    }

    function doGoodSet(uint v) internal {
        value = v;
    }
}

contract Test {
    function go() external {
        Locked locked = new Locked();

        // this call will work since `doGoodSet` is not protected by the lock
        locked.goodSet(10);

        // this will always revert since `unlocked` is `false` during the inner call to `doBadSet`
        locked.badSet(10);
    }
}

Standalone

The contracts in this section are designed to be deployed as standalone modules:

Token

source code // tests

Token implements an ERC20 compatible token with the addition of auth protected mint and burn methods, as well as EIP-2612 (permit) signature based delegation.

The token is fully compliant with the ERC20 interface specification.

Semantics

  • The token is designed to be minimally surprising and avoids any additional checks or reverts that are not strictly essential for implementing the core transfer semantics.
  • The token decimals is hardcoded to 18.
  • The token is immutable and cannot be upgraded once deployed.

mint(address usr, uint amt) auth

  • Allows a ward of the token to mint amt tokens to usr.
  • Reverts if the caller is not a ward.
  • Reverts if totalSupply or balanceOf[usr] would overflow a uint256.
  • If execution succeeds, emits an event Transfer(address(0), usr, amt).

burn(address usr, uint amt) auth

  • Allows a ward of the token to burn amt tokens from usr.
  • Reverts if the caller is not a ward.
  • Reverts if totalSupply or balanceOf[usr] would be less than 0.
  • If execution succeeds, emits an event Transfer(usr, address(0), amt).

approve(address usr, uint amt) returns (bool)

  • Allows usr to spend amt of the callers tokens.
  • An existing approval from the caller to usr will be overridden.
  • Emits an event Approval(msg.sender, usr, amt).
  • Returns true

Note that compliance with the ERC20 standard has been prioritised here and this token implementation is vulnerable to the ERC20 approval race.

transfer(address usr, uint amt) returns (bool)

  • Sends amt tokens from the caller to usr.
  • Reverts if the caller does not have sufficient balance for the transfer.
  • If execution succeeds, emits and event Transfer(msg.sender, dst, amt).
  • If execution succeeds, returns true (or reverts otherwise).

transferFrom(address src, address dst, uint amt) returns (bool)

  • Sends amt tokens from src to dst.
  • Reverts if the caller has not been approved for at least amt by src.
  • Reverts if src does not have sufficient balance for the transfer.
  • If the caller has been approved for type(uint).max by src, then the allowance from src to the caller will not be decreased.
  • If the caller is also src then the allowance from src to caller will not be decreased. This means that a call to transferFrom(address(this), usr, amt) is semantically equivalent to transfer(usr, amt).
  • Aside from the above two cases, the allowance from src to caller will be decreased by amt.
  • If execution succeeds, emits and event Transfer(src, dst, amt).
  • If execution succeeds, returns true (or reverts otherwise).

Permit

The token implements EIP-2612, and as such allows delegation of control over token balances via a signed message. This has two main benefits:

  • It allows the construction of "gassless" relayer based workflows where users can pay for interactions involving the token without having to hold ether.
  • It allows users to both approve a contract on their tokens and then have the contract pull those tokens in a single transaction.

EIP-2612 adds 3 methods to the standard ERC20 ABI:

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
function nonces(address owner) external view returns (uint)
function DOMAIN_SEPARATOR() external view returns (bytes32)

For a full description of the semantics, refer to the EIP.