Git Product home page Git Product logo

Comments (4)

ra-phael avatar ra-phael commented on June 3, 2024

Here's a draft of a smart contract, assuming we rely on a server which holds a private key and submits transactions to certify an address's reputation.

I think it should be improved to use EIP-712 (https://eips.ethereum.org/EIPS/eip-712), but that would just enrich the signed data, the flow would be the same.

Steps

  1. After successful authentication with a web 2 provider and connection with an address, users can get their reputation score from the Reputation Service
  2. Users can then sign an attestation which contains the following information: chainId to avoid replay on another chain, their address (_recipient) and the amount of reputation as determined from the previous step.
  3. Our server calls setReputation on the Reputable contract with the signature, the user's address, and the amount: if the signature is valid, it effectively gives a balance of amount number of tokens to this particular address. Only our server (attestor) can call setReputation.

From there on, checking if the reputation of an address 0x3e5... is greater than X can be performed very easily with 0 downtime:
In Solidity

require(Reputable.reputationOf(0x3e5...) > X, "Reputation is insufficient")

And with ethers

const isReputableEnough = reputable.reputationOf("0x3e5...") > X

Contracts

pragma solidity >=0.8.0;

import "./Controlled.sol";

contract Reputable is Controlled {
    // name and symbol of the token
    string public name;
    string public symbol;
    
    // Each address's balance represents its level of reputation
    mapping(address => uint256) private _balances;
    
    event ReputationSet(address indexed account, uint256 amount);
    
     /**
     * Constructor
     * @param chainId_ makes signatures unique to a chain
     * @param attestor_ is the address of the Reputation Service (backend)
     */
    constructor(string memory name_, string memory symbol_, uint256 chainId_, address attestor_) 
    Controlled(chainId_, attestor_) {
        name = name_;
        symbol = symbol_;
    }
    
    function _mint(address account, uint256 amount) private {
        require(account != address(0), "Can't mint to the zero address");
        
        // overrides with new reputation level
        _balances[account] = amount;
        
        emit ReputationSet(account, amount);
    }
    
    function setReputation(address _recipient, uint256 _amount, bytes memory _signature) public
    onlyAttestor()
    withValidAttestation( _recipient,  _amount, _signature)  {
        _mint(_recipient, _amount);
    }
    
    // Returns the reputation level of _account
    function reputationOf(address _account) public view returns (uint256) {
        return _balances[_account];
    }
}


//////////////////////////////////////////////////////////////////////

pragma solidity >=0.8.0;


import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol";


contract Controlled {
    uint256 public chainId;
    address private attestor;
    
    constructor(uint256 _chainId, address _attestor) {
        chainId = _chainId;
        attestor = _attestor;
    }

    /**
     * Outputs the full "attestation hash" including chainId
     */
    function _getAttestationHash(address _recipient, uint256 _amount)
        private
        view
        returns (bytes32)
    {
        bytes32 hash = keccak256(
            abi.encodePacked(chainId, _recipient, _amount)
        );
        return hash;
    }
    
    /**
     * Outputs the signer of _signature
     * after re-constructing the full attestation hash
     */
    function _recoverSigner(
        address _recipient,
        uint256 _amount,
        bytes memory _signature
    ) private view returns (address) {
        bytes32 hash = _getAttestationHash(_recipient, _amount);
        address signer = ECDSA.recover(hash, _signature);
        
        return signer;
    }
    
     /**
     * Verifies that the attestation was signed by _recipient
     * and reverts if it's not the case
     */
    modifier withValidAttestation(
        address _recipient,
        uint256 _amount,
        bytes memory _signature
    ) {
        require(
            _recoverSigner(_recipient, _amount, _signature) == _recipient,
            "Invalid signature"
        );
        _;
    }
    
     /**
     * Restricts execution to the attestor
     */
     modifier onlyAttestor() {
         require(msg.sender == attestor, "Permission denied");
         _;
     }
    
}

Considerations:

  • Someone whose reputation on Twitter change should be able to go through the process to have their on-chain reputation be updated. setReputation would be called again and the previous balance overridden for simplicity sake.
  • Similarly, the reputation of an address could be revoked by setting its balance to 0. For transferring reputation from address A to address B, that means 2 transactions. We can use the signed data to do that with only 1 transaction, I'll provide an updated version.
  • Is decimals needed? I think we can stick to integers which already provide a level of flexibility : for example a 0-100 or 0-1000 scale
  • Instead of a mapping to balances, we can map to a boolean (reputable or not) with limited changes
  • I wanted to keep this first draft simple but we should add access control - at least to update the attestor's address - pause mechanism, make it upgradable etc...

from reputation-service.

arcalinea avatar arcalinea commented on June 3, 2024

To make a record of our conversations:

  • Instead of balances or a boolean, I think we need to do NTTs (non-transferable tokens) as "badges" for reputation attestations. We don't have a clear rubric for how balances would be calculated right now, and it adds more complexity than a simple token that associates an address with a Web2 account.
  • When a badge is unlinked or revoked, it should be sent to the 0 address, and then our service can issue a new one.
  • There should be an expiry period on a badge, so in case an address is compromised it doesn't steal that reputation forever. Maybe the InterRep service can also unilaterally revoke them? Centralizes power though.

In terms of implementation, we talked today about having a master InterRep contract that links to sub-contracts for each provider - a Twitter contract, a Github contract, etc. Should look into if this is the best approach.

from reputation-service.

ra-phael avatar ra-phael commented on June 3, 2024

Following our conversation, I remove URIs associated with tokens and the hasBadge method. So here's below the updated version.

Regarding the generation of token ids being off-chain, one upside I was thinking of is a bit more privacy. If we have a simple counter that goes 1, 2, 3... it's easy to call ownerOf repeatedly and get a list of addresses with the badge. That might not be a problem for NFT art pieces. But in our case, we could have very different ids among the whole spectrum of 256-bit integers. Now that would take longer to search through.
However, because we're emitting events for each time a token is minted and because they will be indexed, it will be easy to get the list of addresses that got the badge.

Interface

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IBadge {
    /**
     * @dev Emitted when `tokenId` token is minted to `to`.
     * @param to The address that received the token
     * @param tokenId The id of the token that was minted
     * @param timestamp Block timestamp from when the token was minted
     */
    event Minted(
        address indexed to,
        uint256 indexed tokenId,
        uint256 timestamp
    );

    /**
     * @dev Emitted when `tokenId` token is burned.
     * @param owner The address that used to own the token
     * @param tokenId The id of the token that was burned
     * @param timestamp Block timestamp from when the token was burned
     */
    event Burned(
        address indexed owner,
        uint256 indexed tokenId,
        uint256 timestamp
    );

    /**
     * @dev Returns the badge's name.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the badge's symbol.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the ID of the token owned by `owner`, if it owns one, and 0 otherwise
     *
     * Requirements:
     *
     * - `owner` cannot be the zero address.
     */
    function tokenOf(address owner) external view returns (uint256);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address);
}

Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IBadge.sol";

