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: implement arbitrary transactions via reputation #3933

Merged
merged 3 commits into from
Jan 13, 2025

Conversation

Nortsova
Copy link
Contributor

@Nortsova Nortsova commented Dec 12, 2024

Description

This issue covers the reputation (motions) flow of creating arbitrary transaction.

New saga needs to be added that should accept a target contract address and a list of arbitrary transactions, consisting of function name and a list of arguments. It should create a motion to call makeArbitraryTransactions on the Colony contract.

To get the value for action, it can use the contract ABI to create an ethers interface and then encode the call with encodeFunctionData (note there will be "2 layers" of encoding, one for the makeArbitraryTransactions's actions and the other for motion's action).

The existing block-ingestor motion handler might need to be modified to store additional data in the database.

The saga should be wired in with the action form.

Tip

Block ingestor PR: JoinColony/block-ingestor#304

Testing

Step 1. Update src/components/v5/common/ActionSidebar/partials/forms/ArbitraryTxsForm/partials/AddTransactionModal/useGenerateABI.ts line 21 to return; (Unfortunately, we can't get a local contract from arbiscan, so we will put it manually 🙌 )
image

Step 2. Enable Reputation Weighted extension (with "Testing governance" settings)
Step 3. "Manage colony" -> "Custom transactions"

Step 4. Create transaction with:
Contract address: 0xeF841fe1611ce41bFCf0265097EFaf50486F5111
ABI:

[{"type":"constructor","payable":false,"inputs":[{"type":"string","name":"_name"},{"type":"string","name":"_symbol"},{"type":"uint8","name":"_decimals"}]},{"type":"event","anonymous":false,"name":"Approval","inputs":[{"type":"address","name":"src","indexed":true},{"type":"address","name":"guy","indexed":true},{"type":"uint256","name":"wad","indexed":false}]},{"type":"event","anonymous":false,"name":"Burn","inputs":[{"type":"address","name":"guy","indexed":true},{"type":"uint256","name":"wad","indexed":false}]},{"type":"event","anonymous":false,"name":"LogSetAuthority","inputs":[{"type":"address","name":"authority","indexed":true}]},{"type":"event","anonymous":false,"name":"LogSetOwner","inputs":[{"type":"address","name":"owner","indexed":true}]},{"type":"event","anonymous":false,"name":"MetaTransactionExecuted","inputs":[{"type":"address","name":"user","indexed":false},{"type":"address","name":"relayerAddress","indexed":false},{"type":"bytes","name":"functionSignature","indexed":false}]},{"type":"event","anonymous":false,"name":"Mint","inputs":[{"type":"address","name":"guy","indexed":true},{"type":"uint256","name":"wad","indexed":false}]},{"type":"event","anonymous":false,"name":"Transfer","inputs":[{"type":"address","name":"src","indexed":true},{"type":"address","name":"dst","indexed":true},{"type":"uint256","name":"wad","indexed":false}]},{"type":"function","name":"DOMAIN_SEPARATOR","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"bytes32"}]},{"type":"function","name":"PERMIT_TYPEHASH","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"bytes32"}]},{"type":"function","name":"allowance","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"src"},{"type":"address","name":"guy"}],"outputs":[{"type":"uint256"}]},{"type":"function","name":"approve","constant":false,"payable":false,"inputs":[{"type":"address","name":"guy"},{"type":"uint256","name":"wad"}],"outputs":[{"type":"bool"}]},{"type":"function","name":"authority","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"address"}]},{"type":"function","name":"balanceOf","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"src"}],"outputs":[{"type":"uint256"}]},{"type":"function","name":"burn","constant":false,"payable":false,"inputs":[{"type":"uint256","name":"wad"}],"outputs":[]},{"type":"function","name":"burn","constant":false,"payable":false,"inputs":[{"type":"address","name":"guy"},{"type":"uint256","name":"wad"}],"outputs":[]},{"type":"function","name":"decimals","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"uint8"}]},{"type":"function","name":"executeMetaTransaction","constant":false,"stateMutability":"payable","payable":true,"inputs":[{"type":"address","name":"_user"},{"type":"bytes","name":"_payload"},{"type":"bytes32","name":"_sigR"},{"type":"bytes32","name":"_sigS"},{"type":"uint8","name":"_sigV"}],"outputs":[{"type":"bytes"}]},{"type":"function","name":"getMetatransactionNonce","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"_user"}],"outputs":[{"type":"uint256","name":"nonce"}]},{"type":"function","name":"locked","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"bool"}]},{"type":"function","name":"mint","constant":false,"payable":false,"inputs":[{"type":"address","name":"guy"},{"type":"uint256","name":"wad"}],"outputs":[]},{"type":"function","name":"mint","constant":false,"payable":false,"inputs":[{"type":"uint256","name":"wad"}],"outputs":[]},{"type":"function","name":"name","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"string"}]},{"type":"function","name":"nonces","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"_user"}],"outputs":[{"type":"uint256","name":"nonce"}]},{"type":"function","name":"owner","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"address"}]},{"type":"function","name":"permit","constant":false,"payable":false,"inputs":[{"type":"address","name":"owner"},{"type":"address","name":"spender"},{"type":"uint256","name":"value"},{"type":"uint256","name":"deadline"},{"type":"uint8","name":"v"},{"type":"bytes32","name":"r"},{"type":"bytes32","name":"s"}],"outputs":[]},{"type":"function","name":"setAuthority","constant":false,"payable":false,"inputs":[{"type":"address","name":"authority_"}],"outputs":[]},{"type":"function","name":"setOwner","constant":false,"payable":false,"inputs":[{"type":"address","name":"owner_"}],"outputs":[]},{"type":"function","name":"symbol","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"string"}]},{"type":"function","name":"totalSupply","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"uint256"}]},{"type":"function","name":"transfer","constant":false,"payable":false,"inputs":[{"type":"address","name":"dst"},{"type":"uint256","name":"wad"}],"outputs":[{"type":"bool"}]},{"type":"function","name":"transferFrom","constant":false,"payable":false,"inputs":[{"type":"address","name":"src"},{"type":"address","name":"dst"},{"type":"uint256","name":"wad"}],"outputs":[{"type":"bool"}]},{"type":"function","name":"unlock","constant":false,"payable":false,"inputs":[],"outputs":[]},{"type":"function","name":"verify","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"_user"},{"type":"uint256","name":"_nonce"},{"type":"uint256","name":"_chainId"},{"type":"bytes","name":"_payload"},{"type":"bytes32","name":"_sigR"},{"type":"bytes32","name":"_sigS"},{"type":"uint8","name":"_sigV"}],"outputs":[{"type":"bool"}]}]

