Try   HackMD

Secure Deterministic Social Wallet Address

User Story

I want to send some erc20s to someone, but I don't have their address, I only have their twitter handle.

I want to be able to send crypto to a wallet generated by their twitter handle, but that wallet currently does not exist.

Solution

1. In the SDK, someone calls getAddressOffline(twitter handle, rpc, entrypoint address, factory) and we return

Newly Generated Wallet Address = CREATE2(factory contract address, twitter handle, fun wallet bytecode)

Done, do FunWallet.getAddressOffline(uniqueId, index, rpcUrl, factoryAddress)

const {FunWallet} = require("../wallet")
await FunWallet.getAddressOffline("twitter###paradigmeng420", 0)

Then, they can just transfer eth to that address.

Time estimate: 2-3 hours

2. Add a commit function in FunWallet.sol Then, the twitter user who has possession of the twitter handle can call commit

Put this in a FunWallet.sol

mapping(bytes32 => bytes32) commits;
struct commit {
    uint256 expiration;
    bytes32 hash;
}
// @param commitKey keccak(socialHandle + loginType)
// @param hash keccak256(private seed, newOwner address)
function commit(bytes32 commitKey, bytes32 hash) {
    require(commits[commitKey].expiration < block.timestamp);
    commitExpiration = block.timestamp + 10 minutes;
    commits[commitKey] = commit(commitExpiration, hash);
}

TODO: Add this function and tests in the funwallet
Time estimate: 1-2 hours

3. The owner can then post the seed onto their twitter, an oracle comes in and gets the seed, then the wallet is deployed.

Call createAccount with
salt = can be anything
loginType = LoginTypes.TWITTER
socialHandle = twitter###paradigmeng420

enum LoginType{
    EOA;
    Twitter;
    ...
}
struct SocialLoginData {
    uint8 loginType
    uint8 index
    bytes socialHandle
    address newFunWalletOwner
}

function (SocialLoginData sld){
    newOwner = validateSocialLogin()
    // change validationInitData to set Owner
    ... rest of initialize
}

function validateSocialLogin(SocialLoginData sld){
    hashFromOracle, newOwnerAddress = getHashFromOracle(socialHandle, loginType)
    commitKey = keccak(socialHandle, loginType)
    require(hashFromOracle == commits[commitKey].hash);
    salt = keccak(socialHandle + loginType + index)
    return newOwnerAddress
}

function getHashFromOracle(bytes socialHandle, uint8 loginType){
    if (loginType == LoginType.Twitter){
        tweet = gelato.fetchTweet(twitterhandle);
        return keccak(tweet), address;
    }
}

TODO: Create a Oracle that monitors a given user's twitter posts OR that given a tweet link, can return the contents. Then, using the tweet contents, call the reveal function.

Time estimate: 6-8 hours

Scraping twitter:
https://www.scrapingdog.com/blog/scrape-twitter/
https://github.com/trevorhobenshield/twitter-api-client#get-all-usertweet-data
https://www.npmjs.com/package/rettiwt-api

Test everything

TODO: Write an E2E test transferring stuff from twitter accounts and claiming it.
Also, write a few unit tests

Total Time estimate: 9 - 13 hours

TODOs

  • Figure out if it is possible to bypass twitter api(costs 100 per month for 10k) for posts.

Security

  1. Denial of Service: If the "oracle" goes down or the private key of the "oracle" is compromised, no one will be able to claim the wallet.
  2. Denial of Serivce: Someone random can come in and set commit, thus making it impossible for anyone to claim ownership of the wallet.
    • A potential solution would be making commit permissioned as well. However, the oracle could then collude with itself, allowing it to drain any social wallets fun creates.
    • Another solution would be forcing people to pay to call commit. This works as the payment is sent directly to the wallet, if you own it, then you are paying yourself in the future and you can claim it as soon as you call checkReveal.

The Oracle Problem

Basically, we want some permissioned system to fetch the secret from twitter, then pass it into reveal. Here are a few ways to do this:

  • Permissioned Oracle ran by Fun.xyz (Best, but centralized)
  • Chainlink Any Api (Doesn't allow for api authentication)
  • Chainlink Functions (Perfect solution, but not yet live)
  • Gelato Web3 Functions (Researching)
  • Lit Protocol (Researching)

Scratch Work

Alternate implementation

  1. Add oracle address to funwallet factory as a public variable
  2. Add pre-image of salt as a parameter in createAccount in funwallet factory
  3. Add commit to funwallet factory
  4. When calling createAccount, check if the salt contains twitter#, if it does, check if reveal is correct, if reveal passes, then allow account creation.

Tradeoff: This will make createAccount more expensive for everyone, but idk where else we can put a check for this since we need this everytime someone calls createAccount to make sure they can't just create a funwallet from a twitter account

function createAccount(bytes calldata, address impl, bytes32 salt, uint8 loginType, bytes calldata socialHandle, uint8 index) {
    if (loginType == Twitter){
        hash, newOwnerAddress = getHashFromOracle(socialHandle);
        commitKey = keccak256(socialHandle + loginType)
        require(hash == commits[commitKey].hash);
        // note, we change to hashing the salt here instead of passing in a hashed salt
        salt = keccak(socialHandle + loginType)
        require(newOwnerAddress == initializerCalldata.address)
    }
    
    // Handle rest of logic in createAccount like normal
    salt = uniqueId + index
}