Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save skplunkerin/029448936d1dc6ee7edf69948b934ecf to your computer and use it in GitHub Desktop.
Save skplunkerin/029448936d1dc6ee7edf69948b934ecf to your computer and use it in GitHub Desktop.
Multi Sig Wallet Assignment - Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.7+commit.e28d00a7.js&optimize=false&runs=200&gist=
pragma solidity 0.8.7;
import "./ownable.sol";
contract MultiSigWallet is Ownable {
//// TYPE DECLARATIONS ////
struct Signature {
User Owner;
bool Signed;
}
struct Request {
uint256 ID;
User Owner;
uint256 Amount;
address ToAddress;
string Status;
Signature[] Signatures;
}
struct RequestsLookup {
uint256[] Pending;
uint256[] Completed;
// TODO: can add more logic to account for Transfer Requests that get
// denied. (chris)
// uint256[] Denied;
}
//// STATE VARIABLES ////
uint256 public requiredApprovals;
Request[] private transferRequests;
RequestsLookup private requestLog;
string private _pendingStatus = "pending";
string private _completedStatus = "completed";
// constructor sets the owner address in OwnerList.
constructor(address[] memory _owners, uint8 _approvalCount) {
requiredApprovals = _approvalCount;
// Loop through all addresses to add to OwnerList:
for (uint256 i = 0; i < _owners.length; i++) {
ownerList[_owners[i]] = User(_owners[i], true);
}
}
//// EVENTS ////
event Deposited(uint256 _amount, address indexed _fromAddress);
event RequestCreated(
uint256 _requestID,
uint256 _amount,
User indexed _owner,
address indexed _toAddress
);
event RequestApproved(uint256 _requestID, address indexed _signedBy);
event RequestCompleted(uint256 _requestID);
//// MODIFIERS ////
// canApproveRequest requests that the Transfer Request was found; that the
// Contract has enough balance for the Transfer; that the signer isn't
// trying to self-sign their own Transfer Request; and that the signer
// hasn't already signed the Transfer Request.
modifier canApproveRequest(uint256 _requestID) {
Request memory request = transferRequests[_requestID];
require(request.Amount != 0, "Transfer Request not found");
require(request.Amount >= address(this).balance, "Insufficient funds");
require(
msg.sender != request.Owner.Address,
"Cannot self-sign Transfer Requests"
);
bool alreadySigned = false;
for (uint256 i = 0; i < request.Signatures.length; i++) {
if (msg.sender == request.Signatures[i].Owner.Address) {
alreadySigned = true;
break; // exit the loop early
}
}
require(!alreadySigned, "Already signed this Transfer Request");
_;
}
//// PUBLIC FUNCTIONS ////
// getBalance returns the Contracts balance.
function getBalance() public view returns (uint256) {
return address(this).balance;
}
// deposit transfers funds to the Contract (anyone can deposit funds).
function deposit() public payable {
emit Deposited(msg.value, msg.sender);
}
// createRequest will push a new Request to the TransferRequests array if
// msg.sender is an Owner, and if _amount is greater than 0 and less than or
// equal to the contracts balance.
function createRequest(uint256 _amount, address _toAddress) public isOwner {
require(_amount > 0, "Amount must be greater than 0");
require(
address(this).balance >= _amount,
"Amount must be <= contract balance"
);
// NOTE: cannot copy a "memory" type Signature[] to a new Request that
// is stored in "storage", see error:
// - UnimplementedFeatureError: Copying of type struct
// MultiSigWallet.Signature memory[] memory to storage not yet
// supported.
// uint256 requestID = transferRequests.length;
// Signature[] memory signatures;
// transferRequests.push(
// Request(
// requestID,
// _getOwner(),
// _amount,
// _toAddress,
// _pendingStatus,
// signatures
// )
// );
//
// Here's a workaround for this problem:
// source: https://github.com/ethereum/solidity/issues/4115#issuecomment-612309066
transferRequests.push();
uint256 requestID = transferRequests.length - 1;
Request storage newRequest = transferRequests[requestID];
newRequest.ID = requestID;
newRequest.Owner = _getOwner();
newRequest.Amount = _amount;
newRequest.ToAddress = _toAddress;
newRequest.Status = _pendingStatus;
requestLog.Pending.push(requestID);
emit RequestCreated(requestID, _amount, _getOwner(), _toAddress);
}
// pendingRequests returns all un-completed Transfer Requests if msg.sender
// is an Owner.
function pendingRequests() public view isOwner returns (Request[] memory) {
// NOTE: unable to do "requests[0] = request;" in loop unless we set a
// length.
// solution source: https://stackoverflow.com/a/55532934/1180523
Request[] memory requests = new Request[](requestLog.Pending.length);
// Loop through the Pending request ID's and return each Transfer
// Request for the ID's found:
for (uint256 i = 0; i < requestLog.Pending.length; i++) {
// NOTE: ".push()" cannot be used due for memory arrays:
// - Member "push" is not available in struct Request memory[]
// memory outside of storage.
// docs info: https://docs.soliditylang.org/en/v0.8.10/types.html#allocating-memory-arrays
// BUT chaning requests to use storage also doesn't work:
// - This variable is of storage pointer type and can be accessed
// without prior assignment, which would lead to undefined
// behaviour.
//
// This is a workaround, I'm not sure if there's a better approach:
// requests.push(r);
uint256 requestID = requestLog.Pending[i];
requests[i] = transferRequests[requestID];
}
return requests;
}
// completedRequests returns all completed Transfer Requests, this is
// available to Owners and non-Owners.
function completedRequests() public view returns (Request[] memory) {
// NOTE: unable to do "requests[0] = request;" in loop unless we set a
// length.
// solution source: https://stackoverflow.com/a/55532934/1180523
Request[] memory requests = new Request[](requestLog.Completed.length);
// Loop through the Completed request ID's and return each Transfer
// Request for the ID's found:
for (uint256 i = 0; i < requestLog.Completed.length; i++) {
uint256 requestID = requestLog.Completed[i];
requests[i] = transferRequests[requestID];
}
return requests;
}
// approveRequest adds an Approved signature to a Request if isOwner and not
// self-signing the Request; Then if all required approvals are met, the
// Request is moved from Pending to Completed status and the funds are
// transferred.
function approveRequest(uint256 _requestID)
public
isOwner
canApproveRequest(_requestID)
{
Request storage request = transferRequests[_requestID];
request.Signatures.push(Signature(_getOwner(), true));
emit RequestApproved(_requestID, _getOwner().Address);
// if we have enough signatures, mark the request as complete and
// transfer the funds:
if (request.Signatures.length >= requiredApprovals) {
_pendingToCompleted(request);
payable(request.ToAddress).transfer(request.Amount);
emit RequestCompleted(_requestID);
}
}
//// PRIVATE FUNCTIONS ////
// _getOwner returns msg.sender as a User{} struct type if the msg.sender is
// an Owner.
function _getOwner() private view isOwner returns (User memory) {
return User(msg.sender, true);
}
// _pendingToCompleted moves the _request.ID out of RequestLog.Pending to
// RequestLog.Completed and marks _request.Status as completed.
function _pendingToCompleted(Request storage _request) private isOwner {
uint256 pendingLength = requestLog.Pending.length;
uint256 completedLength = requestLog.Completed.length;
string memory previousStatus = _request.Status;
uint256[] memory newPending;
// Move requestID out of Pending to Completed TransferRequests:
uint256 j = 0;
for (uint256 i = 0; i < pendingLength; i++) {
if (requestLog.Pending[i] != _request.ID) {
newPending[j] = requestLog.Pending[i];
j++;
}
}
requestLog.Pending = newPending;
requestLog.Completed.push(_request.ID);
_request.Status = _completedStatus;
assert(requestLog.Pending.length == pendingLength - 1);
assert(requestLog.Completed.length == completedLength + 1);
// to compare strings, convert them to bytes, generate a Hashes, and
// compare the hashes:
// https://docs.soliditylang.org/en/v0.8.10/types.html#bytes-and-string-as-arrays
assert(
keccak256(bytes(_request.Status)) !=
keccak256(bytes(previousStatus))
);
}
}
pragma solidity 0.8.7;
contract Ownable {
//// TYPE DECLARATIONS ////
struct User {
address Address;
bool Exists;
}
//// STATE VARIABLES ////
mapping(address => User) internal ownerList;
//// EVENTS ////
//// MODIFIERS ////
// isOwner requires that msg.sender is in the OwnerList.
modifier isOwner() {
// inspired by:
// https://ethereum.stackexchange.com/a/12539/86218
// https://stackoverflow.com/a/49637782/1180523
require(
ownerList[msg.sender].Exists,
"You must be an owner to do this"
);
_;
}
//// PUBLIC FUNCTIONS ////
//// PRIVATE FUNCTIONS ////
}

