From 61bb342381f772ae43c377a5bdb04ae4ea314cb9 Mon Sep 17 00:00:00 2001 From: Kilter Date: Thu, 17 Oct 2024 11:19:37 -0500 Subject: [PATCH 1/4] DevouchAttester v1 --- src/DevouchAttester.sol | 126 ++++++++++++++++++++++--------------- test/DeVouchAttester.t.sol | 79 +++++++++++++++++++++++ 2 files changed, 153 insertions(+), 52 deletions(-) create mode 100644 test/DeVouchAttester.t.sol diff --git a/src/DevouchAttester.sol b/src/DevouchAttester.sol index 1d35015..074501b 100644 --- a/src/DevouchAttester.sol +++ b/src/DevouchAttester.sol @@ -1,79 +1,101 @@ - - // 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; - - /** - * @dev Set contract deployer as owner - */ - constructor() { - owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor + 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(); + + constructor(address _easAddress) { + owner = msg.sender; + 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; + } + + 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 + }); + } - fallback() external payable { - emit Log("fallback", gasleft()); + MultiAttestationRequest[] memory multiRequests = new MultiAttestationRequest[](1); + multiRequests[0] = MultiAttestationRequest({ + schema: schema, + data: requestDataArray + }); + + eas.multiAttest(multiRequests); } - // 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()); + receive() external payable {} + + fallback() external payable {} + + 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..40246f5 --- /dev/null +++ b/test/DeVouchAttester.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +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); + + 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); + + vm.label(address(easContract), "EAS"); + vm.label(address(schemaRegistry), "SchemaRegistry"); + vm.label(address(devouchAttester), "DeVouchAttester"); + } + + 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 initialBalance = address(devouchAttester).balance; + uint256 fee = devouchAttester.fee(); + + vm.deal(user, 1 ether); + vm.prank(user); + + console.log("Before attestation"); + + vm.recordLogs(); + + devouchAttester.attest{value: fee}(recipient, refUIDArray, data); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + console.log("After attestation"); + console.log("Number of emitted events:", entries.length); + + for (uint i = 0; i < entries.length; i++) { + console.log("Event", i); + console.logBytes32(entries[i].topics[0]); // Event signature + if (entries[i].topics.length > 1) console.logBytes32(entries[i].topics[1]); // First indexed parameter + if (entries[i].topics.length > 2) console.logBytes32(entries[i].topics[2]); // Second indexed parameter + if (entries[i].topics.length > 3) console.logAddress(address(uint160(uint256(entries[i].topics[3])))); // Third indexed parameter (if it's an address) + console.logBytes(entries[i].data); // Non-indexed parameters + } + + assertEq(address(devouchAttester).balance, initialBalance + fee); + assertEq(devouchAttester.schema(), schemaUID); + assertEq(address(devouchAttester.eas()), address(easContract)); + } +} \ No newline at end of file From 8e1fe18f6de9a6ebc1069fc7600a34d8ef0bc11b Mon Sep 17 00:00:00 2001 From: Kilter Date: Thu, 17 Oct 2024 11:44:59 -0500 Subject: [PATCH 2/4] testing fee recollection --- test/DeVouchAttester.t.sol | 75 +++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/test/DeVouchAttester.t.sol b/test/DeVouchAttester.t.sol index 40246f5..f5e84ef 100644 --- a/test/DeVouchAttester.t.sol +++ b/test/DeVouchAttester.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.26; import {Test, console} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; @@ -19,6 +19,7 @@ contract DevouchAttesterTest is Test { address owner; address user = address(2); + address multisig; event Attested(bytes32 indexed uid, bytes32 indexed schema, address indexed recipient); @@ -34,9 +35,12 @@ contract DevouchAttesterTest is Test { 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 { @@ -46,34 +50,69 @@ contract DevouchAttesterTest is Test { refUIDArray[1] = EMPTY_UID; bytes memory data = abi.encode("giveth", "55", true, "this is awesome"); - uint256 initialBalance = address(devouchAttester).balance; uint256 fee = devouchAttester.fee(); - + vm.deal(user, 1 ether); - vm.prank(user); - console.log("Before attestation"); + uint256 userInitialBalance = user.balance; + uint256 contractInitialBalance = address(devouchAttester).balance; + uint256 multisigInitialBalance = multisig.balance; - vm.recordLogs(); + 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(); - console.log("After attestation"); - console.log("Number of emitted events:", entries.length); + uint256 userFinalBalance = user.balance; + uint256 contractFinalBalance = address(devouchAttester).balance; + uint256 multisigFinalBalance = multisig.balance; - for (uint i = 0; i < entries.length; i++) { - console.log("Event", i); - console.logBytes32(entries[i].topics[0]); // Event signature - if (entries[i].topics.length > 1) console.logBytes32(entries[i].topics[1]); // First indexed parameter - if (entries[i].topics.length > 2) console.logBytes32(entries[i].topics[2]); // Second indexed parameter - if (entries[i].topics.length > 3) console.logAddress(address(uint160(uint256(entries[i].topics[3])))); // Third indexed parameter (if it's an address) - console.logBytes(entries[i].data); // Non-indexed parameters + 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(address(devouchAttester).balance, initialBalance + fee); - assertEq(devouchAttester.schema(), schemaUID); - assertEq(address(devouchAttester.eas()), address(easContract)); + 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 From d8ea32620e546a3df3fd40705bf57757beaef208 Mon Sep 17 00:00:00 2001 From: Kilter Date: Thu, 17 Oct 2024 12:00:34 -0500 Subject: [PATCH 3/4] Update DevouchAttester.sol --- src/DevouchAttester.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/DevouchAttester.sol b/src/DevouchAttester.sol index 074501b..49e4e73 100644 --- a/src/DevouchAttester.sol +++ b/src/DevouchAttester.sol @@ -23,8 +23,11 @@ contract DeVouchAttester { error ContractPaused(); error WithdrawFailed(); + /** + * @dev Set contract deployer as owner + */ constructor(address _easAddress) { - owner = msg.sender; + owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor eas = IEAS(_easAddress); } @@ -93,6 +96,7 @@ contract DeVouchAttester { fallback() external payable {} + // Function to withdraw Ether from the contract (for testing purposes) function withdraw() public ownerOnly { uint256 amount = address(this).balance; (bool success, ) = multisig.call{value: amount}(""); From e3dbc7f94550a86105cf914fb973a1bd747bc65c Mon Sep 17 00:00:00 2001 From: Kilter Date: Fri, 25 Oct 2024 09:02:17 -0500 Subject: [PATCH 4/4] Update DevouchAttester.sol --- src/DevouchAttester.sol | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/DevouchAttester.sol b/src/DevouchAttester.sol index 49e4e73..181c897 100644 --- a/src/DevouchAttester.sol +++ b/src/DevouchAttester.sol @@ -11,7 +11,7 @@ import { NO_EXPIRATION_TIME } from "eas-contracts/contracts/Common.sol"; contract DeVouchAttester { address public owner; uint256 public fee = 0.00003 ether; - address public constant multisig = 0x7D52A0Ab02A6A49a1B4b7c4e79C80F977971f700; + address public multisig; bytes32 public schema = 0x421da38e6ff5eb5d0402a4e9be70e70f961bce228e8a20d1eca19634556247fd; IEAS public eas; @@ -22,12 +22,14 @@ contract DeVouchAttester { error InvalidSchema(); error ContractPaused(); error WithdrawFailed(); + error InvalidMultisig(); /** - * @dev Set contract deployer as owner + * @dev Set contract deployer as owner and initial multisig */ constructor(address _easAddress) { - owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor + owner = msg.sender; + multisig = 0x7D52A0Ab02A6A49a1B4b7c4e79C80F977971f700; eas = IEAS(_easAddress); } @@ -45,6 +47,11 @@ contract DeVouchAttester { owner = newOwner; } + function updateMultisig(address newMultisig) public ownerOnly { + if (newMultisig == address(0)) revert InvalidMultisig(); + multisig = newMultisig; + } + function updateFee(uint256 newFee) public ownerOnly { fee = newFee; } @@ -96,8 +103,7 @@ contract DeVouchAttester { fallback() external payable {} - // Function to withdraw Ether from the contract (for testing purposes) - function withdraw() public ownerOnly { + function withdraw() public { uint256 amount = address(this).balance; (bool success, ) = multisig.call{value: amount}(""); if (!success) revert WithdrawFailed();