Step 5. Create Action with two transactions (to check that multiple transactions are working), one of transactions should be mint
Step 6. Submit action with Decision method "Reputation"
image

Step 7. Check that in console you can see Motion with Arbitrary transactions:
Screenshot 2024-12-16 at 20 23 48

Step 8. On the finance page (http://localhost:9091/planex/incoming) check that there are no incoming funds (this means that transactions weren't called)
image

Step 9. Fully support motion:
image

Step 10. Run npm run forward-time 1 in console

Step 11. Refresh page and Finalize motion

Step 12. Check finance page http://localhost:9091/planex/incoming that mint function was called and you can see incoming funds:
image

Step 13. Repeat Step 3 - Step 8.
Step 14. Fully reject motion:
image

Step 15. Verify that functions weren't called and that there is nothing new on http://localhost:9091/planex/incoming
image

Any other tests are welcome 🙌

Contributes to #3536

@Nortsova Nortsova self-assigned this Dec 12, 2024
@Nortsova Nortsova requested a review from a team as a code owner December 12, 2024 20:28
@jakubcolony
Copy link
Collaborator

If this PR is not ready for review, can you move it back to draft please?

@Nortsova Nortsova marked this pull request as draft December 13, 2024 12:24
Comment on lines 74 to 206
networkClient: colonyClient.networkClient,
parentDomainNativeId: rootDomain.nativeId,
parentDomainSkillId: rootDomain.nativeSkillId,
domainNativeId: rootDomain.nativeId,
domainSkillId: rootDomain.nativeSkillId,
});
const { skillId } = yield call(
[colonyClient, colonyClient.getDomain],
Id.RootDomain,
);

