diff --git a/src/DevouchAttester.sol b/src/DevouchAttester.sol index 1d35015..49e4e73 100644 --- a/src/DevouchAttester.sol +++ b/src/DevouchAttester.sol @@ -1,79 +1,105 @@ - - // SPDX-License-Identifier: MIT -pragma solidity >=0.7.0 <0.9.0; +pragma solidity ^0.8.19; -import { IEAS, AttestationRequest, AttestationRequestData, RevocationRequest, RevocationRequestData } from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol"; -import { NO_EXPIRATION_TIME, EMPTY_UID } from "@ethereum-attestation-service/eas-contracts/contracts/Common.sol"; +import { + IEAS, + AttestationRequestData, + MultiAttestationRequest +} from "eas-contracts/contracts/IEAS.sol"; +import { NO_EXPIRATION_TIME } from "eas-contracts/contracts/Common.sol"; -contract DevouchAttester -{ - event Log(string func, uint256 gas); +contract DeVouchAttester { address public owner; uint256 public fee = 0.00003 ether; - address multisig = 0x7D52A0Ab02A6A49a1B4b7c4e79C80F977971f700; - bytes32 schema = 0x421da38e6ff5eb5d0402a4e9be70e70f961bce228e8a20d1eca19634556247fd; + address public constant multisig = 0x7D52A0Ab02A6A49a1B4b7c4e79C80F977971f700; + bytes32 public schema = 0x421da38e6ff5eb5d0402a4e9be70e70f961bce228e8a20d1eca19634556247fd; + + IEAS public eas; + bool public paused; + + error Unauthorized(); + error InvalidFee(); + error InvalidSchema(); + error ContractPaused(); + error WithdrawFailed(); - /** - * @dev Set contract deployer as owner - */ - constructor() { + /** + * @dev Set contract deployer as owner + */ + constructor(address _easAddress) { owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor + eas = IEAS(_easAddress); } modifier ownerOnly() { - require(msg.sender == owner, "Only owner can access this function"); - _; + if (msg.sender != owner) revert Unauthorized(); + _; + } + + modifier whenNotPaused() { + if (paused) revert ContractPaused(); + _; } function updateOwner(address newOwner) public ownerOnly { owner = newOwner; - emit Log("updateOwner", gasleft()); } function updateFee(uint256 newFee) public ownerOnly { fee = newFee; - emit Log("updateFee", gasleft()); } - function updateSchema(bytes32 newSchema) public ownerOnly{ + function updateSchema(bytes32 newSchema) public ownerOnly { + if (newSchema == 0) revert InvalidSchema(); schema = newSchema; - emit Log("updateSchema", gasleft()); } - function attest(address recipient, bytes32 refUID, bytes calldata data) public payable - { - require(address(msg.sender).balance >= fee, "Insufficient balance to pay fee"); - require(msg.value == fee, "Must pay the fee amount"); - - IEAS(0xC2679fBD37d54388Ce493F1DB75320D236e1815e).attest( - AttestationRequest({ - schema: schema, - data: AttestationRequestData({ - recipient: recipient, - expirationTime: NO_EXPIRATION_TIME, - revocable: true, - refUID: refUID, - data: data, - value:0 - }) - }) - ); - - emit Log("attested", gasleft()); + function updateEAS(address newEASAddress) public ownerOnly { + eas = IEAS(newEASAddress); } - receive() external payable {} + function togglePause() public ownerOnly { + paused = !paused; + } - fallback() external payable { - emit Log("fallback", gasleft()); + function attest( + address recipient, + bytes32[] calldata refUIDArray, + bytes calldata data + ) public payable whenNotPaused { + if (msg.value != fee) revert InvalidFee(); + + uint256 len = refUIDArray.length; + AttestationRequestData[] memory requestDataArray = new AttestationRequestData[](len); + + for (uint256 i = 0; i < len; ++i) { + requestDataArray[i] = AttestationRequestData({ + recipient: recipient, + expirationTime: NO_EXPIRATION_TIME, + revocable: true, + refUID: refUIDArray[i], + data: data, + value: 0 + }); + } + + MultiAttestationRequest[] memory multiRequests = new MultiAttestationRequest[](1); + multiRequests[0] = MultiAttestationRequest({ + schema: schema, + data: requestDataArray + }); + + eas.multiAttest(multiRequests); } + receive() external payable {} + + fallback() external payable {} + // Function to withdraw Ether from the contract (for testing purposes) - function withdraw() public { - // Transfer the Ether to the multisig address - (bool success, ) = multisig.call{value: address(this).balance}(""); - require(success, "Failed to send Ether"); - emit Log("withdraw", gasleft()); + function withdraw() public ownerOnly { + uint256 amount = address(this).balance; + (bool success, ) = multisig.call{value: amount}(""); + if (!success) revert WithdrawFailed(); } -} +} \ No newline at end of file diff --git a/test/DeVouchAttester.t.sol b/test/DeVouchAttester.t.sol new file mode 100644 index 0000000..f5e84ef --- /dev/null +++ b/test/DeVouchAttester.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test, console} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {SchemaRegistry, ISchemaRegistry} from "eas-contracts/contracts/SchemaRegistry.sol"; +import {EAS, NO_EXPIRATION_TIME, EMPTY_UID} from "eas-contracts/contracts/EAS.sol"; +import {IEAS, AttestationRequestData, AttestationRequest, MultiAttestationRequest} from "eas-contracts/contracts/IEAS.sol"; +import {ISchemaResolver} from "eas-contracts/contracts/resolver/ISchemaResolver.sol"; +import {DeVouchAttester} from "../src/DeVouchAttester.sol"; + +contract DevouchAttesterTest is Test { + SchemaRegistry schemaRegistry; + string schema = "string projectSource, string projectId, bool vouch, string comment"; + EAS easContract; + IEAS easInterface; + DeVouchAttester devouchAttester; + bytes32 schemaUID; + + address owner; + address user = address(2); + address multisig; + + event Attested(bytes32 indexed uid, bytes32 indexed schema, address indexed recipient); + + function setUp() public { + owner = address(this); + + schemaRegistry = new SchemaRegistry(); + easContract = new EAS(ISchemaRegistry(address(schemaRegistry))); + + devouchAttester = new DeVouchAttester(address(easContract)); + + schemaUID = schemaRegistry.register(schema, ISchemaResolver(address(0)), true); + + devouchAttester.updateSchema(schemaUID); + + multisig = devouchAttester.multisig(); + + vm.label(address(easContract), "EAS"); + vm.label(address(schemaRegistry), "SchemaRegistry"); + vm.label(address(devouchAttester), "DeVouchAttester"); + vm.label(multisig, "Multisig"); + } + + function testMultiAttest() public { + address recipient = address(0); + bytes32[] memory refUIDArray = new bytes32[](2); + refUIDArray[0] = EMPTY_UID; + refUIDArray[1] = EMPTY_UID; + bytes memory data = abi.encode("giveth", "55", true, "this is awesome"); + + uint256 fee = devouchAttester.fee(); + + vm.deal(user, 1 ether); + + uint256 userInitialBalance = user.balance; + uint256 contractInitialBalance = address(devouchAttester).balance; + uint256 multisigInitialBalance = multisig.balance; + + console.log("\n--------------------"); + console.log("Starting Multi-Attest Test"); + console.log("--------------------"); + console.log("Attestation fee:", fee); + console.log("User initial balance:", userInitialBalance); + console.log("Contract initial balance:", contractInitialBalance); + console.log("Multisig initial balance:", multisigInitialBalance); + + vm.prank(user); + vm.recordLogs(); + devouchAttester.attest{value: fee}(recipient, refUIDArray, data); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 userFinalBalance = user.balance; + uint256 contractFinalBalance = address(devouchAttester).balance; + uint256 multisigFinalBalance = multisig.balance; + + console.log("\n--------------------"); + console.log("Attestation Complete"); + console.log("--------------------"); + console.log("Number of emitted events:", entries.length); + console.log("User final balance:", userFinalBalance); + console.log("Contract final balance:", contractFinalBalance); + console.log("Multisig final balance:", multisigFinalBalance); + + console.log("\n--------------------"); + console.log("Assertions"); + console.log("--------------------"); + + assertEq(entries.length, 2, "Should emit two events for multi-attest"); + console.log(" >> Correct number of events emitted"); + + assertEq(userFinalBalance, userInitialBalance - fee, "User balance should decrease by fee amount"); + console.log(" >> User balance decreased correctly"); + + if (contractFinalBalance > contractInitialBalance) { + assertEq(contractFinalBalance, contractInitialBalance + fee, "Contract balance should increase by fee amount"); + console.log(" >> Fee is held in the contract"); + } else if (multisigFinalBalance > multisigInitialBalance) { + assertEq(multisigFinalBalance, multisigInitialBalance + fee, "Multisig balance should increase by fee amount"); + console.log(" >> Fee was sent to the multisig"); + } else { + fail(); + console.log(" >> Fee was not properly accounted for"); + } + + assertEq(devouchAttester.schema(), schemaUID, "Schema in DevouchAttester should match the set schema"); + console.log(" >> Schema in DevouchAttester is correct"); + + assertEq(address(devouchAttester.eas()), address(easContract), "EAS address in DevouchAttester should be correct"); + console.log(" >> EAS address in DevouchAttester is correct"); + + console.log("\n--------------------"); + console.log("Test Complete"); + console.log("--------------------"); + } +} \ No newline at end of file