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
auth.sol
: multi-owner authmath.sol
: fixed point numeric routinesmove.sol
: erc20transferFrom
wrapperlock.sol
: reentrancy mutex
Standalone Contracts
token.sol
: an erc20 token with authed mint / burn andpermit
proxy.sol
: execute atomic transaction sequences from a persistent identitydelay.sol
: a governance timelock delayvalue.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.sol
: multi owner authmath.sol
: common numeric routinesmove.sol
: erc20 transferFrom wrapperlock.sol
: reentrancy mutex
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 wardDeny(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
anduint
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 awad
and aray
)
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 awad
. Precision is lost.rmul
: multiply a quantity by aray
. 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 awad
. Precision is lost.rdiv
: divide a quantity by aray
. 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 basen
: the exponentb
: the fixed point numeric base (e.g. 18 for awad
)
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 uintsmax(uint x, uint y)
: finds the maximum of two uintsimin(int x, int y)
: finds the minimum of two intsimax(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 booleanfalse
.
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 booleanfalse
.
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 to18
. - 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 tousr
. - Reverts if the caller is not a ward.
- Reverts if
totalSupply
orbalanceOf[usr]
would overflow auint256
. - 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 fromusr
. - Reverts if the caller is not a ward.
- Reverts if
totalSupply
orbalanceOf[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 spendamt
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 tousr
. - 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 fromsrc
todst
. - Reverts if the caller has not been approved for at least
amt
bysrc
. - Reverts if
src
does not have sufficient balance for the transfer. - If the caller has been approved for
type(uint).max
bysrc
, then the allowance fromsrc
to the caller will not be decreased. - If the caller is also
src
then the allowance fromsrc
to caller will not be decreased. This means that a call totransferFrom(address(this), usr, amt)
is semantically equivalent totransfer(usr, amt)
. - Aside from the above two cases, the allowance from
src
tocaller
will be decreased byamt
. - 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.