const { key, value, branchMask, siblings } = yield call(
colonyClient.getReputation,
skillId,
AddressZero,
);

return {
context: ClientType.VotingReputationClient,
methodName: 'createMotion',
identifier: colonyAddress,
params: [
Id.RootDomain,
childSkillIndex,
AddressZero,
encodedAction,
key,
value,
branchMask,
siblings,
],
group: {
key: batchKey,
id: metaId,
index: 0,
},
ready: false,
};
}

const transactionParams = yield getCreateMotionParams();
// create transactions
yield fork(createTransaction, createMotion.id, transactionParams);

if (annotationMessage) {
yield fork(createTransaction, annotateRootMotion.id, {
context: ClientType.ColonyClient,
methodName: 'annotateTransaction',
identifier: colonyAddress,
params: [],
group: {
key: batchKey,
id: metaId,
index: 1,
},
ready: false,
});
}

yield takeFrom(createMotion.channel, ActionTypes.TRANSACTION_CREATED);
if (annotationMessage) {
yield takeFrom(
annotateRootMotion.channel,
ActionTypes.TRANSACTION_CREATED,
);
}

yield initiateTransaction(createMotion.id);

const {
payload: {
receipt: { transactionHash: txHash },
},
} = yield waitForTxResult(createMotion.channel);

yield createActionMetadataInDB(txHash, customActionTitle);

if (annotationMessage) {
yield uploadAnnotation({
txChannel: annotateRootMotion,
message: annotationMessage,
txHash,
});
}

setTxHash?.(txHash);

yield put<AllActions>({
type: ActionTypes.MOTION_ARBITRARY_TRANSACTION_SUCCESS,
meta,
});
} catch (caughtError) {
yield putError(
ActionTypes.MOTION_ARBITRARY_TRANSACTION_ERROR,
caughtError,
meta,
);
} finally {
txChannel.close();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the code almost a duplication of the Root Motion Saga; I tried to reuse the Saga but faced types issues. Any ideas on better solution are welcome :)

Copy link
Contributor

@mmioana mmioana Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nortsova I left the needed changes in my PR review comment 🙌

@Nortsova Nortsova marked this pull request as ready for review December 16, 2024 21:05
@Nortsova Nortsova changed the title [WIP]: feat: implement arbitrary transactions via reputation feat: implement arbitrary transactions via reputation Dec 16, 2024
@Nortsova Nortsova force-pushed the feat/3536-arbitrary-via-reputation branch from c9e339e to 5819a12 Compare December 17, 2024 15:20
mmioana
mmioana previously approved these changes Dec 19, 2024
Copy link
Contributor

@mmioana mmioana left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impressive work on this one @Nortsova 🙌

It all works as expected 💯

Added the early-return
Screenshot 2024-12-19 at 10 53 41

Installed the Reputation extension
Screenshot 2024-12-19 at 10 55 52

Started creating some transactions for the given contract address
Screenshot 2024-12-19 at 10 56 29
Screenshot 2024-12-19 at 10 57 40

Could see the motion got created in the logs
Screenshot 2024-12-19 at 11 00 25

The incoming funds page before the mint operation got executed
Screenshot 2024-12-19 at 11 01 39

Finalised, but realised I didn't use the right transaction for minting the tokens - actually didn't use the right address parameter for the other mint method
Screenshot 2024-12-19 at 11 02 24

So here we go again
Screenshot 2024-12-19 at 11 03 32

And after the motion was Supported and Finalised, could see the incoming funds
Screenshot 2024-12-19 at 11 05 31

Tried the scenario of rejecting a motion
Screenshot 2024-12-19 at 11 06 26
Screenshot 2024-12-19 at 11 06 48
Screenshot 2024-12-19 at 11 07 15

And the incoming funds page didn't change after the motion was Rejected and Finalised
Screenshot 2024-12-19 at 11 07 26

Refactoring to use the root motion saga

@Nortsova if you want to make use of the ROOT_MOTION saga you can try out these changes.
Save the below in a .diff and run git apply .diff

