[UPDATE - Issue Resolved]
I think I found the problem, working on a solution now…
The Solidity Compiler had a hidden error that didn’t show up in the editor:
UnimplementedFeatureError: Copying of type struct
MultiSigWallet.Signature memory[] memory to storage
not yet supported.
I then had to hunt for this and found the culprit in my CreateRequest()
function:
Signature[] memory signatures; // <-- this line
I cannot store a memory
variable type to a storage
data location; unfortunately it’s not an easy "replace memory
with storage
" solution.
I have to change:
uint256 requestID = TransferRequests.length;
Signature[] memory signatures;
TransferRequests.push(
Request(
requestID,
_getOwner(),
_amount,
_toAddress,
_pendingStatus,
signatures
)
);
To:
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;
This source is what helped me find a solution to this problem.
I can now Compile my MultiSigWallet
Contract and debug/fix my project.
Help Please - Remix won’t let me deploy
I’m not able to get Remix to work with my 3 files, I don’t know what’s wrong but the main multisig_wallet.sol
file isn’t showing up in the Contract dropdown menu in order for me to Deploy it; however my other 2 files (owner.sol
and transfer_request.sol
) show up. I don’t see any errors, can anyone here see errors in my code that would cause this? Or test to see if this same problem happens to you with my code?
What I did:
I coded this all offline in Visual Studio Code first (with a Solidity linter/formatter); then in Remix I created each file and copied all the contents in. Perhaps there’s something wrong with doing this?
What I’ve tried:
If I comment almost everything out in multisig_wallet.sol
, it’ll finally show up in the Deploy dropdown but when I Deploy it (even after un-commenting all public functions and removing errors) it won’t show the Contracts public functions for me to click in the Deployed Contracts sections. Eventually multisig_wallet.sol
will disappear again from the Deploy dropdown, I have no idea what’s going on :S this seems like a glitch with Remix, but it’s possible there’s something else wrong that Remix isn’t able to lint?
I’ve tried adding public pure
functions, which didn’t work.
I tried removing the
emoji in case that is breaking the compiler, nope…
my files on Gist
My files:
owner.sol
:
pragma solidity 0.8.7;
contract Owner {
//// TYPE DECLARATIONS ////
// inspired by:
// https://ethereum.stackexchange.com/a/12539/86218
// https://stackoverflow.com/a/49637782/1180523
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() {
require(
!OwnerList[msg.sender].Exists,
"You must be an owner to do this"
);
_;
}
//// PUBLIC FUNCTIONS ////
//// PRIVATE FUNCTIONS ////
}
transfer_request.sol
:
pragma solidity 0.8.7;
import "./owner.sol";
contract TransferRequest is Owner {
//// TYPE DECLARATIONS ////
struct Signature {
User Owner;
bool Approved;
}
struct Request {
uint256 ID;
User Owner;
uint256 Amount;
address payable 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 internal RequiredApprovals;
Request[] internal TransferRequests;
RequestsLookup internal RequestLog;
string _pendingStatus = "pending";
string _completedStatus = "completed";
//// EVENTS ////
//// MODIFIERS ////
// notSelfSigning requires that msg.sender isn't the Request.Owner.
modifier notSelfSigning(uint256 _requestID) {
require(
msg.sender != TransferRequests[_requestID].Owner.Address,
"Cannot self-sign Transfer Requests"
);
_;
}
//// PUBLIC FUNCTIONS ////
//// PRIVATE FUNCTIONS ////
}
multisig_wallet.sol
:
pragma solidity 0.8.7;
import "./transfer_request.sol";
contract MMultiSigWallet is TransferRequest {
//// TYPE DECLARATIONS ////
//// STATE VARIABLES ////
// 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 requestCreated(
uint256 _requestID,
uint256 _amount,
User indexed _owner,
address indexed _to
);
event requestApproved(uint256 _requestID, address indexed _signedBy);
event requestCompleted(uint256 _requestID);
//// MODIFIERS ////
//// PUBLIC FUNCTIONS ////
// 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 payable _toAddress) public isOwner {
require(_amount > 0, "Amount must be greater than 0");
require(
address(this).balance >= _amount,
"Amount must be <= contract balance"
);
uint256 requestID = TransferRequests.length;
Signature[] memory signatures;
TransferRequests.push(
Request(
requestID,
_getOwner(),
_amount,
_toAddress,
_pendingStatus,
signatures
)
);
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) {
Request[] memory requests;
// 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++) {
uint256 requestID = RequestLog.Pending[i];
Request memory r = TransferRequests[requestID];
// 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); // 🙅 NOPE!
requests[i] = r;
}
return requests;
}
// CompletedRequests returns all completed Transfer Requests, this is
// available to Owners and non-Owners.
function CompletedRequests() public view returns (Request[] memory) {
Request[] memory requests;
// 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];
Request memory r = TransferRequests[requestID];
// NOTE: ".push()" cannot be used, see comment in PendingRequests().
requests[i] = r;
}
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
payable
isOwner
notSelfSigning(_requestID)
{
Request storage request = TransferRequests[_requestID];
require(request.Amount != 0, "Transfer Request not found");
require(request.Amount > address(this).balance, "Insufficient funds");
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);
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) internal 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))
);
}
}