├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── foundry.toml ├── readme.md └── src ├── Badges ├── Badges.sol └── Badges.test.sol ├── Passport ├── Passport.sol └── Passport.test.sol └── Rep ├── Rep.sol └── Rep.test.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Dotenv file 11 | .env 12 | 13 | #VSCode 14 | .vscode/ 15 | crytic_export/ 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | optimizer = true 6 | optimizer_runs = 200 7 | 8 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | _Warning: this is WIP. Do not use._ 2 | 3 | ## Passport 4 | 5 | [![Passport overview](https://cdn.loom.com/sessions/thumbnails/1fdc5c939543498b969f9fafc9e0f530-with-play.gif)](https://www.loom.com/share/1fdc5c939543498b969f9fafc9e0f530 "Passport overview") 6 | 7 | A Passport NFT (ERC-721) is meant to represent membership in a DAO. 8 | This Passport can only be minted or transferred by the DAO. 9 | 10 | The DAO can also issue a non-transferable token called Rep (ERC-20), a token that can be used for voting, etc. 11 | 12 | The DAO can also issue non-transferable Badges. The badges can be fungible, non-fungible, or semi-fungible tokens (ERC-1155). 13 | 14 | If the Passport owner changes, all Badge tokens, and the Rep token will "follow" the Passport owner. To make the Badges and the Rep visible in the Passport holder's wallet. Anybody can manually trigger the move of the Rep and Badges to the new Passport owner, or it will happen automatically the next time any of the tokens are burned or new tokens are minted. 15 | 16 | ### Deployment Examples 17 | 18 | Passport: 19 | 20 | ``` 21 | forge create src/Passport/Passport.sol:Passport --constructor-args 0xOwner "dOrg Passport v1" "dPass" "https://www.dorg.tech/passport/" --private-key "xxx" --rpc-url "https://rpc.ankr.com/eth_goerli" --verify --etherscan-api-key "xxx" 22 | ``` 23 | 24 | Rep: 25 | 26 | ``` 27 | forge create src/Rep/Rep.sol:Rep --constructor-args 0xOwner 0xPassport "dOrg Rep v1" "dRep" --private-key "xxx" --rpc-url "https://rpc.ankr.com/eth_goerli" --verify --etherscan-api-key "xxx" 28 | ``` 29 | 30 | Badges: 31 | 32 | ``` 33 | forge create src/Badges/Badges.sol:Badges --constructor-args 0xOwner 0xPassport "https://www.dorg.tech/badges/" --private-key "xxx" --rpc-url "https://rpc.ankr.com/eth_goerli" --verify --etherscan-api-key "xxx" 34 | ``` 35 | -------------------------------------------------------------------------------- /src/Badges/Badges.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {ERC1155} from "../../lib/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; 5 | import {Ownable} from "../../lib/openzeppelin-contracts/contracts/access/Ownable.sol"; 6 | 7 | import {Passport} from "../Passport/Passport.sol"; 8 | 9 | contract Badges is ERC1155, Ownable { 10 | Passport public immutable passport; 11 | mapping(uint256 => mapping(uint256 => address)) private passportToTokenToAddress; 12 | 13 | error Disabled(); 14 | 15 | modifier updateAddress(uint256 passportId, uint256[] memory tokenIds) { 16 | updateOwner(passportId, tokenIds); 17 | _; 18 | } 19 | 20 | constructor(address owner, address passport_, string memory uri_) ERC1155(uri_) { 21 | passport = Passport(passport_); 22 | // check that the owner is an admin of the passport 23 | require(passport.hasRole(passport.DEFAULT_ADMIN_ROLE(), owner), "Badges: initial owner must be passport admin"); 24 | transferOwnership(owner); 25 | } 26 | 27 | function updateOwner(uint256 passportId, uint256[] memory tokenIds) public { 28 | address passportOwner = passport.ownerOf(passportId); // reverts if does not exist 29 | mapping(uint256 => address) storage passportAddressForToken = passportToTokenToAddress[passportId]; 30 | 31 | for (uint256 tokenIndex = 0; tokenIndex < tokenIds.length; tokenIndex++) { 32 | uint256 currentTokenId = tokenIds[tokenIndex]; 33 | address currentTokenOwner = passportAddressForToken[currentTokenId]; 34 | 35 | if (currentTokenOwner == address(0)) { 36 | // its the first time we see this passport for this token 37 | passportAddressForToken[currentTokenId] = passportOwner; 38 | } else if (currentTokenOwner != passportOwner) { 39 | // the passport has moved 40 | uint256 balance = balanceOf(passportAddressForToken[currentTokenId], currentTokenId); 41 | _safeTransferFrom(currentTokenOwner, passportOwner, currentTokenId, balance, ""); 42 | 43 | passportAddressForToken[currentTokenId] = passportOwner; 44 | } 45 | } 46 | } 47 | 48 | function setURI(string memory newUri) external onlyOwner { 49 | _setURI(newUri); 50 | } 51 | 52 | function mint(uint256 passportId, uint256 tokenId, uint256 amount, bytes memory data) external onlyOwner { 53 | uint256[] memory tokenIds = new uint256[](1); 54 | tokenIds[0] = tokenId; 55 | updateOwner(passportId, tokenIds); 56 | _mint(passport.ownerOf(passportId), tokenId, amount, data); 57 | } 58 | 59 | function mintBatch(uint256 passportId, uint256[] memory tokenIds, uint256[] memory amounts, bytes memory data) 60 | external 61 | onlyOwner 62 | updateAddress(passportId, tokenIds) 63 | { 64 | _mintBatch(passport.ownerOf(passportId), tokenIds, amounts, data); 65 | } 66 | 67 | function burn(uint256 passportId, uint256 tokenId, uint256 amount) external onlyOwner { 68 | uint256[] memory tokenIds = new uint256[](1); 69 | tokenIds[0] = tokenId; 70 | updateOwner(passportId, tokenIds); 71 | _burn(passport.ownerOf(passportId), tokenId, amount); 72 | } 73 | 74 | function burnBatch(uint256 passportId, uint256[] memory tokenIds, uint256[] memory values) 75 | external 76 | onlyOwner 77 | updateAddress(passportId, tokenIds) 78 | { 79 | _burnBatch(passport.ownerOf(passportId), tokenIds, values); 80 | } 81 | 82 | function balanceOf(uint256 passportId, uint256 tokenId) external view returns (uint256) { 83 | return super.balanceOf(passport.ownerOf(passportId), tokenId); 84 | } 85 | 86 | function balanceOfBatch(uint256[] memory passportIds, uint256[] memory ids) 87 | external 88 | view 89 | returns (uint256[] memory) 90 | { 91 | address[] memory accounts = new address[](passportIds.length); 92 | 93 | for (uint256 i = 0; i < passportIds.length; ++i) { 94 | accounts[i] = passport.ownerOf(passportIds[i]); // this is safe, the passport can be trusted. Also if it should fail, it will be possible to call again with less number of passports 95 | } 96 | return balanceOfBatch(accounts, ids); 97 | } 98 | 99 | function setApprovalForAll( 100 | address, 101 | /* operator */ 102 | bool /* approved */ 103 | ) public pure override { 104 | revert Disabled(); 105 | } 106 | 107 | function isApprovedForAll( 108 | address, 109 | /* account */ 110 | address operator 111 | ) public view override returns (bool) { 112 | return address(this) == operator; 113 | } 114 | 115 | function safeTransferFrom( 116 | address, /* from */ 117 | address, /* to */ 118 | uint256, /* tokenId */ 119 | uint256, /*amount*/ 120 | bytes memory /* data */ 121 | ) public pure override { 122 | revert Disabled(); 123 | } 124 | 125 | function safeBatchTransferFrom( 126 | address, /* from*/ 127 | address, /* to*/ 128 | uint256[] memory, /* ids*/ 129 | uint256[] memory, /* amounts*/ 130 | bytes memory /* data */ 131 | ) public pure override { 132 | revert Disabled(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Badges/Badges.test.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../../lib/forge-std/src/Test.sol"; 5 | import {Strings} from "../../lib/openzeppelin-contracts/contracts/utils/Strings.sol"; 6 | import {Badges} from "./Badges.sol"; 7 | import {Passport} from "../Passport/Passport.sol"; 8 | 9 | contract BadgesTest is Test { 10 | Badges public badges; 11 | address constant owner = address(0x1); 12 | 13 | Passport passport; 14 | address constant passport_transferer = address(0x6); 15 | 16 | address constant alice = address(0x4); 17 | address constant bob = address(0x5); 18 | 19 | uint256 constant alicePassportId = 0; 20 | uint256 constant bobPassportID = 1; 21 | 22 | uint256 constant badgeOne = 0; 23 | 24 | function setUp() public { 25 | passport = new Passport(address(this), "Passport", "PASS", ""); 26 | passport.grantRole(passport.TRANSFERER_ROLE(), passport_transferer); 27 | 28 | badges = new Badges(address(this), address(passport), "http/{id}"); 29 | 30 | badges.transferOwnership(owner); 31 | 32 | // by default the passport deployer gets the minter role 33 | passport.safeMint(alice); // gets a passport id 0 34 | passport.safeMint(bob); // gets a passport id 1 35 | 36 | vm.prank(owner); 37 | badges.mint(alicePassportId, badgeOne, 1, ""); 38 | } 39 | 40 | function testSetupParameters() public { 41 | assertEq(badges.uri(badgeOne), "http/{id}"); 42 | } 43 | 44 | function testOnlyOwnerCanMint() public { 45 | // cant mint with msg.sender 46 | vm.expectRevert(bytes("Ownable: caller is not the owner")); 47 | badges.mint(alicePassportId, badgeOne, 3, ""); 48 | assertEq(badges.balanceOf(alice, badgeOne), 1); 49 | assertEq(badges.balanceOf(alicePassportId, badgeOne), 1); 50 | 51 | // owner can mint 52 | vm.prank(owner); 53 | badges.mint(alicePassportId, badgeOne, 3, ""); 54 | assertEq(badges.balanceOf(alice, badgeOne), 4); 55 | assertEq(badges.balanceOf(alicePassportId, badgeOne), 4); 56 | 57 | vm.prank(owner); 58 | badges.mint(alicePassportId, badgeOne, 3, ""); 59 | assertEq(badges.balanceOf(alice, badgeOne), 7); 60 | assertEq(badges.balanceOf(alicePassportId, badgeOne), 7); 61 | 62 | uint256 newTokeId = 50; 63 | assertEq(badges.balanceOf(alice, newTokeId), 0); 64 | vm.prank(owner); 65 | badges.mint(alicePassportId, newTokeId, 2, ""); 66 | assertEq(badges.balanceOf(alice, newTokeId), 2); 67 | assertEq(badges.balanceOf(alicePassportId, newTokeId), 2); 68 | } 69 | 70 | function testOnlyOwnerCanBurn() public { 71 | // the alice can't burn 72 | vm.expectRevert(); 73 | badges.burn(alicePassportId, badgeOne, 1); 74 | assertEq(badges.balanceOf(alice, badgeOne), 1); 75 | assertEq(badges.balanceOf(alicePassportId, badgeOne), 1); 76 | 77 | // the owner can burn 78 | vm.prank(owner); 79 | badges.burn(alicePassportId, badgeOne, 1); 80 | assertEq(badges.balanceOf(alice, badgeOne), 0); 81 | assertEq(badges.balanceOf(alicePassportId, badgeOne), 0); 82 | 83 | vm.expectRevert(bytes("Ownable: caller is not the owner")); 84 | badges.burn(alicePassportId, badgeOne, 10); 85 | assertEq(badges.balanceOf(alice, badgeOne), 0); 86 | 87 | vm.prank(owner); 88 | vm.expectRevert(bytes("ERC1155: burn amount exceeds balance")); 89 | badges.burn(alicePassportId, badgeOne, 10); 90 | assertEq(badges.balanceOf(alice, badgeOne), 0); 91 | } 92 | 93 | function testNobodyCanTransferer() public { 94 | // the token owner can't transfer 95 | vm.expectRevert(Badges.Disabled.selector); 96 | vm.prank(alice); 97 | badges.safeTransferFrom(alice, bob, badgeOne, 1, ""); 98 | assertEq(badges.balanceOf(alice, badgeOne), 1); 99 | assertEq(badges.balanceOf(bob, badgeOne), 0); 100 | 101 | // the contract owner can't transfer 102 | vm.prank(owner); 103 | vm.expectRevert(Badges.Disabled.selector); 104 | badges.safeTransferFrom(alice, bob, badgeOne, 1, ""); 105 | assertEq(badges.balanceOf(alice, badgeOne), 1); 106 | assertEq(badges.balanceOf(bob, badgeOne), 0); 107 | } 108 | 109 | function testSetApprove() public { 110 | // can not approve 111 | vm.expectRevert(Badges.Disabled.selector); 112 | vm.prank(alice); 113 | badges.setApprovalForAll(bob, true); 114 | assertEq(badges.isApprovedForAll(alice, bob), false); 115 | } 116 | 117 | function testGetApprovedForAll() public { 118 | // should get false for all addresses, except for the transferer 119 | assertEq(badges.isApprovedForAll(msg.sender, alice), false); 120 | assertEq(badges.isApprovedForAll(msg.sender, address(this)), false); 121 | assertEq(badges.isApprovedForAll(msg.sender, address(badges)), true); 122 | } 123 | 124 | function testUpdateOwner() public { 125 | uint256 superBadge = 500; 126 | // mint a badge to alice 127 | vm.prank(owner); 128 | badges.mint(alicePassportId, superBadge, 1, ""); 129 | 130 | address newAlice = address(0x7); 131 | assertEq(badges.balanceOf(alice, superBadge), 1); 132 | assertEq(badges.balanceOf(newAlice, superBadge), 0); 133 | assertEq(badges.balanceOf(alicePassportId, superBadge), 1); 134 | 135 | vm.prank(passport_transferer); 136 | passport.safeTransferFrom(alice, newAlice, alicePassportId); 137 | 138 | assertEq(badges.balanceOf(alice, superBadge), 1); 139 | assertEq(badges.balanceOf(newAlice, superBadge), 0); 140 | assertEq(badges.balanceOf(alicePassportId, superBadge), 0); 141 | 142 | uint256[] memory badgeIds = new uint256[](1); 143 | badgeIds[0] = superBadge; 144 | badges.updateOwner(alicePassportId, badgeIds); 145 | 146 | assertEq(badges.balanceOf(alice, superBadge), 0); 147 | assertEq(badges.balanceOf(newAlice, superBadge), 1); 148 | assertEq(badges.balanceOf(alicePassportId, superBadge), 1); 149 | 150 | // test implicit updateOwner 151 | vm.prank(passport_transferer); 152 | passport.safeTransferFrom(newAlice, alice, alicePassportId); 153 | 154 | assertEq(badges.balanceOf(newAlice, superBadge), 1); 155 | assertEq(badges.balanceOf(alice, superBadge), 0); 156 | assertEq(badges.balanceOf(alicePassportId, superBadge), 0); 157 | 158 | vm.prank(owner); 159 | badges.mint(alicePassportId, superBadge, 10, ""); 160 | 161 | assertEq(badges.balanceOf(newAlice, superBadge), 0); 162 | assertEq(badges.balanceOf(alice, superBadge), 11); 163 | assertEq(badges.balanceOf(alicePassportId, superBadge), 11); 164 | 165 | vm.prank(passport_transferer); 166 | passport.safeTransferFrom(alice, newAlice, alicePassportId); 167 | 168 | vm.prank(owner); 169 | badges.burn(alicePassportId, superBadge, 5); 170 | assertEq(badges.balanceOf(newAlice, superBadge), 6); 171 | assertEq(badges.balanceOf(alice, superBadge), 0); 172 | assertEq(badges.balanceOf(alicePassportId, superBadge), 6); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Passport/Passport.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {ERC721} from "../../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 5 | import {AccessControl} from "../../lib/openzeppelin-contracts/contracts/access/AccessControl.sol"; 6 | import {EIP712} from "../../lib/openzeppelin-contracts/contracts/utils/cryptography/draft-EIP712.sol"; 7 | import {ERC721Votes} from "../../lib/openzeppelin-contracts/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; 8 | import {Counters} from "../../lib/openzeppelin-contracts/contracts/utils/Counters.sol"; 9 | 10 | contract Passport is ERC721, AccessControl, EIP712, ERC721Votes { 11 | using Counters for Counters.Counter; 12 | 13 | error Disabled(); 14 | 15 | bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 16 | bytes32 public constant TRANSFERER_ROLE = keccak256("TRANSFERER_ROLE"); 17 | Counters.Counter private _tokenIdCounter; 18 | string private baseURI; 19 | 20 | constructor(address owner, string memory name_, string memory symbol_, string memory baseURI_) 21 | ERC721(name_, symbol_) 22 | EIP712(name_, "1") 23 | { 24 | _grantRole(DEFAULT_ADMIN_ROLE, owner); 25 | _grantRole(MINTER_ROLE, owner); 26 | _grantRole(TRANSFERER_ROLE, owner); 27 | baseURI = baseURI_; 28 | } 29 | 30 | function _baseURI() internal view override returns (string memory) { 31 | return baseURI; 32 | } 33 | 34 | function updateBaseURI(string memory newBaseURI) external onlyRole(DEFAULT_ADMIN_ROLE) { 35 | baseURI = newBaseURI; 36 | } 37 | 38 | function totalSupply() public view returns (uint256) { 39 | return _tokenIdCounter.current(); 40 | } 41 | 42 | function safeMint(address to) external onlyRole(MINTER_ROLE) { 43 | uint256 tokenId = _tokenIdCounter.current(); 44 | _tokenIdCounter.increment(); 45 | _safeMint(to, tokenId); 46 | } 47 | 48 | // Configure the approval functionality to return expected values 49 | function approve( 50 | address, 51 | /* to */ 52 | uint256 /* tokenId */ 53 | ) public pure override { 54 | revert Disabled(); 55 | } 56 | 57 | function setApprovalForAll( 58 | address, 59 | /* operator */ 60 | bool /* approved */ 61 | ) public pure override { 62 | revert Disabled(); 63 | } 64 | 65 | function getApproved(uint256 tokenId) public view override returns (address) { 66 | _requireMinted(tokenId); 67 | 68 | return address(0); 69 | } 70 | 71 | // Make sure the token is only transferable by the TRANSFERRER_ROLE 72 | 73 | function isApprovedForAll( 74 | address, 75 | /* owner */ 76 | address operator /* operator */ 77 | ) public view override returns (bool) { 78 | return hasRole(TRANSFERER_ROLE, operator); 79 | } 80 | 81 | function _isApprovedOrOwner(address spender, uint256 tokenId) internal view override returns (bool) { 82 | _requireMinted(tokenId); 83 | return isApprovedForAll(address(0), spender); 84 | } 85 | 86 | // To make sensible error messages 87 | function transferFrom(address from, address to, uint256 tokenId) public override onlyRole(TRANSFERER_ROLE) { 88 | _transfer(from, to, tokenId); 89 | } 90 | 91 | function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) 92 | public 93 | override 94 | onlyRole(TRANSFERER_ROLE) 95 | { 96 | _safeTransfer(from, to, tokenId, data); 97 | } 98 | 99 | // The following functions are overrides required by Solidity. 100 | 101 | function _afterTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize) 102 | internal 103 | override (ERC721, ERC721Votes) 104 | { 105 | super._afterTokenTransfer(from, to, tokenId, batchSize); 106 | } 107 | 108 | function supportsInterface(bytes4 interfaceId) public view override (ERC721, AccessControl) returns (bool) { 109 | return super.supportsInterface(interfaceId); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Passport/Passport.test.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../../lib/forge-std/src/Test.sol"; 5 | import {Strings} from "../../lib/openzeppelin-contracts/contracts/utils/Strings.sol"; 6 | import {Passport} from "./Passport.sol"; 7 | 8 | contract PassportTest is Test { 9 | Passport passport; 10 | address constant admin = address(0x1); 11 | address constant minter = address(0x2); 12 | address constant transferer = address(0x3); 13 | 14 | address constant alice = address(0x4); 15 | address constant bob = address(0x5); 16 | 17 | function setUp() public { 18 | passport = new Passport(address(this), "Passport", "PASS", ""); 19 | passport.grantRole(passport.DEFAULT_ADMIN_ROLE(), admin); 20 | passport.grantRole(passport.MINTER_ROLE(), minter); 21 | passport.grantRole(passport.TRANSFERER_ROLE(), transferer); 22 | 23 | passport.revokeRole(passport.MINTER_ROLE(), address(this)); 24 | passport.revokeRole(passport.TRANSFERER_ROLE(), address(this)); 25 | passport.revokeRole(passport.DEFAULT_ADMIN_ROLE(), address(this)); 26 | } 27 | 28 | function testSetupParameters() public { 29 | assertEq(passport.name(), "Passport"); 30 | assertEq(passport.symbol(), "PASS"); 31 | } 32 | 33 | function testOnlyMinterCanSafeMint() public { 34 | // cant mint with msg.sender 35 | vm.expectRevert(missingRoleError(passport.MINTER_ROLE(), address(this))); 36 | passport.safeMint(msg.sender); 37 | assertEq(passport.balanceOf(msg.sender), 0); 38 | 39 | // can mint with minter 40 | vm.prank(minter); 41 | passport.safeMint(msg.sender); 42 | assertEq(passport.balanceOf(msg.sender), 1); 43 | } 44 | 45 | function testTotalSupply() public { 46 | assertEq(passport.totalSupply(), 0); 47 | 48 | // can mint with minter 49 | vm.prank(minter); 50 | passport.safeMint(msg.sender); 51 | assertEq(passport.totalSupply(), 1); 52 | } 53 | 54 | function testOnlyTransfererCanTransfer() public { 55 | // mint a Passport to alice 56 | vm.prank(minter); 57 | passport.safeMint(alice); 58 | 59 | // the owner (alice) can't transfer 60 | vm.expectRevert(missingRoleError(passport.TRANSFERER_ROLE(), alice)); 61 | vm.prank(alice); 62 | passport.transferFrom(alice, bob, 0); 63 | assertEq(passport.balanceOf(alice), 1); 64 | assertEq(passport.balanceOf(bob), 0); 65 | 66 | // the transferer can transfer 67 | vm.prank(transferer); 68 | passport.transferFrom(alice, bob, 0); 69 | assertEq(passport.balanceOf(alice), 0); 70 | assertEq(passport.balanceOf(bob), 1); 71 | } 72 | 73 | function testOnlyTransfererCanSafeTransfer() public { 74 | // mint a Passport to the msg.sender 75 | vm.prank(minter); 76 | passport.safeMint(alice); 77 | 78 | // the owner (msg.sender) can't transfer 79 | vm.expectRevert(missingRoleError(passport.TRANSFERER_ROLE(), alice)); 80 | vm.prank(alice); 81 | passport.safeTransferFrom(alice, bob, 0); 82 | assertEq(passport.balanceOf(alice), 1); 83 | assertEq(passport.balanceOf(bob), 0); 84 | 85 | // the transferer can transfer 86 | vm.prank(transferer); 87 | passport.safeTransferFrom(alice, bob, 0); 88 | assertEq(passport.balanceOf(alice), 0); 89 | assertEq(passport.balanceOf(bob), 1); 90 | } 91 | 92 | function testOnlyAdminCanUpdateBaseURI() public { 93 | // mint a Passport to the msg.sender 94 | vm.prank(minter); 95 | passport.safeMint(msg.sender); 96 | 97 | // cant update baseURI with msg.sender 98 | vm.expectRevert(missingRoleError(passport.DEFAULT_ADMIN_ROLE(), address(this))); 99 | passport.updateBaseURI("https://example.com/"); 100 | assertEq(passport.tokenURI(0), ""); 101 | 102 | // can update baseURI with admin 103 | vm.prank(admin); 104 | passport.updateBaseURI("https://example.com/"); 105 | assertEq(passport.tokenURI(0), "https://example.com/0"); 106 | } 107 | 108 | function testSetApprove() public { 109 | // mint a Passport to alice 110 | vm.prank(minter); 111 | passport.safeMint(alice); 112 | 113 | // can not approve 114 | vm.expectRevert(Passport.Disabled.selector); 115 | vm.prank(alice); 116 | passport.approve(bob, 0); 117 | assertEq(passport.getApproved(0), address(0)); 118 | } 119 | 120 | function testSetApprovalForAll() public { 121 | // mint a Passport to alice 122 | vm.prank(minter); 123 | passport.safeMint(alice); 124 | 125 | // can not approve 126 | vm.expectRevert(Passport.Disabled.selector); 127 | vm.prank(alice); 128 | passport.setApprovalForAll(bob, true); 129 | assertEq(passport.getApproved(0), address(0)); 130 | } 131 | 132 | function testGetApproved() public { 133 | // mint a Passport to the msg.sender 134 | vm.prank(minter); 135 | passport.safeMint(msg.sender); 136 | 137 | // should get "empty" approval for minted token 138 | assertEq(passport.getApproved(0), address(0)); 139 | 140 | // should revert for non-excising token 141 | vm.expectRevert(bytes("ERC721: invalid token ID")); 142 | passport.getApproved(1); 143 | } 144 | 145 | function testGetApprovedForAll() public { 146 | // should get false for all addresses, except for the transferer 147 | assertEq(passport.isApprovedForAll(msg.sender, alice), false); 148 | assertEq(passport.isApprovedForAll(msg.sender, address(this)), false); 149 | assertEq(passport.isApprovedForAll(msg.sender, transferer), true); 150 | } 151 | } 152 | 153 | function missingRoleError(bytes32 role, address account) pure returns (bytes memory) { 154 | return bytes( 155 | string( 156 | abi.encodePacked( 157 | "AccessControl: account ", 158 | Strings.toHexString(account), 159 | " is missing role ", 160 | Strings.toHexString(uint256(role), 32) 161 | ) 162 | ) 163 | ); 164 | } 165 | -------------------------------------------------------------------------------- /src/Rep/Rep.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {ERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; 5 | import {ERC20Snapshot} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Snapshot.sol"; 6 | import {AccessControl} from "../../lib/openzeppelin-contracts/contracts/access/AccessControl.sol"; 7 | import {ERC20Permit} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; 8 | import {ERC20Votes} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Votes.sol"; 9 | import {Passport} from "../Passport/Passport.sol"; 10 | 11 | contract Rep is ERC20, ERC20Snapshot, AccessControl, ERC20Permit, ERC20Votes { 12 | error Disabled(); 13 | 14 | bytes32 public constant SNAPSHOT_ROLE = keccak256("SNAPSHOT_ROLE"); 15 | bytes32 public constant MINTER_BURNER_ROLE = keccak256("MINTER_BURNER_ROLE"); 16 | 17 | Passport public immutable passport; 18 | mapping(uint256 => address) private passportToAddress; 19 | 20 | modifier updateAddress(uint256 passportId) { 21 | updateOwner(passportId); 22 | _; 23 | } 24 | 25 | constructor(address owner, address passport_, string memory name_, string memory symbol_) 26 | ERC20(name_, symbol_) 27 | ERC20Permit(name_) 28 | { 29 | passport = Passport(passport_); 30 | // check that the owner is an admin of the passport 31 | require(passport.hasRole(passport.DEFAULT_ADMIN_ROLE(), owner), "Rep: initial owner must be passport admin"); 32 | _grantRole(DEFAULT_ADMIN_ROLE, owner); 33 | _grantRole(SNAPSHOT_ROLE, owner); 34 | _grantRole(MINTER_BURNER_ROLE, owner); 35 | } 36 | 37 | function updateOwner(uint256 passportId) public { 38 | address passportOwner = passport.ownerOf(passportId); // reverts if does not exist 39 | address currentOwner = passportToAddress[passportId]; 40 | if (currentOwner == address(0)) { 41 | // its the first time we see this passport 42 | passportToAddress[passportId] = passportOwner; 43 | } else if (currentOwner != passportOwner) { 44 | // the passport has moved 45 | _transfer(currentOwner, passportOwner, balanceOf(currentOwner)); 46 | passportToAddress[passportId] = passportOwner; 47 | } 48 | } 49 | 50 | function snapshot() public onlyRole(SNAPSHOT_ROLE) { 51 | _snapshot(); 52 | } 53 | 54 | function mint(uint256 toPassportId, uint256 amount) 55 | public 56 | onlyRole(MINTER_BURNER_ROLE) 57 | updateAddress(toPassportId) 58 | { 59 | _mint(passport.ownerOf(toPassportId), amount); 60 | } 61 | 62 | function balanceOf(uint256 passportId) public view returns (uint256) { 63 | return balanceOf(passport.ownerOf(passportId)); 64 | } 65 | 66 | function transfer( 67 | address, 68 | /* to */ 69 | uint256 /* amount */ 70 | ) public pure override returns (bool) { 71 | revert Disabled(); 72 | } 73 | 74 | function transferFrom( 75 | address, 76 | /* from */ 77 | address, 78 | /* to */ 79 | uint256 /* amount */ 80 | ) public virtual override returns (bool) { 81 | revert Disabled(); 82 | } 83 | 84 | function increaseAllowance( 85 | address, 86 | /* spender */ 87 | uint256 /* addedValue */ 88 | ) public pure override returns (bool) { 89 | revert Disabled(); 90 | } 91 | 92 | function decreaseAllowance( 93 | address, 94 | /* spender */ 95 | uint256 /* subtractedValue */ 96 | ) public pure override returns (bool) { 97 | revert Disabled(); 98 | } 99 | 100 | function allowance( 101 | address, 102 | /* owner */ 103 | address /* spender */ 104 | ) public pure override returns (uint256) { 105 | return 0; 106 | } 107 | 108 | function approve( 109 | address, 110 | /* spender */ 111 | uint256 /* amount */ 112 | ) public pure override returns (bool) { 113 | revert Disabled(); 114 | } 115 | 116 | function burnFrom(uint256 passportId, uint256 amount) 117 | public 118 | onlyRole(MINTER_BURNER_ROLE) 119 | updateAddress(passportId) 120 | { 121 | _burn(passport.ownerOf(passportId), amount); 122 | } 123 | 124 | // The following functions are overrides required by Solidity. 125 | 126 | function _beforeTokenTransfer(address from, address to, uint256 amount) internal override (ERC20, ERC20Snapshot) { 127 | super._beforeTokenTransfer(from, to, amount); 128 | } 129 | 130 | function _afterTokenTransfer(address from, address to, uint256 amount) internal override (ERC20, ERC20Votes) { 131 | super._afterTokenTransfer(from, to, amount); 132 | } 133 | 134 | function _mint(address to, uint256 amount) internal override (ERC20, ERC20Votes) { 135 | super._mint(to, amount); 136 | } 137 | 138 | function _burn(address account, uint256 amount) 139 | internal 140 | override (ERC20, ERC20Votes) 141 | onlyRole(MINTER_BURNER_ROLE) 142 | { 143 | super._burn(account, amount); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Rep/Rep.test.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../../lib/forge-std/src/Test.sol"; 5 | import {Strings} from "../../lib/openzeppelin-contracts/contracts/utils/Strings.sol"; 6 | import {Rep} from "./Rep.sol"; 7 | import {Passport} from "../Passport/Passport.sol"; 8 | 9 | contract RepTest is Test { 10 | Rep rep; 11 | address constant admin = address(0x1); 12 | address constant minter_burner = address(0x2); 13 | address constant snapshoter = address(0x3); 14 | 15 | Passport passport; 16 | address constant passport_transferer = address(0x6); 17 | 18 | address constant alice = address(0x4); 19 | address constant bob = address(0x5); 20 | 21 | uint256 constant alicePassportId = 0; 22 | uint256 constant bobPassportID = 1; 23 | 24 | function setUp() public { 25 | passport = new Passport(address(this), "Passport", "PASS", ""); 26 | passport.grantRole(passport.TRANSFERER_ROLE(), passport_transferer); 27 | 28 | rep = new Rep(address(this), address(passport), "Reputation", "REP"); 29 | rep.grantRole(rep.DEFAULT_ADMIN_ROLE(), admin); 30 | rep.grantRole(rep.MINTER_BURNER_ROLE(), minter_burner); 31 | rep.grantRole(rep.SNAPSHOT_ROLE(), snapshoter); 32 | 33 | rep.revokeRole(rep.MINTER_BURNER_ROLE(), address(this)); 34 | rep.revokeRole(rep.SNAPSHOT_ROLE(), address(this)); 35 | rep.revokeRole(rep.DEFAULT_ADMIN_ROLE(), address(this)); 36 | 37 | // by default the passport deployer gets the minter role 38 | passport.safeMint(alice); // gets a passport id 0 39 | passport.safeMint(bob); // gets a passport id 1 40 | } 41 | 42 | function testSetupParameters() public { 43 | assertEq(passport.ownerOf(0), alice); 44 | assertEq(passport.ownerOf(1), bob); 45 | assertEq(rep.name(), "Reputation"); 46 | assertEq(rep.symbol(), "REP"); 47 | } 48 | 49 | function testUpdateOwner() public { 50 | // mint a rep to alice 51 | vm.prank(minter_burner); 52 | rep.mint(alicePassportId, 100); 53 | 54 | address newAlice = address(0x7); 55 | assertEq(rep.balanceOf(alice), 100); 56 | assertEq(rep.balanceOf(newAlice), 0); 57 | assertEq(rep.balanceOf(alicePassportId), 100); 58 | 59 | vm.prank(passport_transferer); 60 | passport.safeTransferFrom(alice, newAlice, alicePassportId); 61 | 62 | assertEq(rep.balanceOf(alice), 100); 63 | assertEq(rep.balanceOf(newAlice), 0); 64 | assertEq(rep.balanceOf(alicePassportId), 0); 65 | 66 | rep.updateOwner(alicePassportId); 67 | 68 | assertEq(rep.balanceOf(alice), 0); 69 | assertEq(rep.balanceOf(newAlice), 100); 70 | assertEq(rep.balanceOf(alicePassportId), 100); 71 | 72 | // test implicit updateOwner 73 | vm.prank(passport_transferer); 74 | passport.safeTransferFrom(newAlice, alice, alicePassportId); 75 | 76 | assertEq(rep.balanceOf(newAlice), 100); 77 | assertEq(rep.balanceOf(alice), 0); 78 | assertEq(rep.balanceOf(alicePassportId), 0); 79 | 80 | vm.prank(minter_burner); 81 | rep.mint(alicePassportId, 100); 82 | 83 | assertEq(rep.balanceOf(newAlice), 0); 84 | assertEq(rep.balanceOf(alice), 200); 85 | assertEq(rep.balanceOf(alicePassportId), 200); 86 | } 87 | 88 | function testOnlyMinterBurnerCanMint() public { 89 | // cant mint with msg.sender 90 | vm.expectRevert(missingRoleError(rep.MINTER_BURNER_ROLE(), address(this))); 91 | rep.mint(alicePassportId, 100); 92 | assertEq(rep.balanceOf(alice), 0); 93 | 94 | // can mint with minter 95 | vm.prank(minter_burner); 96 | rep.mint(alicePassportId, 100); 97 | assertEq(rep.balanceOf(alice), 100); 98 | } 99 | 100 | function testNobodyCanTransferer() public { 101 | // mint a rep to alice 102 | vm.prank(minter_burner); 103 | rep.mint(alicePassportId, 100); 104 | 105 | // the owner (msg.sender) can't transfer 106 | vm.expectRevert(Rep.Disabled.selector); 107 | vm.prank(alice); 108 | rep.transfer(bob, 10); 109 | assertEq(rep.balanceOf(alice), 100); 110 | assertEq(rep.balanceOf(bob), 0); 111 | 112 | // non of the other roles can transfer 113 | vm.prank(admin); 114 | vm.expectRevert(Rep.Disabled.selector); 115 | rep.transferFrom(alice, bob, 10); 116 | 117 | vm.prank(snapshoter); 118 | vm.expectRevert(Rep.Disabled.selector); 119 | rep.transferFrom(alice, bob, 10); 120 | 121 | vm.prank(minter_burner); 122 | vm.expectRevert(Rep.Disabled.selector); 123 | rep.transferFrom(alice, bob, 10); 124 | 125 | assertEq(rep.balanceOf(alice), 100); 126 | assertEq(rep.balanceOf(bob), 0); 127 | } 128 | 129 | function testOnlyMinterBurnerCanBurn() public { 130 | // mint a rep to the msg.sender 131 | vm.prank(minter_burner); 132 | rep.mint(alicePassportId, 200); 133 | 134 | // the owner (msg.sender) can't burn 135 | vm.expectRevert(missingRoleError(rep.MINTER_BURNER_ROLE(), address(this))); 136 | rep.burnFrom(alicePassportId, 100); 137 | assertEq(rep.balanceOf(alice), 200); 138 | 139 | // the minter_burner can burn 140 | vm.prank(minter_burner); 141 | rep.burnFrom(alicePassportId, 50); 142 | assertEq(rep.balanceOf(alice), 150); 143 | } 144 | 145 | function testSetApprove() public { 146 | // mint a rep to the msg.sender 147 | vm.prank(minter_burner); 148 | rep.mint(alicePassportId, 100); 149 | 150 | // can not approve 151 | vm.expectRevert(Rep.Disabled.selector); 152 | rep.approve(alice, 10); 153 | assertEq(rep.allowance(msg.sender, alice), 0); 154 | } 155 | 156 | function testGetAllowance() public { 157 | // mint a rep to the msg.sender 158 | vm.prank(minter_burner); 159 | rep.mint(alicePassportId, 100); 160 | 161 | // should get false for all addresses, except for the transferer 162 | assertEq(rep.allowance(msg.sender, alice), 0); 163 | assertEq(rep.allowance(msg.sender, address(this)), 0); 164 | } 165 | 166 | // we must detect when a passport has moved and update the rep accordingly when a function is called 167 | function testBurnWhenPassportMove() public { 168 | // mint a rep to the msg.sender 169 | vm.prank(minter_burner); 170 | rep.mint(alicePassportId, 100); 171 | 172 | address aliceNewAddress = address(0x44); 173 | 174 | vm.prank(passport_transferer); 175 | passport.safeTransferFrom(alice, aliceNewAddress, alicePassportId); 176 | assertEq(passport.ownerOf(alicePassportId), aliceNewAddress); 177 | 178 | // should still be able to burn Alice's rep (even though the passport has moved) 179 | vm.prank(minter_burner); 180 | rep.burnFrom(alicePassportId, 70); 181 | assertEq(rep.balanceOf(alicePassportId), 30); 182 | } 183 | } 184 | 185 | function missingRoleError(bytes32 role, address account) pure returns (bytes memory) { 186 | return bytes( 187 | string( 188 | abi.encodePacked( 189 | "AccessControl: account ", 190 | Strings.toHexString(account), 191 | " is missing role ", 192 | Strings.toHexString(uint256(role), 32) 193 | ) 194 | ) 195 | ); 196 | } 197 | --------------------------------------------------------------------------------