contract Badge is IBadge {
    // Badge's name
    string private _name;

    // Badge's symbol
    string private _symbol;

    // Mapping from token ID to owner's address
    mapping(uint256 => address) private _owners;

    // Mapping from owner's address to token ID
    mapping(address => uint256) private _tokens;

    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    // Returns the badge's name
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    // Returns the badge's symbol
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    // Returns the token ID owned by `owner`, if it exists, and 0 otherwise
    function tokenOf(address owner) public view virtual override returns (uint256) {
        require(owner != address(0), "Invalid owner at zero address");

        return _tokens[owner];
    }

    // Returns the owner of a given token ID, reverts if the token does not exist
    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        require(tokenId != 0, "Invalid tokenId value");

        address owner = _owners[tokenId];

        require(owner != address(0), "Invalid owner at zero address");

        return owner;
    }

    // Checks if a token ID exists
    function _exists(uint256 tokenId) internal view virtual returns (bool) {
        return _owners[tokenId] != address(0);
    }

    /**
     * @dev Mints `tokenId` and transfers it to `to`.
     * Requirements:
     *
     * - `tokenId` must not exist.
     * - `to` cannot be the zero address.
     *
     * Emits a {Minted} event.
     */
    function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), "Invalid owner at zero address");
        require(tokenId != 0, "Token ID cannot be zero");
        require(!_exists(tokenId), "Token already minted");
        require(tokenOf(to) == 0, "Owner already has a token");


        _tokens[to] = tokenId;
        _owners[tokenId] = to;

        emit Minted(to, tokenId, block.timestamp);
    }

    /**
     * @dev Burns `tokenId`.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     *
     * Emits a {Burned} event.
     */
    function _burn(uint256 tokenId) internal virtual {
        address owner = Badge.ownerOf(tokenId);

        delete _tokens[owner];
        delete _owners[tokenId];

        emit Burned(owner, tokenId, block.timestamp);
    }
}

TwitterBadge

Example of implementation for our case

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./Badge.sol";
import "../shared/Controlled.sol";

contract TwitterBadge is Badge, Controlled {
    constructor(address backendAddress_)
        Badge("TwitterBadge", "TWITT")
        Controlled(backendAddress_)
    {}

    function mint(address to, uint256 tokenId) external {
        require(msg.sender == _backendAddress, "Unauthorized");
        _mint(to, tokenId);
    }

    function burn(uint256 tokenId) external {
        require(
            msg.sender == _backendAddress || msg.sender == ownerOf(tokenId),
            "Unauthorized"
        );
        _burn(tokenId);
    }
}

Source: https://github.com/arcalinea/reputation-service/tree/contracts-draft/src/contracts/OneContractPerProvider

from reputation-service.

ra-phael avatar ra-phael commented on June 3, 2024

Closing this issue as we went for a standard ERC721 NFT. The reasons were better interoperability and allowing transfers so that the NFTs can be used as collateral.

from reputation-service.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.