Solidity contract for storing and interacting with key/value address pairs
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
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 ofyour_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 thenode_modules
subs-directories
Tip, whenever possible it is recommended to utilize
InterfaceAddressStorage
instead, because calling methods directly fromAddressStorage
will cause compiler to copy method byte code.
Review the Truffle -- Package Management via NPM documentation for more details.
How to utilize this repository
Write a set of contracts that make use of, and extend, AddressStorage
features.
// 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
// 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
toaddress(Account)
foractive_accounts
andbanned_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.
Application Programming Interfaces for Solidity smart contracts
Solidity contract for storing and interacting with key/value address pairs
Source contracts/AddressStorage.sol
Properties
-
data
{mapping(address => address)} Store key/valueaddress
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 specifiedaddress
-
authorized
{mapping(address => bool)} Allow mutation from mappedaddress
s
Developer note -> Depends on
@solidity-utilities/library-mapping-address
Insert
address
intomapping
ofauthorized
data structure
Source addAuthorized(address _key)
Parameters
_key
{address} Key to set value oftrue
Throws -> {Error} "AddressStorage.addAuthorized: message sender not an owner"
Developer note -> Does not check if address
is already authorized
Overwrite old
owner
with new owneraddress
Source changeOwner(address _new_owner)
Parameters
_new_owner
{address} New owner address
Throws -> {Error} "AddressStorage.changeOwner: message sender not an owner"
Delete
mapping
address key/value pairs and remove alladdress
fromkeys
Source clear()
Throws -> {Error} "AddressStorage.clear: message sender not authorized"
Developer note -> Warning may fail if storing many address
pairs
Remove
address
frommapping
ofauthorized
data structure
Source deleteAuthorized(address _key)
Parameters
_key
{address} Key to set value offalse
Throws
-
{Error}
"AddressStorage.deleteAuthorized: message sender not authorized"
-
{Error}
"AddressStorage.deleteAuthorized: cannot remove owner"
Retrieve stored value
address
or throws an error if undefined
Source get(address _key)
Parameters
_key
{address} Mapping keyaddress
to lookup corresponding valueaddress
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
Retrieve stored value
address
or provided defaultaddress
if undefined
Source getOrElse(address _key, address _default)
Parameters
-
_key
{address} Mapping keyaddress
to lookup corresponding valueaddress
for -
_default
{address} Value to return if keyaddress
lookup is undefined
Returns -> {address} Value address
for given key address
or _default
if undefined
Developer note -> Forwards parameters to
data.getOrElse
Allow for defining custom error reason if value
address
is undefined
Source getOrError(address _key, string _reason)
Parameters
-
_key
{address} Mapping keyaddress
to lookup corresponding valueaddress
for -
_reason
{string} Custom error message to throw if valueaddress
is undefined
Returns -> {address} Value for given key address
Throws -> {Error} _reason
if value is undefined
Developer note -> Forwards parameters to
data.getOrError
Check if
address
key has a corresponding valueaddress
defined
Source has(address _key)
Parameters
_key
{address} Mapping key to check if valueaddress
is defined
Returns -> {bool} true
if value address
is defined, or false
if
undefined
Developer note -> Forwards parameter to
data.has
Index for
address
key withinkeys
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
Index for
address
key withinkeys
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
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
Delete value
address
for given_key
Source remove(address _key)
Parameters
_key
{address} Mapping key to delete corresponding valueaddress
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
Delete value
address
for given_key
Source removeOrError(address _key, string _reason)
Parameters
-
_key
{address} Mapping key to delete corresponding valueaddress
for -
_reason
{string} Custom error message to throw if valueaddress
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
Call
selfdestruct
with providedaddress
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"
Store
_value
under given_key
while preventing unintentional overwrites
Source set(address _key, address _value)
Parameters
-
_key
{address} Mapping key to set corresponding valueaddress
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
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 valueaddress
for -
_value
{address} Mapping value to set -
_reason
{string} Custom error message to present if valueaddress
is defined
Throws
-
{Error}
"AddressStorage.setOrError: message sender not authorized"
-
{Error}
_reason
if value is defined
Developer note -> Forwards parameters to
data.setOrError
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
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.
Options for contributing to address-storage and solidity-utilities
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 ofinstall
to avoid mutating thepackage.json
, and/orpackage-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.
Methods for financially supporting
solidity-utilities
that maintainsaddress-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.
Note, this section only documents breaking changes or major feature releases
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.
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.
module.exports = {
/* ... */
compilers: {
solc: {
version: "0.8.7",
settings: {
optimizer: {
enabled: true,
runs: 200
},
/* ... */
},
},
},
/* ... */
};
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.