Skip to content

Solidity contract for storing and interacting with key/value address pairs

License

Notifications You must be signed in to change notification settings

solidity-utilities/address-storage

Repository files navigation

Address Storage

Solidity contract for storing and interacting with key/value address pairs

Byte size of Address Storage Open Issues Open Pull Requests Latest commits Build Status



Requirements

Prerequisites and/or dependencies that this project needs to function properly

This project utilizes Truffle for organization of source code and tests, thus it is recommended to install Truffle globally to your current user account

npm install -g truffle

Quick Start

Perhaps as easy as one, 2.0,...

NPM and Truffle are recommended for importing and managing dependencies

cd your_project

npm install @solidity-utilities/address-storage

Note, source code will be located within the node_modules/@solidity-utilities/address-storage directory of your_project root

Solidity contracts may then import code via similar syntax as shown

import {
    AddressStorage
} from "@solidity-utilities/address-storage/contracts/AddressStorage.sol";

import {
    InterfaceAddressStorage
} from "@solidity-utilities/address-storage/contracts/InterfaceAddressStorage.sol";

Note, above path is not relative (ie. there's no ./ preceding the file path) which causes Truffle to search the node_modules subs-directories

Tip, whenever possible it is recommended to utilize InterfaceAddressStorage instead, because calling methods directly from AddressStorage will cause compiler to copy method byte code.

Review the Truffle -- Package Management via NPM documentation for more details.


Usage

How to utilize this repository

Write a set of contracts that make use of, and extend, AddressStorage features.

contracts/Account.sol

// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.7;

/// @title Example contract instance to be reconstituted by `Host`
/// @author S0AndS0
contract Account {
    address public owner;
    string public name;

    /* -------------------------------------------------------------------- */

    ///
    constructor(address _owner, string memory _name) {
        owner = _owner;
        name = _name;
    }

    /* -------------------------------------------------------------------- */

    /// @notice Require message sender to be an instance owner
    /// @param _caller {string} Function name that implements this modifier
    modifier onlyOwner(string memory _caller) {
        string memory _message = string(
            abi.encodePacked(
                "Account.",
                _caller,
                ": message sender not an owner"
            )
        );
        require(msg.sender == owner, _message);
        _;
    }

    /* -------------------------------------------------------------------- */

    /// @notice Update `Account.name`
    /// @param _new_name **{string}** Name to assign to `Account.name`
    /// @custom:throws **{Error}** `"Account.changeName: message sender not an owner"`
    function changeName(string memory _new_name)
        public
        onlyOwner("changeName")
    {
        name = _new_name;
    }

    /// @notice Update `Account.owner`
    /// @param _new_owner **{address}** Address to assign to `Account.owner`
    /// @custom:throws **{Error}** `"Account.changeOwner: message sender not an owner"`
    function changeOwner(address _new_owner) public onlyOwner("changeOwner") {
        owner = _new_owner;
    }
}

Above the Account.sol contract;

  • stores owner information, such as name

  • restricts certain mutation actions to owner only

  • allows updating stored information by owner

contracts/Host.sol

// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.7;

import {
    AddressStorage
} from "@solidity-utilities/address-storage/contracts/AddressStorage.sol";

import {
    InterfaceAddressStorage
} from "@solidity-utilities/address-storage/contracts/InterfaceAddressStorage.sol";

import { Account } from "./Account.sol";

/// @title Example contract to demonstrate further abstraction of `AddressStorage` features
/// @author S0AndS0
contract Host {
    address public active_accounts = address(new AddressStorage(address(this)));
    address public banned_accounts = address(new AddressStorage(address(this)));
    address public owner;

    /* -------------------------------------------------------------------- */

    ///
    constructor(address _owner) {
        owner = _owner;
    }

    /* -------------------------------------------------------------------- */

    /// @notice Require message sender to be an instance owner
    /// @param _caller {string} Function name that implements this modifier
    modifier onlyOwner(string memory _caller) {
        string memory _message = string(
            abi.encodePacked("Host.", _caller, ": message sender not an owner")
        );
        require(msg.sender == owner, _message);
        _;
    }

    /// @notice Require `_key` to not be stored by `active_accounts`
    /// @param _key **{address}** Reference to `Account.owner`
    modifier onlyNotActive(address _key, string memory _caller) {
        string memory _message = string(
            abi.encodePacked("Host.", _caller, ": account already active")
        );
        require(!InterfaceAddressStorage(active_accounts).has(_key), _message);
        _;
    }

    /// @notice Require `_key` to not be stored by `banned_accounts`
    /// @param _key **{address}** Reference to `Account.owner`
    modifier onlyNotBanned(address _key, string memory _caller) {
        string memory _message = string(
            abi.encodePacked("Host.", _caller, ": account was banned")
        );
        require(!InterfaceAddressStorage(banned_accounts).has(_key), _message);
        _;
    }

    /* -------------------------------------------------------------------- */

    ///
    event ActivatedAccount(address owner, address account_reference);

    ///
    event BannedAccount(address owner, address account_reference);

    /* -------------------------------------------------------------------- */

    /// @notice Move `Account` reference from `active_accounts` to `banned_accounts`
    /// @param _key **{address}** Key within `active_accounts` to ban
    /// @custom:throws `"Host.banAccount: not active"`
    /// @custom:throws `"Host.banAccount: already banned"`
    function banAccount(address _key) external onlyOwner("banAccount") {
        address _account_reference = InterfaceAddressStorage(active_accounts)
            .removeOrError(_key, "Host.banAccount: not active");
        InterfaceAddressStorage(banned_accounts).setOrError(
            _key,
            _account_reference,
            "Host.banAccount: already banned"
        );
        emit BannedAccount(_key, _account_reference);
    }

    /// @notice Add existing `Account` instance to `active_accounts`
    /// @param **{Account}** Previously deployed `Account` contract instance
    /// @return **{Account}** Instance of `Account`
    /// @custom:throws `"Host.importAccount: account already active"`
    /// @custom:throws `"Host.importAccount: account was banned"`
    function importAccount(Account _account)
        public
        onlyNotActive(_account.owner(), "importAccount")
        onlyNotBanned(_account.owner(), "importAccount")
        returns (Account)
    {
        address _owner = _account.owner();
        address _account_reference = address(_account);
        InterfaceAddressStorage(active_accounts).set(
            _owner,
            _account_reference
        );
        emit ActivatedAccount(_owner, _account_reference);
        return _account;
    }

    /// @notice Initialize new instance of `Account` and add to `active_accounts`
    /// @param _owner **{address}** Account owner to assign
    /// @param _name **{string}** Account name to assign
    /// @return **{Account}** Instance of `Account` with given `owner` and `name`
    /// @custom:throws `"Host.registerAccount: account already active"`
    /// @custom:throws `"Host.registerAccount: account was banned"`
    function registerAccount(address _owner, string memory _name)
        external
        onlyNotActive(_owner, "registerAccount")
        onlyNotBanned(_owner, "registerAccount")
        returns (Account)
    {
        Account _account = new Account(_owner, _name);
        address _account_reference = address(_account);

        InterfaceAddressStorage(active_accounts).set(
            _owner,
            _account_reference
        );
        emit ActivatedAccount(_owner, _account_reference);

        return _account;
    }

    /// @notice Delete reference from either `active_accounts` or `banned_accounts`
    /// @param _key **{address}** Owner of `Account` instance to remove
    /// @return **{Account}** Instance from removed value `address`
    /// @custom:throws `"Host.removeAccount: message sender not an owner"`
    /// @custom:throws `"Host.removeAccount: account not available"`
    function removeAccount(address _key)
        external
        onlyOwner("removeAccount")
        returns (Account)
    {
        address _account_reference;
        if (InterfaceAddressStorage(active_accounts).has(_key)) {
            _account_reference = InterfaceAddressStorage(active_accounts)
                .remove(_key);
        } else if (InterfaceAddressStorage(banned_accounts).has(_key)) {
            _account_reference = InterfaceAddressStorage(banned_accounts)
                .remove(_key);
        }

        require(
            _account_reference != address(0x0),
            "Host.removeAccount: account not available"
        );

        return Account(_account_reference);
    }

    /// @notice Sync `active_accounts` key with `Account.owner`
    /// @dev Account instance should update `owner` before calling this method
    /// @param _key **{address}** Old owner `address` to sync with `Account.owner`
    /// @custom:throws **{Error}** `"Host.updateKey: message sender not Account owner"`
    function updateKey(address _key) external {
        Account _account = Account(
            InterfaceAddressStorage(active_accounts).get(_key)
        );
        require(
            msg.sender == _account.owner(),
            "Host.updateKey: message sender not Account owner"
        );
        InterfaceAddressStorage(active_accounts).remove(_key);
        importAccount(_account);
    }

    /// @notice Retrieve `Account.name` for given `_key`
    /// @param _key **{address}** Owner of `active_accounts` instance
    /// @return **{string}** Name saved within `Account` instance
    /// @custom:throws **{Error}** `"Host.whoIs: account not active"`
    function whoIs(address _key) external view returns (string memory) {
        address _account_reference = InterfaceAddressStorage(active_accounts)
            .getOrError(_key, "Host.whoIs: account not active");
        Account _account_instance = Account(_account_reference);
        return _account_instance.name();
    }
}

Above the Host contract;

  • demonstrates how to utilize InterfaceAddressStorage within another contract

  • maintains mapping of Account.owner to address(Account) for active_accounts and banned_accounts

  • restricts certain mutation actions to owner only

  • provides convenience functions for retrieving information about Account instances


There is much more that can be accomplished by leveraging abstractions provided by AddressStorage, check the API section for full set of features available. And review the test/test__examples__Account.js and test/test__examples__Host.js files for inspiration on how to use these examples within projects.


API

Application Programming Interfaces for Solidity smart contracts


Contract AddressStorage

Solidity contract for storing and interacting with key/value address pairs

Source contracts/AddressStorage.sol

Properties

  • data {mapping(address => address)} Store key/value address pairs

  • indexes {mapping(address => uint256)} Warning order of indexes NOT guaranteed!

  • keys {address[]} Warning order of keys NOT guaranteed!

  • owner {address} Allow mutation or selfdestruct from specified address

  • authorized {mapping(address => bool)} Allow mutation from mapped addresss

Developer note -> Depends on @solidity-utilities/library-mapping-address


Method addAuthorized

Insert address into mapping of authorized data structure

Source addAuthorized(address _key)

Parameters

  • _key {address} Key to set value of true

Throws -> {Error} "AddressStorage.addAuthorized: message sender not an owner"

Developer note -> Does not check if address is already authorized


Method changeOwner

Overwrite old owner with new owner address

Source changeOwner(address _new_owner)

Parameters

  • _new_owner {address} New owner address

Throws -> {Error} "AddressStorage.changeOwner: message sender not an owner"


Method clear

Delete mapping address key/value pairs and remove all address from keys

Source clear()

Throws -> {Error} "AddressStorage.clear: message sender not authorized"

Developer note -> Warning may fail if storing many address pairs


Method deleteAuthorized

Remove address from mapping of authorized data structure

Source deleteAuthorized(address _key)

Parameters

  • _key {address} Key to set value of false

Throws

  • {Error} "AddressStorage.deleteAuthorized: message sender not authorized"

  • {Error} "AddressStorage.deleteAuthorized: cannot remove owner"


Method get

Retrieve stored value address or throws an error if undefined

Source get(address _key)

Parameters

  • _key {address} Mapping key address to lookup corresponding value address for

Returns -> {address} Value for given key address

Throws -> {Error} "AddressStorage.get: value not defined"

Developer note -> Passes parameter to data.getOrError with default Error _reason to throw


Method getOrElse

Retrieve stored value address or provided default address if undefined

Source getOrElse(address _key, address _default)

Parameters

  • _key {address} Mapping key address to lookup corresponding value address for

  • _default {address} Value to return if key address lookup is undefined

Returns -> {address} Value address for given key address or _default if undefined

Developer note -> Forwards parameters to data.getOrElse


Method getOrError

Allow for defining custom error reason if value address is undefined

Source getOrError(address _key, string _reason)

Parameters

  • _key {address} Mapping key address to lookup corresponding value address for

  • _reason {string} Custom error message to throw if value address is undefined

Returns -> {address} Value for given key address

Throws -> {Error} _reason if value is undefined

Developer note -> Forwards parameters to data.getOrError


Method has

Check if address key has a corresponding value address defined

Source has(address _key)

Parameters

  • _key {address} Mapping key to check if value address is defined

Returns -> {bool} true if value address is defined, or false if undefined

Developer note -> Forwards parameter to data.has


Method indexOf

Index for address key within keys array

Source indexOf(address _key)

Parameters

  • _key {address} Key to lookup index for

Returns -> {uint256} Current index for given _key within keys array

Throws -> {Error} "AddressStorage.indexOf: key not defined"

Developer note -> Passes parameter to indexOfOrError with default _reason


Method indexOfOrError

Index for address key within keys array

Source indexOfOrError(address _key, string _reason)

Parameters

  • _key {address} Key to lookup index for

Returns -> {uint256} Current index for given _key within keys array

Throws -> {Error} _reason if value for _key is undefined

Developer note -> Cannot depend on results being valid if mutation is allowed between calls


Method listKeys

Convenience function to read all mapping key addresses

Source listKeys()

Returns -> {address[]} Keys address array

Developer note -> Cannot depend on results being valid if mutation is allowed between calls


Method remove

Delete value address for given _key

Source remove(address _key)

Parameters

  • _key {address} Mapping key to delete corresponding value address for

Returns -> {address} Value address that was removed from data storage

Throws

  • {Error} "AddressStorage.remove: message sender not authorized"

  • {Error} "AddressStorage.remove: value not defined"

Developer note -> Passes parameter to removeOrError with default _reason


Method removeOrError

Delete value address for given _key

Source removeOrError(address _key, string _reason)

Parameters

  • _key {address} Mapping key to delete corresponding value address for

  • _reason {string} Custom error message to throw if value address is undefined

Returns -> {address} Value address that was removed from data storage

Throws

  • {Error} "AddressStorage.removeOrError: message sender not authorized"

  • {Error} _reason if value is undefined

Developer note -> Warning reorders keys, and mutates indexes, for efficiency reasons


Method selfDestruct

Call selfdestruct with provided address

Source selfDestruct(address payable _to)

Parameters

  • _to {address} Where to transfer any funds this contract has

Throws -> {Error} "AddressStorage.selfDestruct: message sender not an owner"


Method set

Store _value under given _key while preventing unintentional overwrites

Source set(address _key, address _value)

Parameters

  • _key {address} Mapping key to set corresponding value address for

  • _value {address} Mapping value to set

Throws

  • {Error} "AddressStorage.set: message sender not authorized"

  • {Error} "AddressStorage.set: value already defined"

Developer note -> Forwards parameters to setOrError with default _reason


Method setOrError

Store _value under given _key while preventing unintentional overwrites

Source setOrError(address _key, address _value, string _reason)

Parameters

  • _key {address} Mapping key to set corresponding value address for

  • _value {address} Mapping value to set

  • _reason {string} Custom error message to present if value address is defined

Throws

  • {Error} "AddressStorage.setOrError: message sender not authorized"

  • {Error} _reason if value is defined

Developer note -> Forwards parameters to data.setOrError


Method size

Number of key/value address pairs currently stored

Source size()

Returns -> {uint256} Length of keys array

Developer note -> Cannot depend on results being valid if mutation is allowed between calls


Notes

Additional things to keep in mind when developing

In some cases it may be cheaper for deployment costs to use the library-mapping-address project directly instead, especially if tracking defined keys is not needed.


This repository may not be feature complete and/or fully functional, Pull Requests that add features or fix bugs are certainly welcomed.


Contributing

Options for contributing to address-storage and solidity-utilities


Forking

Tips for forking address-storage

Make a Fork of this repository to an account that you have write permissions for.

  • Clone fork URL. The URL syntax is git@github.com:<NAME>/<REPO>.git, then add this repository as a remote...
mkdir -p ~/git/hub/solidity-utilities

cd ~/git/hub/solidity-utilities

git clone --origin fork git@github.com:<NAME>/address-storage.git

git remote add origin git@github.com:solidity-utilities/address-storage.git
  • Install development dependencies
cd ~/git/hub/solidity-utilities/address-storage

npm ci

Note, the ci option above is recommended instead of install to avoid mutating the package.json, and/or package-lock.json, file(s) implicitly

  • Commit your changes and push to your fork, eg. to fix an issue...
cd ~/git/hub/solidity-utilities/address-storage


git commit -F- <<'EOF'
:bug: Fixes #42 Issue


**Edits**


- `<SCRIPT-NAME>` script, fixes some bug reported in issue
EOF


git push fork main
  • Then on GitHub submit a Pull Request through the Web-UI, the URL syntax is https://github.com/<NAME>/<REPO>/pull/new/<BRANCH>

Note; to decrease the chances of your Pull Request needing modifications before being accepted, please check the dot-github repository for detailed contributing guidelines.


Sponsor

Methods for financially supporting solidity-utilities that maintains address-storage

Thanks for even considering it!

Via Liberapay you may [![sponsor__shields_io__liberapay]][sponsor__link__liberapay] on a repeating basis.

For non-repeating contributions Ethereum is accepted via the following public address;

0x5F3567160FF38edD5F32235812503CA179eaCbca

Regardless of if you're able to financially support projects such as address-storage that solidity-utilities maintains, please consider sharing projects that are useful with others, because one of the goals of maintaining Open Source repositories is to provide value to the community.


Change Log

Note, this section only documents breaking changes or major feature releases


Version 0.1.0

Make eligible functions external

git diff 'v0.1.0' 'v0.0.2'

Developer notes

Recent update to version 0.1.0 of library-mapping-address dependency now attempts to prevent assigning values of 0x0

Functions get, has, and remove are now external typed. However, getOrError and removeOrError will remain public due to code duplication causing "out of gas" errors for some use cases.


Version 0.2.0

Allow multiple addresses to mutate data

git diff 'v0.2.0' 'v0.1.0'

Developer notes

Functions clear, remove, removeOrError, set, and setOrError now utilize the onlyAuthorized modifier. If using try/catch and filtering on reason, then please update from "AddressStorage.__name__: message sender not an owner" to "AddressStorage.__name__: message sender not authorized"

Warning the authorized mapping does not track defined keys, and is intended for allowing other smart contract(s) mutation permissions.

Warning due to code additions of addAuthorized and deleteAuthorized compiler optimization is currently necessary, eg.

truffle-config.js (snip)

module.exports = {
  /* ... */
  compilers: {
    solc: {
      version: "0.8.7",
      settings: {
        optimizer: {
          enabled: true,
          runs: 200
        },
        /* ... */
      },
    },
  },
  /* ... */
};

Attribution


License

Legal side of Open Source

Solidity contract for storing and interacting with key/value address pairs
Copyright (C) 2021 S0AndS0

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

For further details review full length version of AGPL-3.0 License.