diff --git a/src/components/v5/common/ActionSidebar/partials/forms/ArbitraryTxsForm/hooks.ts b/src/components/v5/common/ActionSidebar/partials/forms/ArbitraryTxsForm/hooks.ts
index bce215888..e5f3474c6 100644
--- a/src/components/v5/common/ActionSidebar/partials/forms/ArbitraryTxsForm/hooks.ts
+++ b/src/components/v5/common/ActionSidebar/partials/forms/ArbitraryTxsForm/hooks.ts
@@ -1,11 +1,14 @@
 import { Id } from '@colony/colony-js';
+import { Interface } from 'ethers/lib/utils';
 import { useMemo } from 'react';
 import { useWatch } from 'react-hook-form';
 
 import { useColonyContext } from '~context/ColonyContext/ColonyContext.ts';
 import { ActionTypes } from '~redux/index.ts';
+import { RootMotionMethodNames } from '~redux/types/actions/motion.ts';
 import { DecisionMethod } from '~types/actions.ts';
 import { mapPayload } from '~utils/actions.ts';
+import { extractColonyRoles } from '~utils/colonyRoles.ts';
 import { extractColonyDomains } from '~utils/domains.ts';
 import { sanitizeHTML } from '~utils/strings.ts';
 import { DECISION_METHOD_FIELD_NAME } from '~v5/common/ActionSidebar/consts.ts';
@@ -30,7 +33,7 @@ export const useCreateArbitraryTxs = (
     actionType:
       decisionMethod === DecisionMethod.Permissions
         ? ActionTypes.CREATE_ARBITRARY_TRANSACTION
-        : ActionTypes.MOTION_ARBITRARY_TRANSACTION,
+        : ActionTypes.ROOT_MOTION,
     validationSchema,
     getFormOptions,
     defaultValues: useMemo(
@@ -45,18 +48,38 @@ export const useCreateArbitraryTxs = (
       const commonPayload = {
         annotationMessage: safeDescription,
         customActionTitle: payload.title,
-        transactions: payload.transactions,
         colonyAddress,
       };
 
       if (payload.decisionMethod === DecisionMethod.Reputation) {
+        const contractAddresses: string[] = [];
+        const methodsBytes: string[] = [];
+
+        payload.transactions.forEach(({ contractAddress, ...item }) => {
+          try {
+            const encodedFunction = new Interface(
+              item.jsonAbi,
+            ).encodeFunctionData(
+              item.method,
+              item.args?.map((arg) => arg.value),
+            );
+            contractAddresses.push(contractAddress);
+            methodsBytes.push(encodedFunction);
+          } catch (e) {
+            console.error(e);
+          }
+        });
+
         return {
           ...commonPayload,
+          motionParams: [contractAddresses, methodsBytes, true],
+          operationName: RootMotionMethodNames.MakeArbitraryTransaction,
           colonyDomains: extractColonyDomains(colony.domains),
+          colonyRoles: extractColonyRoles(colony.roles),
         };
       }
 
-      return commonPayload;
+      return { ...commonPayload, transactions: payload.transactions };
     }),
   });
 };
