Skip to content

Instantly share code, notes, and snippets.

@wilfredjonathanjames
Last active May 30, 2024 13:43
Show Gist options
  • Save wilfredjonathanjames/bb66ab585ef8512f2c1a3f4eb07f9a25 to your computer and use it in GitHub Desktop.
Save wilfredjonathanjames/bb66ab585ef8512f2c1a3f4eb07f9a25 to your computer and use it in GitHub Desktop.
SmartWallet with social recovery features
// SPDX-License-Identifier: MIT
/*
A smart wallet with the following features:
- 1 owner
- Uses a fallback function to receive funds
- Can send money to EOA and contract accounts
- Can give an allowance to addresses to spend
- Can take back remaining allowances
- 5 guardians defined at setup, any 3 can vote to replace the owner of the contract
- A vote time limit defined at contract creation prevents timing issues
- Guardians can be replaced by the owner
Known issues:
- Duplicate guardian addresses will lead to undefined behaviour
Remix test data:
- Creation:
- Owner: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
- Guardians: ["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db","0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB","0x617F2E2fD72FD9D5503197092aC168c91465E7f2","0x17F6AD8Ef982297579C203069C1DbfFE4348c372"]
- Guardian voting window: 60
- Ownership change:
- New owner: 0xdD870fA1b7C4700F2BD7f44238821C26f7392148
- Guardian change:
- Guardians: ["0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678","0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7","0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C","0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC","0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c"]
*/
pragma solidity 0.8.17;
struct GuardianData {
// remember which vote was made previously, so that we can remove it
address previousVoteAddress;
uint256 voteTimestamp;
// because mappings return a result for any index, use
// a flag to allow an efficient check of guardianship
bool isGuardian;
}
contract Wallet {
uint8 constant GUARDIAN_COUNT = 5;
uint8 constant GUARDIAN_VOTE_THRESHOLD = 3;
event CheckTime(uint256 a, uint256 b, bool result);
event OwnerChangeVote(
address guardian,
address newOwner,
uint256 timestamp
);
event OwnerChanged(
address newOwner,
address[GUARDIAN_VOTE_THRESHOLD] guardians
);
address public owner;
mapping(address => uint256) public allowances;
uint256 public guardianVotingWindow;
uint8 public guardianVotesThreshold = GUARDIAN_VOTE_THRESHOLD;
address[5] public guardianAddresses; // allows us to iterate over guardians
mapping(address => GuardianData) public guardianData;
constructor(
address _owner,
address[GUARDIAN_COUNT] memory _guardianAddresses,
uint32 _guardianVotingWindow
) {
owner = _owner;
guardianVotingWindow = _guardianVotingWindow;
setGuardianAddreses(_guardianAddresses);
}
receive() external payable {}
function checkSenderIsOwner() private view {
require(msg.sender == owner, "Unauthorised: Not owner.");
}
function checkGuardian() private view {
require(
guardianData[msg.sender].isGuardian,
"Unauthorised: Not guardian."
);
}
function checkSenderIsOwnerOrBeneficiary() private view returns (bool) {
bool isBeneficiary = allowances[msg.sender] > 0;
require(msg.sender == owner || isBeneficiary, "Unauthorised.");
return isBeneficiary;
}
function transfer(uint256 amount, address payable destination, bytes memory payload) public returns(bytes memory) {
bool isBeneficiary = checkSenderIsOwnerOrBeneficiary();
require(amount <= address(this).balance, "Not enough balance.");
// if this user is a beneficiary, ensure that we check that they have enough
// and subtract from it
if (isBeneficiary) {
require(allowances[msg.sender] >= amount, "Not enough allowance.");
_takeAllowance(amount, msg.sender);
}
(bool success, bytes memory returnValue) = destination.call{value: amount}(payload);
require(
success,
"Destination unable to receive payment."
);
return returnValue;
}
// Allowance management
function giveAllowance(uint256 amount, address beneficiary) public {
checkSenderIsOwner();
allowances[beneficiary] += amount;
}
function takeAllowance(uint256 amount, address beneficiary) public {
checkSenderIsOwner();
_takeAllowance(amount, beneficiary);
}
function _takeAllowance(uint256 amount, address beneficiary) private {
// if amount is more than or equal to allowance, delete allowance
if (amount >= allowances[beneficiary]) {
delete allowances[beneficiary];
// if amount is subtractable from allowance, subtract amount
} else {
allowances[beneficiary] -= amount;
}
}
// Guardian voting
function setGuardianAddreses(
address[GUARDIAN_COUNT] memory _guardianAddresses
) private {
// remove any existing guardians
for (uint8 i = 0; i < guardianAddresses.length; i++) {
delete guardianData[guardianAddresses[i]];
}
// set the new guardians
for (uint8 i = 0; i < GUARDIAN_COUNT; i++) {
guardianData[_guardianAddresses[i]] = GuardianData(
address(0),
0,
true
);
}
guardianAddresses = _guardianAddresses;
}
function replaceGuardians(address[GUARDIAN_COUNT] memory _guardianAddresses)
public
{
checkSenderIsOwner();
setGuardianAddreses(_guardianAddresses);
}
function guardianVotingWindowPassed(uint256 timestamp)
private view
returns (bool)
{
uint256 timeDifference = block.timestamp - timestamp;
bool result = timeDifference > guardianVotingWindow;
return result;
}
function guardianVote(address _newOwner) public {
checkGuardian();
GuardianData storage guardian = guardianData[msg.sender];
// retrieve previous vote data
address previousVoteAddress = guardian.previousVoteAddress;
bool previousTooOld = guardianVotingWindowPassed(
guardian.voteTimestamp
);
// check we haven't voted for the same person before the vote expired
require(
previousVoteAddress != _newOwner || previousTooOld,
"You have already voted for this new owner within the voting window."
);
uint256 timestamp = block.timestamp;
// update the guiardian's vote timestamp and previous vote address
guardian.voteTimestamp = timestamp;
guardian.previousVoteAddress = _newOwner;
emit OwnerChangeVote(msg.sender, _newOwner, timestamp);
// attempt to execute the vote
executeVotesForAddress(_newOwner);
}
function executeVotesForAddress(address _newOwner) private {
uint8 votes;
uint8 guardiansIndex;
address[GUARDIAN_VOTE_THRESHOLD] memory guardians;
// go through all guardians
for (uint8 i = 0; i < guardianAddresses.length; i++) {
address guardianAddress = guardianAddresses[i];
GuardianData memory guardian = guardianData[guardianAddress];
// if this guardian voted for the newOwner address within the guardianVotingWindow
bool tooOld = guardianVotingWindowPassed(guardian.voteTimestamp);
if (guardian.previousVoteAddress == _newOwner && !tooOld) {
// increment the vote count
votes++;
guardians[guardiansIndex] = guardianAddress;
guardiansIndex++;
// check if we have enough votes
if (votes >= guardianVotesThreshold) {
// reset successful guardians
for (uint8 n = 0; n < GUARDIAN_VOTE_THRESHOLD; n++) {
address resetGuardianAddress = guardians[n];
GuardianData storage resetGuardian = guardianData[
resetGuardianAddress
];
resetGuardian.voteTimestamp = 0;
}
// change the owner
owner = _newOwner;
emit OwnerChanged(owner, guardians);
break;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment