Last active
May 30, 2024 13:43
-
-
Save wilfredjonathanjames/bb66ab585ef8512f2c1a3f4eb07f9a25 to your computer and use it in GitHub Desktop.
SmartWallet with social recovery features
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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