Deploy Instructions

  1. Go to https://remix.ethereum.org

  2. Click on the Deploy & run transactions on the left navigation

  3. Choose the MultiSigWallet contract in the CONTRACT dropdown

  4. Paste the following in the Deploy input, then click Deploy:

    ["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db"], 2
    

How can I improve my code?

  • From watching Filip's implementation:
    1. in constructor:
      • check that each Owner address is unique, you shouldn't be able to add the same address
    2. in approveRequest:
      • owner shouldn't be able to approve a request that has already been sent
  • Gold plating:
    • what I wanted to add but didn't to keep this simple:
      • add a way to deny requests

Project Requirements

– ✅ Anyone should be able to deposit ether into the smart contract

– ✅ The contract creator should be able to input the following (in the constructor) :

  1. ✅ the addresses of the owners
  2. ✅ the number of approvals required for a transfer

For example, input 3 addresses and set the approval limit to 2.

– ✅ Any one of the owners should be able to create a transfer request.

  • ✅ The creator of the transfer request will specify:
    1. ✅ what amount
    2. ✅ what address to transfer amount to

– ✅ Owners should be able to approve transfer requests.

– ✅ When a transfer request has the required approvals, the transfer should be sent.

MultiSig Wallet Plan:

  • Object types (break out to own contracts?):

    • MultiSig (project)
      • objects needed:
        • address[3] Owners;
        • struct Approval { address Owner bool Approved }
        • struct TransferRequest { uint Amount address payable ToAddress []Approval Approvals bool Completed }
      • modifiers needed:
        • isOwner
      • functions needed:
        • Deposit() payable (anyone)
        • CreateTransferRequest(amount, toAddress) isOwner
        • ApprovedTransferRequests
        • DeniedTransferRequests
        • TransferRequests isOwner
        • ApproveTransferRequest isOwner
    • Owner (child)
    • Transfer Request (child)
      • Create TR
        • the amount
        • the wallet to transfer to
      • Return TR's
      • Approve TR's
        • when the last needed approval is made, the transfer will occur.
  • Functions/Actions:

    • Constructor
      • input 3 addresses and approval limit on contract creation
    • Deposit money
      • any one can deposit (not just owners)
    • Transfer Requests
      • only owners can create, view, and approve TR's
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment