Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Otterspace badges voting strategy #242

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable
branch = v4.8.0
[submodule "lib/otterspace-contracts"]
path = lib/otterspace-contracts
url = https://github.com/otterspace-xyz/otterspace-contracts
[submodule "lib/ERC4973"]
path = lib/ERC4973
url = https://github.com/otterspace-xyz/ERC4973
1 change: 1 addition & 0 deletions lib/ERC4973
Submodule ERC4973 added at 717075
1 change: 1 addition & 0 deletions lib/otterspace-contracts
Submodule otterspace-contracts added at aec3d4
2 changes: 2 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ forge-gas-snapshot/=lib/forge-gas-snapshot/src
@prb/test/=lib/prb-test/src/
@zodiac/=lib/zodiac/contracts/
@gnosis.pm/safe-contracts=lib/safe-contracts
@otterspace/contracts/=lib/otterspace-contracts/src
ERC4973/=lib/ERC4973/src/
60 changes: 60 additions & 0 deletions src/voting-strategies/OtterspaceBadgesVotingStrategy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;

// import { Badges } from "@otterspace/contracts/Badges.sol";
import { IVotingStrategy } from "../interfaces/IVotingStrategy.sol";

interface IBadges {
function isBadgeValid(uint256 _badgeId) external view returns (bool);
}

/// @title Otterspace Badge Voting Strategy
/// @notice Allows Otterspace Badges to be used for voting power.
contract OtterspaceBadgesVotingStrategy is IVotingStrategy {
error InvalidBadge();

/// @notice The address of the Otterspace Badges Contract.
address public badgesRegistry;

/// @dev Data stored as parameters for each Badge.
struct Badge {
// spec of the badge.
string specUri;
// The voting power granted to badges of this spec.
uint96 vp;
}

constructor(address BadgesRegistry) {

Check warning on line 28 in src/voting-strategies/OtterspaceBadgesVotingStrategy.sol

View workflow job for this annotation

GitHub Actions / lint

Variable name must be in mixedCase
badgesRegistry = BadgesRegistry;
}

/// @notice Returns the voting power of an address.
/// @param voter The address to get the voting power of.
/// @param params Parameter array containing an array of Badge structs.
/// @param userParams Parameter array containing an array of indices of the badges the voter owns.
/// @return votingPower The voting power of the address if it exists in the whitelist, otherwise reverts.
function getVotingPower(
uint32 /* blockNumber */,
address voter,
bytes calldata params,
bytes calldata userParams
) external view override returns (uint256 votingPower) {
Badge[] memory badges = abi.decode(params, (Badge[]));
uint8[] memory userBadgeIndices = abi.decode(userParams, (uint8[]));

uint256 vp;
for (uint8 i = 0; i < userBadgeIndices.length; i++) {
uint256 tokenId = uint256(getBadgeIdHash(voter, badges[userBadgeIndices[i]].specUri));
if (!IBadges(badgesRegistry).isBadgeValid(tokenId)) revert InvalidBadge();
vp += badges[userBadgeIndices[i]].vp;
}

return vp;
}

/// @dev Generates the unique ID of a badge for a given address and spec.
function getBadgeIdHash(address _to, string memory _uri) internal pure returns (bytes32) {
return keccak256(abi.encode(_to, _uri));
}
}
117 changes: 117 additions & 0 deletions test/OtterspaceBadgesVotingStrategy.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import { Test } from "forge-std/Test.sol";
import { OtterspaceBadgesVotingStrategy } from "../src/voting-strategies/OtterspaceBadgesVotingStrategy.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import { Badges } from "@otterspace/contracts/Badges.sol";
import { SpecDataHolder } from "@otterspace/contracts/SpecDataHolder.sol";
import { Raft } from "@otterspace/contracts/Raft.sol";

contract UUPSProxy is ERC1967Proxy {
constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) {}
}

contract OtterspaceBadgesVotingStrategyTest is Test {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

Badges badgesImplementationV1;
SpecDataHolder specDataHolderImplementationV1;
Raft raftImplementationV1;

UUPSProxy badgesUUPS;
UUPSProxy raftUUPS;
UUPSProxy sdhUUPS;

Badges badgesProxy;
Raft raftProxy;
SpecDataHolder sdhProxy;

uint256 passivePrivateKey = 0xad54bdeade5537fb0a553190159783e45d02d316a992db05cbed606d3ca36b39;

uint256 raftHolderPrivateKey = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;
address passiveAddress = vm.addr(passivePrivateKey);

uint256 claimantPrivateKey = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a;

uint256 randomPrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;

address randomAddress = vm.addr(randomPrivateKey);

address raftOwner = vm.addr(raftHolderPrivateKey);

address claimantAddress = vm.addr(claimantPrivateKey);
address zeroAddress = address(0);

string[] specUris = ["spec1", "spec2"];
string badTokenUri = "bad token uri";

string errAirdropUnauthorized = "airdrop: unauthorized";
string err721InvalidTokenId = "ERC721: invalid token ID";
string errBadgeAlreadyRevoked = "revokeBadge: badge already revoked";
string errBalanceOfNotValidOwner = "balanceOf: address(0) is not a valid owner";
string errGiveToManyArrayMismatch = "giveToMany: recipients and signatures length mismatch";
string errGiveRequestedBadgeToManyArrayMismatch =
"giveRequestedBadgeToMany: recipients and signatures length mismatch";
string errInvalidSig = "safeCheckAgreement: invalid signature";
string errGiveRequestedBadgeInvalidSig = "giveRequestedBadge: invalid signature";
string errOnlyBadgesContract = "onlyBadgesContract: unauthorized";
string errNoSpecUris = "refreshMetadata: no spec uris provided";
string errNotOwner = "Ownable: caller is not the owner";
string errNotRaftOwner = "onlyRaftOwner: unauthorized";
string errCreateSpecUnauthorized = "createSpec: unauthorized";
string errNotRevoked = "reinstateBadge: badge not revoked";
string errSafeCheckUsed = "safeCheckAgreement: already used";
string errSpecAlreadyRegistered = "createSpec: spec already registered";
string errSpecNotRegistered = "mint: spec is not registered";
string errGiveUnauthorized = "give: unauthorized";
string errUnequipSenderNotOwner = "unequip: sender must be owner";
string errTakeUnauthorized = "take: unauthorized";
string errMerkleInvalidLeaf = "safeCheckMerkleAgreement: invalid leaf";
string errMerkleInvalidSignature = "safeCheckMerkleAgreement: invalid signature";
string errTokenDoesntExist = "tokenExists: token doesn't exist";
string errTokenExists = "mint: tokenID exists";
string errRevokeUnauthorized = "revokeBadge: unauthorized";
string errReinstateUnauthorized = "reinstateBadge: unauthorized";
string errRequestedBadgeUnauthorized = "giveRequestedBadge: unauthorized";

string specUri = "some spec uri";
uint256 raftTokenId;

function setUp() public {
address contractOwner = address(this);

badgesImplementationV1 = new Badges();
specDataHolderImplementationV1 = new SpecDataHolder();
raftImplementationV1 = new Raft();

badgesUUPS = new UUPSProxy(address(badgesImplementationV1), "");
raftUUPS = new UUPSProxy(address(raftImplementationV1), "");
sdhUUPS = new UUPSProxy(address(specDataHolderImplementationV1), "");

badgesProxy = Badges(address(badgesUUPS));
raftProxy = Raft(address(raftUUPS));
sdhProxy = SpecDataHolder(address(sdhUUPS));

badgesProxy.initialize("Badges", "BADGES", "0.1.0", contractOwner, address(sdhUUPS));
raftProxy.initialize(contractOwner, "Raft", "RAFT");
sdhProxy.initialize(address(raftUUPS), contractOwner);

sdhProxy.setBadgesAddress(address(badgesUUPS));

vm.label(passiveAddress, "passive");
vm.expectEmit(true, true, true, false);
emit Transfer(zeroAddress, raftOwner, 1);
raftTokenId = raftProxy.mint(raftOwner, specUri);

assertEq(raftTokenId, 1);
assertEq(raftProxy.balanceOf(raftOwner), 1);

vm.prank(raftOwner);
badgesProxy.createSpec(specUri, raftTokenId);
assertEq(sdhProxy.isSpecRegistered(specUri), true);
}

function testOtterspaceBadgesVotingPower() public {}
}
Loading