diff --git a/src/redux/types/actions/motion.ts b/src/redux/types/actions/motion.ts
index adb7f8e8d..3de7c0a85 100644
--- a/src/redux/types/actions/motion.ts
+++ b/src/redux/types/actions/motion.ts
@@ -39,6 +39,7 @@ export enum RootMotionMethodNames {
   MoveFunds = 'moveFunds',
   Upgrade = 'upgrade',
   UnlockToken = 'unlockToken',
+  MakeArbitraryTransaction = 'makeArbitraryTransactions',
 }
 
 export interface ExpenditureFundMotionPayload extends ExpenditureFundPayload {

And this should do the trick 🙌

However, I'm okay even with using a separate saga for this, so I'll approve your PR. Nice work! 🥇

@Nortsova Nortsova marked this pull request as draft December 19, 2024 11:44
@Nortsova
Copy link
Contributor Author

Thanks, @mmioana 🙌 Me and @jakubcolony discussed a couple of solutions, and this one as well; we decided that we don't want to overcomplicate UI logic and want to leave transformation inside Saga. But now, with new changes that @jakubcolony did in #3966, it has become more complicated to reuse ROOT_MOTION. ❤️

@Nortsova Nortsova force-pushed the feat/3536-arbitrary-via-reputation branch from 5819a12 to 71cc9f1 Compare December 19, 2024 15:19
@Nortsova Nortsova marked this pull request as ready for review December 19, 2024 15:20
Base automatically changed from feat/arbitrary-txs to master December 19, 2024 15:38
@jakubcolony jakubcolony dismissed mmioana’s stale review December 19, 2024 15:38

The base branch was changed.

@Nortsova Nortsova requested a review from mmioana December 19, 2024 22:35
mmioana
mmioana previously approved these changes Dec 20, 2024
Copy link
Contributor

@mmioana mmioana left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nortsova it makes sense and I think it's even more clear to keep the sagas separate. Just noticed your previous comment and wanted to try it out 🙌

Awesome work, it still works as expected after the rebase

Installed the extension
Screenshot 2024-12-20 at 09 51 52

Created a Custom transactions using Reputation
Screenshot 2024-12-20 at 09 52 59
Screenshot 2024-12-20 at 09 54 13

Incoming page before finalising the staked motion
Screenshot 2024-12-20 at 09 53 25
Screenshot 2024-12-20 at 09 54 59

Incoming page after finalising the staked motion
Screenshot 2024-12-20 at 09 55 21

Created another Custom transactions with Repuation
Screenshot 2024-12-20 at 09 56 03

Rejected it
Screenshot 2024-12-20 at 09 56 34

And the Incoming funds page remained unchanged 👍
Screenshot 2024-12-20 at 09 56 47

Really cool work on this one @Nortsova 🥇

bassgeta
bassgeta previously approved these changes Dec 20, 2024
Copy link
Contributor

@bassgeta bassgeta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice and straightforward 💯
I actually fully support this new arbitratyTx saga, I wouldn't merge it with rootMotion, since that one is already doing 6 different things 😅 But if this turns out super simple, we can merge them down the line.
Filled out the form nicely ✔️
image
Motion data is logged ✔️
image
image
Fully supported it and finalized ✔️
image
image
Incoming funds 👀
image

When rejecting, nothing happens 👌
image
image
Just the previous funds incoming 👀
image

@Nortsova Nortsova dismissed stale reviews from bassgeta and mmioana via 7db7bd4 January 8, 2025 12:14
@Nortsova Nortsova force-pushed the feat/3536-arbitrary-via-reputation branch from 71cc9f1 to 7db7bd4 Compare January 8, 2025 12:14
Copy link
Contributor

@iamsamgibbs iamsamgibbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job getting this working so nicely! It works exactly as expected when creating a motion in the root domain:

Screen.Recording.2025-01-09.at.16.39.33.mov

No incoming funds:

Screenshot 2025-01-09 at 16 40 54

Then after finalising the funds appear:

Screen.Recording.2025-01-09.at.16.41.45.mov

Only one very minor change, the Created in field should be readonly as it is only possible to create an arbitrary transaction in the root domain.

Screenshot 2025-01-09 at 17 05 24

Comment on lines 153 to 154
case ColonyActionType.MakeArbitraryTransactionsMotion:
// @NOTE: Enabling expenditure-related motions above temporarily (action UI will be missing)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this comment is still relevant, might be worth moving the arbitrary motion above the expenditure ones just in case.

jakubcolony
jakubcolony previously approved these changes Jan 9, 2025
Copy link
Collaborator

@jakubcolony jakubcolony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smashing work @Nortsova! Apart from the Created in field mentioned by Sam, everything looks & works great 💯

image image image

{
customActionTitle: string;
colonyAddress: Address;
colonyName?: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems unused

@Nortsova
Copy link
Contributor Author

Thanks guys! Fixed 🙌

Copy link
Contributor

@iamsamgibbs iamsamgibbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks for the changes!

Copy link
Contributor

@bassgeta bassgeta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reapproving after rebase, let's go 🚢

@Nortsova Nortsova merged commit aed9401 into master Jan 13, 2025
2 checks passed
@Nortsova Nortsova deleted the feat/3536-arbitrary-via-reputation branch January 13, 2025 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants