Hey all!
After finishing the course, encouraged by Filip, I also wanted to write something on my own to get some experience and I came up with this:
A contract for creating shared accounts
The idea was for the contract to:
- Provide tools to create and use an indefinate number of shared accounts (make transfers, top-up, withdraw funds).
- Support adding and deleting users from account
- Keep track of all users of each account
- Allow for 3 different levels of user rights within account.
- Block non-users from using accounts.
- Keep track of all accounts of each individual user
Note: Contract uses the functionality from Ownable.sol and Destroyable.sol as they were provided in the course materials (link to github repository).
For clarity of reading I divided the functionality in 4 parts:
1. Creatable
Here I defined:
- structs for Account and user's Rights
- function to create an account and give it a unique ID
- mappings of each ID to its corresponding account
- mapping of each user's address to all of his/her accounts
import "./Destroyable.sol";
pragma solidity 0.5.12;
contract Creatable is Destroyable {
struct Account{
bool created;
string name;
address[] users;
mapping(address => Rights) userRights; // mapping of each user's rights
uint saldo;
}
struct Rights{ // struct containing user rights to:
bool user; // make transactions and withdraw funds
bool manager; // add and delete users
bool creator; // change user rights
}
mapping(address => bytes32[]) internal accountIds; // mapping of all account of each individual user
mapping(bytes32 => Account) internal accounts; // mapping of each account to its id
event accountCreated(bytes32 accId, address creator);
// create an account with one user (creator) and balance equal to value sent with function
function createAccount( string memory name) public payable {
address creator = msg.sender;
bytes32 id = keccak256( abi.encodePacked( name, creator ) ); // create account ID
// verify if this is account already created
require(!accounts[id].created, "You already have an account with that name");
// create temp Account
Account memory newAccount;
newAccount.created = true;
newAccount.name = name; // set account name
address[] memory allUsers = new address[](1); // insert creator's address into users array
allUsers[0] = creator;
newAccount.users = allUsers;
newAccount.saldo = msg.value; // set balance equal to value sent
accounts[id] = newAccount; // insert account to "accounts" mapping by ID
accounts[id].userRights[creator] = Rights(true,true,true); // give all rights to creator
accountIds[creator].push(id); // insert account ID into user's array of IDs
// assert if account was inserted correctly
assert(
keccak256(
abi.encodePacked(
newAccount.created,
newAccount.name,
newAccount.saldo
)
)
==
keccak256(
abi.encodePacked(
accounts[id].created,
accounts[id].name,
accounts[id].saldo
)
)
);
emit accountCreated(id,creator);
}
}
2. Functional
This contract defines inspection functionality by providing:
- modifiers to verify if input indexes are correct
- modifiers to check user's rights
- functions to inspect account IDs, account users, their addresses and rights
import "./Creatable.sol";
pragma solidity 0.5.12;
contract Functional is Creatable{
// check if the given accIndex is within the "accountIDs" array
modifier checkAccIndex(uint accIndex){
require(accIndex < accountIds[ msg.sender ].length, "You do not have that many accounts");
_;
}
// check if the given userIndex is within the account's "users" array
modifier checkUserIndex(bytes32 accId, uint userIndex){
require(userIndex < accounts[ accId ].users.length, "There isn't that many users in account");
_;
}
modifier onlyUser(bytes32 accId){
require(accounts[ accId ].userRights[msg.sender].user, "You don't have rights to do this");
_;
}
modifier onlyManager(bytes32 accId){
require(accounts[ accId ].userRights[msg.sender].manager, "You don't have rights to do this");
_;
}
modifier onlyCreator(bytes32 accId){
require(accounts[ accId ].userRights[msg.sender].creator, "You don't have rights to do this");
_;
}
// check if the newUser address is already added to the account
modifier noUser(bytes32 accId, address newUser){
require(!accounts[accId].userRights[newUser].user, "User already added");
_;
}
// returns biggest index of sender's "accountIds" array
function myAccounIdMaxIndex() public view checkAccIndex(0) returns(uint){
return( accountIds[ msg.sender ].length -1 );
}
// return ID of senders account with chosen index
function myAccountId( uint accIndex ) public view checkAccIndex(accIndex) returns( bytes32 ) {
return( accountIds[ msg.sender ][ accIndex ] );
}
// returns biggest index of
function maxUserIndex( bytes32 accId ) public view onlyUser(accId) returns(uint){
return( accounts[ accId ].users.length -1 );
}
// return address of chosen user of one of your accounts
function userAddress( bytes32 accId, uint userIndex ) public view onlyUser(accId) checkUserIndex(accId, userIndex) returns( address user ){
return( accounts[ accId ].users[ userIndex ] );
}
// return user rights
function userRights(bytes32 accId, address user) public view onlyUser(accId) returns (bool, bool, bool){
return(
accounts[accId].userRights[user].user,
accounts[accId].userRights[user].manager,
accounts[accId].userRights[user].creator
);
}
}
3. Manageable
This part includes:
- functions to add and delete a user from account
- function to modify user rights
import "./Functional.sol";
pragma solidity 0.5.12;
contract Manageable is Functional{
event userAdded(bytes32 accId, address newUser);
event userDeleted(bytes32 accId, address deletedUser);
event userRightsChanged(bytes32 accId, address user, bool manager, bool owner);
// add a new user to your account
// new user will receive user rights by default
function addUser( bytes32 accId, address newUser )
public onlyManager(accId) noUser(accId, newUser) {
uint usersCount = accounts[ accId ].users.length;
accounts[ accId ].users.push( newUser ); // add new user to account
accounts[ accId ].userRights[newUser].user = true; // mark new user as created
accountIds[ newUser ].push( accId ); // add account to user's "accountIds" array
assert( accounts[ accId ].users.length == usersCount + 1 );
emit userAdded(accId, newUser);
}
// delete user from your account
// removes also the account from user's "accountIds" array
function deleteUser( bytes32 accId, uint userIndex )
public onlyManager(accId) checkUserIndex(accId, userIndex) {
require(userIndex > 0, "You cannot remove creator");
uint usersCount = accounts[accId].users.length; // get user count
address deletedUser = accounts[accId].users[userIndex]; // get deleted user address
delete accounts[accId].users[userIndex]; // delete user from account
if(userIndex < usersCount -1){ // move all to the left to fill gap
for (uint i=userIndex; i<usersCount-1; i++){
accounts[accId].users[i] = accounts[accId].users[i+1];
}
assert(accounts[accId].users[userIndex] != deletedUser);
}
accounts[accId].users.length--; // decrease length
assert(accounts[accId].users[usersCount-2] != deletedUser);
delete accounts[accId].userRights[deletedUser]; // delete user rights
deleteAccFromUser(deletedUser, accId); // delete account from user's accIds array
emit userDeleted(accId, deletedUser);
}
// remove account from user's "accountIds" array
function deleteAccFromUser(address deletedUser, bytes32 accId) internal {
uint userAccCount = accountIds[deletedUser].length; // get length of user's accIds array
for (uint i=0; i < userAccCount; i++){ // find index of the account
if (accountIds[deletedUser][i] == accId){
if (userAccCount==1){ // if it is the only user's account
delete accountIds[deletedUser]; // delete user from mapping
} else {
delete accountIds[deletedUser][i]; // delete account from user
// move all to the left to fill gap
if(i < userAccCount -1){
for (uint j=i; j<userAccCount -1; j++){
accountIds[deletedUser][j] = accountIds[deletedUser][j+1];
}
assert(accountIds[deletedUser][i] != accId);
}
// update array length
accountIds[deletedUser].length--;
assert(accountIds[deletedUser][ userAccCount-2 ] != accId);
break;
}
}
}
}
// modify user's "manager" and "owner" rights values
function changeUserRights(bytes32 accId, uint userIndex, bool manager, bool creator)
public onlyCreator(accId) {
address user = userAddress(accId, userIndex);
accounts[ accId ].userRights[user].manager = manager;
accounts[ accId ].userRights[user].creator = creator;
emit userRightsChanged(accId, user, manager, creator);
}
}
4. AccountManager
This contract defines funds-handling functionality:
- modifiers for transfer functions
- top-up function
- external (to addresses) and internal (to other accounts within contract) transfer functions
- withdraw function
- function to check account balance
import "./Manageable.sol";
pragma solidity 0.5.12;
contract AccountManager is Manageable{
event toppedUp(bytes32 accId, uint amount);
event transferSuccessful(bytes32 senderId, address receiver, uint amount);
event internalTransferSuccessful(bytes32 senderId, bytes32 receiverId, uint amount);
event withdrawalSuccessful(bytes32 senderId, address receiver, uint amount);
modifier nonZeroValue(uint value){
require(value > 0, "Non-zero value required");
_;
}
modifier balanceCheck(bytes32 accId, uint toTransfer){
require(toTransfer <= accounts[ accId ].saldo, "Insufficient funds");
_;
}
function topUp( bytes32 accId ) public payable nonZeroValue(msg.value){
accounts[ accId ].saldo += msg.value;
emit toppedUp(accId, msg.value);
}
function transfer(bytes32 accId, address payable receiver, uint toTransfer)
public onlyUser(accId) nonZeroValue(toTransfer) balanceCheck( accId, toTransfer){
uint oldBalance = accounts[ accId ].saldo;
accounts[ accId ].saldo = oldBalance - toTransfer;
receiver.transfer(toTransfer);
emit transferSuccessful(accId,receiver, toTransfer);
}
function internalTransfer(bytes32 senderAccId, bytes32 receiverAccId, uint toTransfer)
public onlyUser(senderAccId) nonZeroValue(toTransfer) balanceCheck( senderAccId, toTransfer){
uint balanceSum = accounts[ senderAccId ].saldo + accounts[ receiverAccId ].saldo;
accounts[ senderAccId ].saldo -= toTransfer;
accounts[ receiverAccId ].saldo += toTransfer;
assert( balanceSum == accounts[ senderAccId ].saldo + accounts[ receiverAccId ].saldo );
emit internalTransferSuccessful(senderAccId, receiverAccId, toTransfer);
}
function withdraw(bytes32 accId, uint toTransfer)
public onlyUser(accId) nonZeroValue(toTransfer) balanceCheck( accId, toTransfer) {
uint oldBalance = accounts[ accId ].saldo;
accounts[ accId ].saldo = oldBalance - toTransfer;
msg.sender.transfer(toTransfer);
emit withdrawalSuccessful(accId, msg.sender, toTransfer);
}
function accountBalance( bytes32 accId ) public view onlyUser(accId) returns(uint) {
return( accounts[ accId ].saldo );
}
}
Problems
The biggest struggle throughout the whole process was for me creating the deleteUser function.
The function had to not only delete users from account but also the account from the user (as all user addresses are mapped to the array of account IDs). This plus accounting for the edge cases and not leaving behind gaps in the arrays or duplicated values made it time consuming to make it work.
Here I learned to appreciate the assert() statements, as they turned out to be a great help in identifying bugs.
Contract interactions
The contract is deployed on Ropsten testnet on the following address:
0xd5f414294e5D6B12A1fF62952Ca583BDe27513b2
One way to interact with it is to copy the code from this link and paste it into a new file in Remix. Then go to Deploy & run transaction tab, choose Injected Web3 environment, AccountManager contract and paste the contract address into the At Address box and click that button. Contract will appear under Deployed contracts.
Up front I am sorry that the destroy() functions sends the whole contract balance back to me. Next step in improving could definitely be to send back all account balances to account owners on destroying the contract.
Hope this gives you some help or motivation and I will appreciate any feedback.
Good luck to everybody